La généricité en C++

© 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

Sommaire

Introduction
Cas le plus simple
Fonctions génériques
Classes génériques
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
 

Introduction

    En C++, la généricité est obtenue par le mot réservé template que l'on peut traduire par "patron" ou "gabarit".

Cas le plus simple

    Considérons la déclaration suivante :
 
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;

Sommaire

Fonctions génériques

    Une fonction générique est obtenue en faisant précéder sa déclaration de :
 
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)
{
    // ...
    cout << "min. de " << 1 << " et " << 2 << " = "
         << min (1, 2) << endl;
    cout << "min. de " << 1.1 << " et " << 2.1 << " = "
         <<  min (1.1, 2.2) << endl;
    return 0;

} // 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 ()
{
    cout << "min. de " << 1 << " et " << 2
         << min (1, 2) << endl;                                // {1}
    cout << "min. de " << 1.0 << " et "  << 2.0
         << min (1.0, 2.0) << endl;
    return 0;
}

int min (const int i, const int j)
{
    int prov;
    cout << (prov = (i < j) ? i : j);
    return  prov;
}

    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
{
    return (U*) t;
}

    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 ()
{
    short sh;
    double db;
    cout << ff (3, sh) << ' ' << ff (3, db) << endl;
}

    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()).

Sommaire

Classes génériques

    Nous avons vu plus haut la possibilité de créer simplement une classe générique. Toutes les fonctions membres d'une classe générique sont aussi génériques. Si elles sont définies en dehors de la déclaration de la classe, elles doivent donc être présentées comme telles :
 
template <class T >
class CX
{
    T m_Val;
    //...
  public :
    T GetVal () const;

}; // CX

template <c1ass T>
T CX <T>:: GetVal () const { return m_Val; }

Sommaire

Paramètres de généricité d'une classe

    Il est possible d'utiliser des expressions de quelque type que ce soit comme paramètre de généricité d'une classe. Par exemple :
 
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.

Sommaire

Spécialisation d'une classe générique

    Certaines fonctions membres d'une classe générique peuvent être trop générales ou mal adaptées à certains cas particuliers prévisibles. Considérons la classe générique suivante :
 
template <class T>
class CSave
{
    T m_Valeur;

  public :
    CSave (const T & t) : m_Valeur (t) {}
    void Editer (void) const ( cout << m_Valeur << endl;  }

}; // CSave

typedef char * pChar_t;                                        // {1}

void Essl()
{
    CSave <int> Savel (3);
    Savel.Editer ();

    pChar_t p = new char [20];
    strcpy (p, "bonjour");
    CSave <pChar_t> Save2 (p);
    Save2.Editer();
    strcpy (p, "au revoir"),
    Save2.Editer();

} // 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.

Sommaire

Membres statiques d'une classe générique

    Chaque instanciation de la classe générique instancie en même temps une donnée membre statique qui lui est propre.

Sommaire

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique