Nous sommes maintenant capables d'exprimer plusieurs types de relations entre objets:
Nous allons voir dans ce chapitre comment exprimer
la relation: Est une liste de, ou Est
un tableau de,
etc.; nous avons défini au début du cours une
classe appelée
complexe
, qui comprend essentiellement deux
membres
privés, r
et i
.
Or, ces deux nombres étaient
définis comme des float
, ce qui
peut paraître un peu
restrictif... Ne faudrait-il pas plutôt définir
les parties réelle et
imaginaire des complexes comme des double, pour avoir une meilleure
précision lors des calculs numériques ? Nous ne
voulons pas avoir à choisir, en
tous cas pas au moment d'écrire l'objet
complexe
: les algorithmes
utilisés seront les mêmes, quelque soit le type
des champs utilisés pour la
partie réelle et pour la partie imaginaire. D'où
la notion de
modèles.
Les modèles sont très utilisés pour créer des objets de type conteneurs: ce sont des objets qui vont en "contenir" d'autres. Nous en reparlerons dans le chapitre sur la bibliothèque standard
La fonction min
ci-dessous renvoie simplement le plus petit de deux nombres... ou de deux objets:
template <typename T> min(const T& a,const T& b) { return a > b ? b : a; }
La fonction peut être utilisée avec n'importe quel objet (ou presque):
int i=4,j=6; int m = min(i,j); float x=3.14, y=5.65; float z= min(x,y); string s1="houlala", s2="aieaieaie"; string s = min(s1, s2); complexe c1=0, c2=1; complexe c = min(c1,c2); // NON !!! Ne passe pas à la compilation !
Dans le code ci-dessus, quatre versions de la même fonction ont été compilées. On dit qu'elles ont été "instantiées". Le compilateur
a choisi la fonction à instantier à partir du type des arguments. Cependant, la quatrième a abouti à une erreur de compilation,
car l'opérateur < n'est pas défini pour la classe complexe
.
Le code suivant aboutit lui aussi à une erreur de compilation:
int i=4; float y=3.14; cout << min(i,y) << endl;
En effet, le compilateur n'a aucun moyen de savoir quelle fonction template doit être utilisée, il se retrouve devant une ambiguïté. Le programmeur doit dans ce cas soit forcer une conversion, soit imposer le type d'instantiation. Cela s'écrit de la manière suivante:
int i=4; float y=3.14; cout << min<float>(i,y) << endl;
Voici la nouvelle définition de la classe complexe
,
en
utilisant des modèles:
template<typename NUM=float> class complexe {
public:
complexe(NUM x=0, NUM y=0);
complexe(const complexe<NUM> & );
~complexe();
operator NUM();
complexe<NUM> & operator=(const complexe<NUM> &);
complexe<NUM> & operator+= (const complexe<NUM> &);
NUM get_r() const { return r;};
NUM get_i() const { return i;};
void set_r(NUM x) { r=x; m_flg=false;};
void set_i(NUM x) { i=x; m_flg=false;};
NUM get_m() const;
static void set_debug() { debflg=true;};
static void clr_debug() { debflg=false;};
private:
NUM r;
NUM i;
mutable bool m_flg;
mutable NUM m;
static bool debflg;
void _calc_module() const {m=sqrt(r*r+i*i);};
};
Le nom de classe est précédé
par le mot-clé
template<typename NUM>
(que l'on
traduit
par modèle,ou patron),
ce qui
signifie qu'un certain type sera
utilisé lors de
l'instantiation de cette classe. D'autre part, dès que le
nom
complexe est utilisé pour désigner la classe qui
est en train d'être
définie, on doit citer le
paramètre, d'où la notation un peu
lourde complexe<NUM>
partout
dans la définition des
paramètres.
L'expression
typename NUM
, peut aussi s'écrire class NUM
. Dans ce contexte, class
signifie en fait
"n'importe quel type"; La notation typename
, plus récente, est donc bien meilleure et doit être utilisée dans les nouveaux codes.
Le type est
complexe<T>
, on fera référence à ce type complet lorsqu'on
annoncera par exemple un type de retour de fonction. Par contre, le constructeur a pour nom complexe
,
le destructeur ~complexe
.
Définir une fonction en-dehors de la portée de la déclaration de classe
revient à définir un modèle de fonction:
template <typename NUM> complexe<NUM>::~complexe()
Ce qui signifie:
NUM
complexe<NUM>
~complexe
(le destructeur, en l'occurrence)Dans le code ci-dessus, on remarque la structure
typename NUM=float
, qui revient à donner à NUM
une valeur par défaut (float
en l'occurrence).
Le programme principal ressemblera à ce qui suit:
typedef complexe<> complexe_float; typedef complexe<double> complexe_double; main() { complexe_float F; complexe_double D; }
Le typedef
ne fait rien d'autre que de déclarer un synonyme. Autrement
dit, il ne crée pas un nouveau type. Il n'est pas indispensable, mais simplement très souhaitable pour la
lisibilité du code: en effet, la syntaxe complexe<float>
se révèle difficilement lisible.
Dans le cas des flottants, il suffit de déclarer complexe<>
, le symbole <>
étant là pour rappeler qu'il s'agit bien d'un modèle.
On peut mettre plusieurs paramètres dans les modèles. Ces paramètres peuvent être de deux sortes:
typename
suivi d'un nom de type existant.int
suivi d'un nombre, pour exprimer par exemple une dimension.La classe tableau
définie ci-dessus
peut être réécrite en utilisant un modèle:
template<size_t TAILLE> class tableau { private: int buffer[TAILLE]; void copie(const tableau<TAILLE> &); public: tableau(){}; tableau(const tableau &); tableau<TAILLE> & operator=(const tableau<TAILLE> &); ~tableau() {}; }; template<size_t TAILLE> tableau<TAILLE>::tableau(const tableau & b ) { copie(b); cout << "copie effectuee par constructeur de copie\n"; }; template<size_t TAILLE> void tableau<TAILLE>::copie(const tableau<TAILLE> & b) { memcpy(buffer,b.buffer,TAILLE); };
Cette implémentation est intéressante,
parce qu'on n'a plus besoin d'utiliser l'opérateur new
dans le constructeur. Du coup, on n'a plus besoin non plus du "trio infernal"
Par contre, on ne peut plus choisir la taille du tableau lors de l'exécution du code, celle-ci doit être fixée à
la compilation. Il résulte de tout ça que l'implémentation est finalement bien plus simple:
template<size_t TAILLE> class tableau { private: int buffer[TAILLE]; };
Si A et B sont des objets de type tableau, on pourra écrire:A=B, le compilateur saura générer automatiquement l'opérateur =. Bien sûr, dans une vraie implémentation on définira également les opérateurs[].
Il n'est pas toujours possible de s'en tenir à
l'écriture du code général, tel que le
modèle l'implémente: ne
serait-ce que pour des raisons de performance, il est parfois
nécessaire de réécrire le code pour
certains types particuliers. Par exemple, une pile d'objets peut être
implémenté sous forme de modèle mais on conçoit qu'une pile
de booléens peut être écrite différemment, en utilisant le fait que des booléens ne tiennent pas sur un ou plusieurs octets,
mais sur un bit
. On écrira cette spécialisation de la manière suivante:
template<> class pile<bool> { }
On peut aussi spécialiser partiellement les modèles. Par exemple, la classe générale:
template<typename T1, typename T2> class Machin { ... }
peut être spécialisée de plusieurs manières:
// Les deux paramètres ont le même type template<typename T> class Machin<T,T> { ... } // Le second type est double template<typename T1> class Machin<T1,double> { ... } // On travaille avec des pointeurs template<typename T1, typename T2> class Machin<T1 *, T2 *> { ... }
Les modèles permettent de définir des objets de manière extrêmement générale, en ce sens ils constituent un outil très puissant. Mais, chaque médaille ayant son revers, ils sont d'une utilisation assez délicate. Il semble d'ailleurs que, encore actuellement, tous les compilateurs n'implémentent pas les modèles de manière complètement identique, ce qui n'est pas pour faciliter les choses...
Il est important de connaître la syntaxe des modèles, car elle est employée en permanence dans la bibliothèque standard: cela est tout-à-fait compréhensible, dans la mesure où une bonne partie de la bibliothèque standard est constituée de "conteneurs" et d'algorithmes associées, c'est-à-dire d'objets encapsulant des structures de données: seuls les modèles permettent de les décrire de manière générale
Quelques conseils, pour ne pas se noyer dans les modèles:
typedef
,
afin de n'entrer qu'une seule fois le modèle
lui-même avec toute sa complexité.int
, par exemple)
ou des constantes littérales. Mettez au point votre code, alors
seulement remplacez vos types et vos constantes par des
paramètres, afin de gagner en
généralité. Cette approche progressive
permettra d'isoler les problèmes liés
à l'utilisation des modèles, et les
problèmes plus classiques liés à tout
programme en cours d'écriture.