Programmation • IoT

Résumé – Chap 9 – La classe Matrice<T>

La première version de Matrice stockait uniquement des coefficients de type double. Créer ensuite une classe pour les int, une autre pour les float et ainsi de suite obligerait à recopier les mêmes algorithmes. Le but du chapitre est donc de transformer cette classe déjà complète en Matrice<T>, une famille de classes dont le type des coefficients est choisi à la compilation.

Cette transformation ne consiste pas à remplacer aveuglément tous les types par T. Les dimensions et les indices restent des int, car ils décrivent la structure de la matrice. En revanche, le tableau de coefficients devient un T*, getCoefficient() retourne un T et setCoefficient() reçoit une valeur de type T. Il faut distinguer ce qui dépend réellement du contenu de ce qui appartient au fonctionnement général de la classe.

L’allocation dynamique suit exactement la même logique qu’auparavant : le constructeur réserve maintenant un tableau avec new T[nombreLignes * nombreColonnes]. Le constructeur de copie crée toujours une nouvelle zone mémoire, l’opérateur d’affectation protège l’auto-affectation puis remplace l’ancien tableau, et le destructeur appelle toujours delete[]. Le template change le type des objets stockés, mais ni leur durée de vie ni la nécessité d’une copie profonde.

Les définitions des méthodes doivent préciser à la fois template <typename T> et le nom complet de la classe, par exemple Matrice<T>::Matrice(...). Elles sont placées dans un fichier Matrice.tpp inclus à la fin de Matrice.h. Ce choix permet de séparer visuellement déclarations et définitions tout en laissant le code complet visible au compilateur lorsqu’il doit fabriquer une instanciation.

Plusieurs opérations deviennent génériques presque mécaniquement. Lire ou modifier un coefficient emploie désormais T, tandis que la transposition déplace les valeurs sans effectuer de calcul sur elles et retourne une nouvelle Matrice<T>. Même l’affichage reste identique, à condition que le type choisi puisse être envoyé vers std::cout avec l’opérateur <<.

La multiplication révèle en revanche une hypothèse cachée de l’ancienne version : l’accumulateur était déclaré par double somme = 0.0. Dans une matrice générique, il doit avoir le même type que les coefficients. L’écriture T somme{} l’initialise avec une valeur adaptée à T et évite d’imposer artificiellement une conversion depuis un double ou même depuis l’entier zéro.

Cette correction montre pourquoi un template n’est pas automatiquement utilisable avec n’importe quel type. Pour multiplier des matrices, T doit pouvoir être initialisé, multiplié et additionné avec +=. Pour afficher la matrice, il doit être compatible avec le flux de sortie. Le contrat d’un template se lit donc dans les opérations effectuées par ses méthodes : rendre un type paramétrable ne fait pas disparaître les exigences de l’algorithme.

Chaque choix de T produit par ailleurs un véritable type distinct. Matrice<int> et Matrice<double> utilisent le même modèle, mais ce ne sont pas deux variantes interchangeables d’un même objet. La conversion naturelle d’un int en double ne crée pas automatiquement une conversion entre ces deux matrices ; il faudrait programmer explicitement une telle possibilité.

La généralisation se prolonge dans l’héritage avec MatriceCarree<T> : public Matrice<T>. La classe dérivée transmet le même paramètre à sa classe de base et son constructeur appelle Matrice<T>(ordre, ordre). Ainsi, une matrice carrée d’entiers est bien une matrice d’entiers, et son mineur conserve lui aussi le type de ses coefficients.

L’héritage d’une base dépendante d’un paramètre template introduit une particularité de syntaxe. Pour certains membres hérités, il faut écrire par exemple this->getNombreLignes() afin d’indiquer au compilateur que le nom doit être recherché dans l’objet courant et donc dans Matrice<T>. Cette contrainte vient de l’analyse des templates, non du raisonnement mathématique.

Le déterminant reste ici déclaré en double, choix pratique pour les types numériques étudiés mais qui rappelle qu’une classe générique peut encore contenir des décisions non génériques. Étendre la matrice à des nombres complexes, par exemple, obligerait à réexaminer ce type de retour aussi bien que les opérations utilisées dans le calcul.

Au terme de cette évolution, Matrice<T> réunit encapsulation, allocation dynamique, copie profonde, surcharge d’opérateurs, héritage et programmation générique. Les templates évitent la duplication sans affaiblir le typage du C++, mais ils obligent à rendre explicites les hypothèses autrefois dissimulées derrière double. La classe gère encore manuellement ses ressources avec new[] et delete[], ce qui prépare naturellement l’étude suivante : confier cette responsabilité à des objets grâce au RAII et aux outils du C++ moderne.