TD1 - Template based class

Nous vous proposons de développer une classe template SquareMatrix paramétrée par

  • Le type des valeurs qu’elle contient

  • Sa dimension

Vous allez implémenter les fonctionnalités dans l’ordre demandé en les validant à chaque fois par des tests.

Constructeurs et affichage

Complétez le code suivant pour que la classe SquareMatrix soit fonctionnelle :

  • Un constructeur par défaut sans initialisation des valeurs.

  • Un constructeur initialisant la matrice ligne par ligne à partir de valeurs entières passées en paramètre.

  • Une fonction d’affichage print().

template ...
class SquareMatrix
{
  private:
        // données internes
        ...

  public:

        // Constructeur sans argument
        SquareMatrix()    {}

        // Constructeur recevant une série de valeurs
        SquareMatrix(const array<...> & Data) {...}

        // affichage
        void print() {...}
}

L’affichage produit par la fonction print est optimisée pour les nombres entiers entre 0 et 99. Le format d’affichage à respecter est le suivant :

  • Deux colonnes par valeur.

  • Une colonne d’espacement entre les valeurs.

Vous devez mettre en place un code capable d’exécuter la fonction test1() fournie ci-dessous. Vous devez obtenir un affichage identique.

const int N=3;
using SM3 = SquareMatrix<int, N>;

void test1()
{
        // Initialisation avec valeurs
        SM3 M({ 11,2,3, 4,55,6, 7,8,99 });

        M.print();

        cout << "Fin du test 1------------" << endl;
}

>> 11  2  3
>>  4 55  6
>>  7  8 99

Il faut fixer un type pour le paramètre du constructeur. L’écriture {1,2,3,4,5,6,7,8,9} est compatible avec l’initialisation d’un array (ou d’un vector). Comme nous voulons utiliser cette écriture compacte et pratique, nous choisissons donc le type array pour notre paramètre car il correspond à une liste de taille fixe ce qui est notre contexte. Dans l’écriture :

SquareMatrix(const array<...> & T) {...}

Nous avons utilisé une const référence, pourquoi ?

  • Un utilisateur peut décider de passer un objet array à cette fonction. Il peut le faire puisqu’elle accepte des array ! Dans cette situation, la référence évitera une recopie.

  • Le qualificatif const permet d’indiquer au développeur que l’argument passé n’est pas modifié.

Note

Comment peut-on avoir une référence sur un objet alors qu’aucun objet array n’a été créé ? En effet, il y a une création implicite d’un objet anonyme suite à l’appel du constructeur. Cet objet sera associé à la référence.

Opérateurs d’accès

L’opérateur d’indexation [] n’accepte qu’un seul argument. Il n’est donc pas possible en C++ de mettre en place une syntaxe de la forme [i,j].

Il est possible de chaîner deux opérateurs d’indexation en écrivant [i][j] mais cela suppose que l’on créé une classe intermédiaire pour retourner une ligne de la matrice ce qui complexifie un peu l’exercice.

Pour l’instant, nous choisissons l’option la plus simple en surchargeant l’opérateur () et lui transmettant les deux indices i et j : (i,j). Nous vous demandons de respecter la convention suivante :

../_images/matrice.jpg

Version NON-CONST :

// Accès aux éléments (lecture/écriture)
T& operator()(int i, int j) { ... }

Cet opérateur retourne une référence alors que nous avons conseillé plusieurs fois précédemment de retourner des objets temporaires ! En effet, cette affirmation avait été faite dans le cas du retour d’un objet créé dans la corps de l’opérateur ET on ne peut retourner une référence vers un objet local qui s’apprête à être détruit. Dans notre cas, on peut retourner une référence car les données de la matrice sont pérennes. Cette référence permet d’éviter une recopie et elle sert de l-value dans la syntaxe suivante :

M(i,j) = 5;

Version CONST :

const T& operator()(int i, int j) const { ... }

On pourrait se servir uniquement de la version non-const mais à un moment, C++ oblige, le compilateur va finir par se plaindre ! En effet, une fonction peut prendre en argument une const référence vers une matrice, elle a le droit et c’est judicieux si par exemple elle ne fait qu’afficher ses valeurs. Cependant, la const référence ne permet d’utiliser que les membres const de la classe matrice, ceux qui sont déclarés comme ne modifiant pas les données internes. Par conséquent, dans ce cas, il faut pouvoir disposer d’un accesseur const sous peine de ne pas pouvoir faire grand chose :)

Vous devez mettre en place une classe capable d’exécuter le code de test fourni ci-dessous :

const int N=3;
using SM3 = SquareMatrix<int, N>;


void fnt2(const SM3 & M)
{
        cout << M(0, 0);
}

void test2()
{
        // Exemple d'utilisation
        SM3 M;

        for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                        M(i, j) = i * i + j;

        for (int i = 0; i < N; i++)
                for (int j = 0; j < N; j++)
                        if ( M(i, j) != i * i + j )
                                cout << "ERREUR";

        fnt2(M);
        cout << "Test 2 terminé" << endl;
}

Initialiseurs supplémentaires

Ajoutez à la classe matrice les fonctions suivantes :

  • Une fonction membre rand() permettant d’initialiser la matrice actuelle avec des entiers choisis aléatoirement entre 0 et 99.

  • Une fonction membre diag() initialisant la matrice pour qu’elle corresponde à une matrice diagonale.

  • Une fonction membre fill(…) qui initialise les valeurs de la matrice à partir de la valeur fournie en argument.

Effectuez des affichages pour contrôler votre travail.

Transposition

Ajoutez :

  • Une fonction membre transpose() qui transpose les valeurs actuelles de la matrice (inplace).

  • Une fonction externe … t(…) qui retourne une nouvelle matrice correspondant à la transposée de la matrice passée en paramètre.

Écrivez un test pour valider votre implémentation.

Affichage

A partir de maintenant, toutes les fonctions ajoutées dans le code sont des fonctions externes à la classe (pas de fonction membre).

Surchargez l’opérateur << pour afficher le contenu de la matrice dans le même format que précédemment.

ostream& operator << (ostream& os, ... )
{
        ...
}

Lorsque l’on écrit : cout << M, l’argument de gauche correspond à un objet de type ostream. Il ne faut pas le recopier, ce qui explique le passage par référence. Le paramètre de droite correspond à la matrice transmise lors de l’appel. Choisissez judicieusement le type associé. N’oubliez pas de chaîner votre opérateur en retournant l’objet cout afin de permettre l’écriture suivante : cout << M1 << M2 << endl;

Écrivez un test pour valider votre implémentation.

Note

Vous pouvez appeler la fonction print() dans cet opérateur plutôt que d’effectuer un copié-collé.

Opérateurs

Mettez en place les opérateurs suivant :

  • Addition et soustraction de deux matrices

  • Multiplication entre deux matrices

  • Multiplication d’une matrice par un scalaire (gauche et droite)

Comme nous créons des opérateurs externes (non membre de la classe), ils ne peuvent accéder aux données privées et doivent donc utiliser les accesseurs publics. Cependant, si vous voulez accéder aux données privées depuis une classe, un opérateur ou une fonction externe, alors il faut déclarer cette entité comme amie. Pour cela, on écrit la déclaration de l’entité dans la classe SquareMatrix en ajoutant le qualificatif friend, comme ceci :

template ...
class SquareMatrix
{
    ...

        template ... friend ... operator+(...,...);
};

Test final

Grâce à votre code, effectuez le calcul suivant :

\[A = A \cdot A^t + 2 \cdot A - A \cdot A\]

Utilisez la matrice A= [ [1,2,3], [2,3,4], [3,4,5]] pour tester votre résultat. Vous devez obtenir :

>>  2  4  6
>>  4  6  8
>>  6  8 10