© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 20/12/1999 - Dernière mise à jour : 13/01/2000
Paramètres de généricité d'une classe
Spécialisation d'une classe générique
Membres statiques d'une classe générique
typedef ... T;
class CMaClasse { // utilise le type T }; // CMaClasse |
Si on désire créer une classe qui effectue
les mêmes traitements pour un type Q, il faut dupliquer
complètement le code de la classe. Une solution beaucoup plus élégante
consiste à déclarer la classe "générique" pour
le type T :
template <class T>
class CMaClasse : { // utilise le "type" T }; // CMaClasse |
Contrairement aux apparences, le type générique
qui servira à instancier la classe n'est pas obligatoirement une
classe définie par le mot réservé class,
mais peut être n'importe quel type, prédéfini ou utilisateur
(sous réserve bien sûr qu'il soit compatible avec les opérations
définies dans la classe CMaClasse). La classe générique
pourra être instanciée par exemple dans la déclaration
suivante :
CMaClasse <int> Objetl;
CMaClasse <CTree> Objet2; |
template <liste_de_parametres_generiques> |
Chaque élément de la liste des paramètres
formels de généricité est l'identificateur d'une classe
ou d'un type, précédé du mot réservé
class.
Contrairement aux classes génériques, l'instanciation des
fonctions génériques est faite automatiquement par le compilateur
au fur et à mesure de ses besoins. Dans l'exemple très classique
ci-dessous :
template <class T> T min (const T a, const T b) { return (a
< b) ? a : b; }
int main (void)
} // main() |
lorsque le compilateur rencontre l'appel min(1, 2), il recherche s'il connaît une fonction ayant le même profil (deux ints constants comme paramètres). Si tel est le cas il l'utilise. Sinon il recherche s'il connaît une fonction générique ayant le même profil. Si tel est le cas il l'instancie avec le type int. A la rencontre de l'appel min(1.1, 2.2 ), il agit de même avec le type float. L'utilisateur n'a donc pas à se soucier si la fonction a déjà été instanciée ou non.
En réalité le compilateur n'instancie
pas immédiatement la fonction générique quand il en
a besoin, il se contente de la repérer, puis continue son activité.
Ce n'est qu'à l'édition de liens qu'il instancie deux fois
la fonction min(), avec des entiers et avec des réels,
si toutefois il n'a pas rencontré entre-temps une fonction ordinaire
ayant le même profil. Dans ce cas c'est cette dernière qu'il
utilise. Ainsi, une fonction ordinaire peut "surcharger" une fonction template,
même si elle est définie ultérieurement, comme dans
l'exemple ci-dessous :
template <class T>
inline T min (const T a, const T b) { return (a < b) ? a : b }; int main ()
int min (const int i, const int j)
|
C'est la deuxième fonction min() qui est utilisée pour les entiers dans l'instruction {1}. Noter la position du spécificateur inline.
Aucune conversion implicite de type des paramètres
effectifs génériques n'est effectuée. En revanche,
le résultat de la fonction peut naturellement être converti
si nécessaire (et si la conversion est possible) :
int crete = 5;
template <class T> T min (const T a, const T b); short ecreter (const short i) // return min (crete, i); // faux : crete est un int, i est un short // return min (5, i); // faux : 5 est un int; i est un short // return min (i, 5); // faux : i est un short, 5 est un int return min (5, (int) i); // correct {1} return min (short (crete), i); // correct {2} |
Dans le cas {1} c'est une instance de min(short, short) qui est générée. Dans le cas {2} c'est une instance de min(int, int). Le résultat de la fonction min() est alors un int qui est ensuite converti en short pour être renvoyé.
Plusieurs paramètres de généricité
différents peuvent être utilisés :
template <class T, class U> U f (const int i, T t, U u); //
correct
template <class T, class T> T g (const int i, T t, T t); // faux |
Les paramètres formels de la fonction peuvent
ne pas tous être des paramètres de généricité
(comme le paramètre i ci-dessus), mais tous les paramètres
de généricité doivent apparaître au moins une
fois dans la liste des paramètres formels de la fonction :
template <class T, class U> void f (const T t)
// faux
{ U local; //... } template <class T, class U> inline U * transtyp (T * t) // faux
|
Une solution est d'ajouter un paramètre formel
fictif : tout se passe comme si le type U était passé
en paramètre :
template <class T, class U> void f (const T t, const U bidon)
// correct
{ U local; } |
Lorsqu'un paramètre n'est pas utilisé
dans une fonction, son identificateur peut être omis (paramètre
anonyme). Dans l'exemple suivant, le second paramètre ne sert qu'à
passer le type de retour de la fonction.
template <class X, class Y> Y ff (const X x, const Y)
{ return x * 1.5; // cette opération est toujours faite sur des // doubles } int main ()
|
Le résultat de l'exécution de ce programme
est :
4 4.5 |
Comme pour les fonctions ordinaires, les fonctions
génériques ne peuvent se distinguer par le type de retour,
puisque le C++ admet les conversion implicites. En revanche, plusieurs
déclarations différentes peuvent désigner la même
fonction générique :
template <class T> T f (T, T);
// ... utilisation de la fonction f avec le type int {1} template <class V> V f (T, T) { //... } {2} template <class U> U f (U, U); // ... utilisation de la fonction f avec le type int {3} |
En {1} et en {3} c'est la fonction générique {2} qui est utilisée, instanciée avec le type int (sauf bien sûr s'il existe une fonction ordinaire f()).
template <class T >
class CX { T m_Val; //... public : T GetVal () const; }; // CX template <c1ass T>
|
template <class T, int size> class CBuffer { /* ... */ }; |
Le paramètre effectif utilisé lors de l'instanciation de la classe générique peut être n'importe quelle expression constante. Elle doit en effet pouvoir être évaluée par le compilateur avant l'édition de lien, en particulier pour lui éviter plusieurs instanciations identiques.
Lorsque plusieurs instanciations sont proposées par le programme avec des expressions différentes mais ayant exactement la même valeur et le même type, le compilateur considère qu'il n'y a qu'une seule instanciation. Il n'y a pas de conversion implicite des paramètres : les valeurs 12 et 12L sont différentes et provoquent donc deux instanciations.
template <class T>
class CSave { T m_Valeur; public :
}; // CSave typedef char * pChar_t; // {1} void Essl()
pChar_t p = new char [20];
} // Essl() |
La modification de l'objet pointé par p
ne devrait pas modifier celle de l'objet Save2. Malheureusement,
c'est seulement le pointeur qui a été copié et non
la chaîne correspondante. Le C++ offre la possibilité
de spécialiser certaines fonctions membres (ici le constructeur)
pour des types particuliers (ici le char*). Ainsi, il suffit d'ajouter
à la ligne {1} une instance particulière du constructeur
:
CSave <pChar_t>::CSave (const pChar_t t)
{ m_Valeur = new char [strlen (t) + 1]; strcpy (m_Valeur, t); } |
Certains compilateurs (BC V4.0 par ex.) ne permettent cependant pas cette nouvelle définition du constructeur. De plus, le problème de la libération de la mémoire (delete) n'est pas résolu. Dans tous les cas il est préférable d'utiliser une classe dotée d'un constructeur, comme string par exemple, qui évitera ce type de problème.
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique