Aller au contenu principal

Templates

Généralités​

Nous avons vu précédemment comment réaliser des structures de données relativement indépendantes de la classe de leurs données (c'est-à-dire leur type) avec les classes abstraites. Par ailleurs, il est faisable de faire des fonctions travaillant sur de nombreux types grâce à la surcharge.

Cependant, l'emploi des classes abstraites est assez fastidieux et a l'inconvénient d'affaiblir le contrôle des types réalisé par le compilateur. De plus, la surcharge n'est pas généralisable pour tous les types de données. Il serait possible d'utiliser des macros pour faire des fonctions non typées, mais cela serait au détriment de la taille du code et du contrôle des types.

Le C++ permet de résoudre ces problèmes grâce aux paramètres génériques, que l'on appelle encore paramètres template. Un paramètre template est soit un type générique, soit une constante dont le type est assimilable à un type intégral. Comme leur nom l'indique, les paramètres template permettent de paramétrer la définition des fonctions et des classes. Les fonctions et les classes ainsi paramétrées sont appelées respectivement fonctions template et classes template.

Les fonctions template sont donc des fonctions qui peuvent travailler sur des objets dont le type est un type générique (c'est-à-dire un type quelconque), ou qui peuvent être paramétrées par une constante de type intégral. Les classes template sont des classes qui contiennent des membres dont le type est générique ou qui dépendent d'un paramètre intégral.

En général, la génération du code a lieu lors d'une opération au cours de laquelle les types génériques sont remplacés par des vrais types et les paramètres de type intégral prennent leur valeur. Cette opération s'appelle l'instanciation des template. Elle a lieu lorsqu'on utilise une fonction ou la classe template pour la première fois. Les types réels à utiliser à la place des types génériques sont déterminés lors de cette première utilisation par le compilateur, soit implicitement à partir du contexte d'utilisation du template, soit par les paramètres donnés explicitement par le programmeur.

Déclaration des paramètres template​

Les paramètres template sont, comme on l'a vu, soit des types génériques, soit des constantes dont le type peut être assimilé à un type intégral.

Déclaration des types template​

Les template qui sont des types génériques sont déclarés par la syntaxe suivante :

template <class|typename nom[=type]
[, class|typename nom[=type]
[...]>

où nom est le nom que l'on donne au type générique dans cette déclaration. Le mot-clé class a ici exactement la signification de "type". Il peut d'ailleurs être remplacé indifféremment dans cette syntaxe par le mot-clé typename (l'inverse n'est pas vrai ailleurs : seul typename permet d'indiquer qu'un nom dépendant d'un paramètre template, comme typename T::value_type, désigne un type). La même déclaration peut être utilisée pour déclarer un nombre arbitraire de types génériques, en les séparant par des virgules. Les paramètres template qui sont des types peuvent prendre des valeurs par défaut, en faisant suivre le nom du paramètre d'un signe égal et de la valeur. Ici, la valeur par défaut doit évidemment être un type déjà déclaré.

template <class T, typename U, class V=int>

Dans cet exemple, T, U et V sont des types génériques. Ils peuvent remplacer n'importe quel type du langage déjà déclaré au moment où la déclaration template est faite. De plus, le type générique V a pour valeur par défaut le type entier int. On voit bien dans cet exemple que les mots-clés typename et class peuvent être utilisés indifféremment.

Lorsqu'on donne des valeurs par défaut à un type générique, on doit donner des valeurs par défaut à tous les types génériques qui le suivent dans la déclaration template. Pour une classe template, la ligne suivante provoquera donc une erreur de compilation : template <class T=int, class V>.

Il est possible d'utiliser une classe template en tant que type générique. Dans ce cas, la classe doit être déclarée comme étant template à l'intérieur même de la déclaration template. La syntaxe est donc la suivante :

template <template <class Type> class Classe [,...]>

où Type est le type générique utilisé dans la déclaration de la classe template Classe. On appelle les paramètres template qui sont des classes template des paramètres template template. Rien n'interdit de donner une valeur par défaut à un paramètre template template : le type utilisé doit alors être une classe template déclarée avec la déclaration template.

template <class T>
class Tableau
{
// Définition de la classe template Tableau.
};

template <class U, class V, template <class T> class C=Tableau>
class Dictionnaire
{
C<U> Clef;
C<V> Valeur;
// Reste de la définition de la classe Dictionnaire.
};

Dans cet exemple, la classe template Dictionnaire permet de relier des clés à leurs éléments. Ces clés et ces valeurs peuvent prendre n'importe quel type. Les clés et les valeurs sont stockées parallèlement dans les membres Clef et Valeur. Ces membres sont en fait des conteneurs template, dont la classe est générique et désignée par le paramètre template template C. Le paramètre template de C est utilisé pour donner le type des données stockées, à savoir les types génériques U et V dans le cas de la classe Dictionnaire. Enfin, la classe Dictionnaire peut utiliser un conteneur par défaut, qui est la classe template Tableau.

Déclaration des constantes template​

La déclaration des paramètres constants se fait de la manière suivante :

template <type paramètre[=valeur][,...]>

où type est le type du paramètre constant, paramètre est le nom du paramètre et valeur est sa valeur par défaut. Il est possible de donner des paramètres template qui sont des types génériques et des paramètres template qui sont des constantes dans la même déclaration.

Le type des constantes template doit obligatoirement être l'un des types suivants :

  • type intégral ou énuméré ;
  • pointeur ou référence d'objet ;
  • pointeur ou référence de fonction ;
  • pointeur sur membre ;
  • std::nullptr_t (depuis C++11).

Ce sont donc tous les types qui peuvent être assimilés à des valeurs entières (entiers, énumérés ou adresses). Depuis C++20, les types flottants et certains types classes littéraux sont également autorisés.

template <class T, int i, void (*f)(int)>

Cette déclaration template comprend un type générique T, une constante template i de type int, et une constante template f de type pointeur sur fonction prenant un entier en paramètre et ne renvoyant rien.

Fonctions et classes template​

Après la déclaration d'un ou de plusieurs paramètres template suit en général la déclaration ou la définition d'une fonction ou d'une classe template. Dans cette définition, les types génériques peuvent être utilisés exactement comme s'il s'agissait de types normaux. Les constantes template peuvent être utilisées pour la fonction ou la classe template comme des constantes locales.

Fonctions template​

La déclaration et la définition des fonctions template se font exactement comme s'il s'agissait de fonctions normales, à ceci près qu'elle doit être précédée de la déclaration des paramètres template. La syntaxe d'une déclaration de fonction template est donc la suivante :

template <paramètres_template>
type fonction(paramètres_fonction);

où paramètres_template est la liste des paramètres template et paramètres_fonction est la liste des paramètres de la fonction. type est le type de la valeur de retour de la fonction, ce peut être un des types génériques de la liste des paramètres template.

Les paramètres template qui sont utilisés dans la liste des paramètres de la fonction peuvent être déduits par le compilateur à partir des types des arguments lors de l'appel. Les autres doivent être précisés explicitement lors de l'appel (par exemple f<int>()) ou avoir une valeur par défaut.

La définition d'une fonction template se fait comme une déclaration avec le corps de la fonction. Il est alors possible d'y utiliser les paramètres template comme s'ils étaient des types normaux : des variables peuvent être déclarées avec un type générique, et les constantes template peuvent être utilisées comme des variables locales qualifiées const. Les fonctions template s'écrivent donc exactement comme des fonctions classiques.

template <class T>
T Min(T x, T y)
{
return x<y ? x:y;
}

La fonction Min ainsi définie fonctionnera parfaitement pour toute classe pour laquelle l'opérateur < est défini. Le compilateur déterminera automatiquement quel est l'opérateur à employer pour chaque fonction Min qu'il rencontrera.

Les fonctions template peuvent être surchargées, aussi bien par des fonctions classiques que par d'autres fonctions template. Lorsqu'une fonction template et une fonction normale qui la surcharge conviennent aussi bien l'une que l'autre pour un appel, la fonction classique est préférée ; la fonction template n'est retenue que si elle fournit une meilleure correspondance.

Une fonction template peut être déclarée amie d'une classe, template ou non, pourvu que cette classe ne soit pas locale. Toutes les instances générées à partir d'une fonction amie template sont amies de la classe donnant l'amitié, et ont donc libre accès sur toutes les données de cette classe.

Les classes template​

La déclaration et la définition d'une classe template se font comme celles d'une fonction template : elles doivent être précédées de la déclaration template des types génériques. La déclaration suit donc la syntaxe suivante :

template <paramètres_template>
class|struct|union nom;

La seule particularité dans la définition des classes template est que si les méthodes de la classe ne sont pas définies dans la déclaration de la classe, elles devront elles aussi être déclarées template :

template <paramètres_template>
type classe<paramètres>::nom(paramètres_méthode)
{
...
}

Il est absolument nécessaire dans ce cas de spécifier tous les paramètres template de la liste paramètres_template dans paramètres, séparés par des virgules, afin de caractériser le fait que c'est la classe qui est template et qu'il ne s'agit pas d'une méthode template d'une classe normale. D'une manière générale, il faudra toujours spécifier les types génériques de la classe entre les signes d'infériorité et de supériorité, juste après son nom, à chaque fois qu'on voudra la référencer. Cette règle est cependant facultative lorsque la classe est référencée à l'intérieur de sa propre définition ou d'une de ses fonctions membres.

Contrairement aux fonctions template non membres, les méthodes des classes template peuvent utiliser des types génériques de leur classe sans pour autant qu'ils soient utilisés dans la liste de leurs paramètres. En effet, le compilateur détermine quels sont les types à identifier aux types génériques lors de l'instanciation de la classe template, et n'a donc pas besoin d'effectuer cette identification avec les types des paramètres utilisés.

template <class T>
class Stack
{
typedef struct stackitem
{
T Item; // On utilise le type T comme
struct stackitem *Next; // si c'était un type normal.
} StackItem;

StackItem *Tete;

public: // Les fonctions de la pile :
Stack(void);
Stack(const Stack<T> &);
// La classe est référencée en indiquant
// son type entre < et > ("Stack<T>").
// Ici, ce n'est pas une nécessité
// cependant.
~Stack(void);
Stack<T> &operator=(const Stack<T> &);
void push(T);
T pop(void);
bool is_empty(void) const;
void flush(void);
};

// Pour les fonctions membres définies en dehors de la déclaration
// de la classe, il faut une déclaration de type générique :

template <class T>
Stack<T>::Stack(void) // La classe est référencée en indiquant
// son type entre < et > ("Stack<T>").
// C'est impératif en dehors de la
// déclaration de la classe.
{
Tete = NULL;
return;
}

template <class T>
Stack<T>::Stack(const Stack<T> &Init)
{
Tete = NULL;
StackItem *tmp1 = Init.Tete, *tmp2 = NULL;
while (tmp1!=NULL)
{
if (tmp2==NULL)
{
Tete= new StackItem;
tmp2 = Tete;
}
else
{
tmp2->Next = new StackItem;
tmp2 = tmp2->Next;
}
tmp2->Item = tmp1->Item;
tmp1 = tmp1->Next;
}
if (tmp2!=NULL) tmp2->Next = NULL;
return;
}

template <class T>
Stack<T>::~Stack(void)
{
flush();
return;
}

template <class T>
Stack<T> &Stack<T>::operator=(const Stack<T> &Init)
{
if (&Init == this) return *this; // Autoaffectation.
flush();
StackItem *tmp1 = Init.Tete, *tmp2 = NULL;

while (tmp1!=NULL)
{
if (tmp2==NULL)
{
Tete = new StackItem;
tmp2 = Tete;
}
else
{
tmp2->Next = new StackItem;
tmp2 = tmp2->Next;
}
tmp2->Item = tmp1->Item;
tmp1 = tmp1->Next;
}
if (tmp2!=NULL) tmp2->Next = NULL;
return *this;
}

template <class T>
void Stack<T>::push(T Item)
{
StackItem *tmp = new StackItem;
tmp->Item = Item;
tmp->Next = Tete;
Tete = tmp;
return;
}

template <class T>
T Stack<T>::pop(void)
{
T tmp;
StackItem *ptmp = Tete;

if (Tete!=NULL)
{
tmp = Tete->Item;
Tete = Tete->Next;
delete ptmp;
}
return tmp;
}

template <class T>
bool Stack<T>::is_empty(void) const
{
return (Tete==NULL);
}

template <class T>
void Stack<T>::flush(void)
{
while (Tete!=NULL) pop();
return;
}

Les classes template peuvent parfaitement avoir des fonctions amies, que ces fonctions soient elles-mêmes template ou non.

Fonctions membres template​

Les destructeurs mis à part, les méthodes d'une classe peuvent être template, que la classe elle-même soit template ou non, pourvu que la classe ne soit pas une classe locale.

Les fonctions membres template peuvent appartenir à une classe template ou à une classe normale.

Lorsque la classe à laquelle elles appartiennent n'est pas template, leur syntaxe est exactement la même que pour les fonctions template non membres.

class A
{
int i; // Valeur de la classe.
public:
template <class T>
void add(T valeur);
};

template <class T>
void A::add(T valeur)
{
i=i+((int) valeur); // Ajoute valeur à A::i.
return ;
}

Si, en revanche, la classe dont la fonction membre fait partie est elle aussi template, il faut spécifier deux fois la syntaxe template : une fois pour la classe, et une fois pour la fonction. Si la fonction membre template est définie à l'intérieur de la classe, il n'est pas nécessaire de donner les paramètres template de la classe, et la fonction membre template s'écrit donc exactement comme une fonction template classique.

template<class T>
class Chaine
{
public:
// Fonction membre template définie
// à l'extérieur de la classe template :

template<class T2> int compare(const T2 &);

// Fonction membre template définie
// à l'intérieur de la classe template :

template<class T2>
Chaine(const Chaine<T2> &s)
{
// ...
}
};

// À l'extérieur de la classe template, on doit donner
// les déclarations template pour la classe
// et pour la fonction membre template :

template<class T> template<class T2>
int Chaine<T>::compare(const T2 &s)
{
// ...
return 0;
}

Les fonctions membres virtuelles ne peuvent pas être template. Si une fonction membre template a le même nom qu'une fonction membre virtuelle d'une classe de base, elle ne constitue pas une redéfinition de cette fonction. Par conséquent, les mécanismes de virtualité sont inutilisables avec les fonctions membres template. On peut contourner ce problème de la manière suivante : on définira une fonction membre virtuelle non template qui appellera la fonction membre template.

class B
{
virtual void f(int);
};

class D : public B
{
template <class T>
void f(T); // Cette fonction ne redéfinit pas B::f(int).

void f(int i) // Cette fonction redéfinit B::f(int).
{
f<>(i); // Elle appelle la fonction template.
return ;
}
};

Dans l'exemple précédent, on est obligé de préciser que la fonction à appeler dans la fonction virtuelle est template, et qu'il ne s'agit donc pas d'un appel récursif de la fonction virtuelle. Pour cela, on fait suivre le nom de la fonction template d'une paire de signes inférieur et supérieur.

Plus généralement, si une fonction membre template d'une classe peut être spécialisée en une fonction qui a la même signature qu'une autre fonction membre de la même classe, et que ces deux fonctions ont le même nom, toute référence à ce nom utilisera la fonction non-template. Il est possible de passer outre cette règle, à condition de donner explicitement la liste des paramètres template entre les signes inférieur et supérieur lors de l'appel de la fonction.

Instanciation des template​

La définition des fonctions et des classes template ne génère aucun code tant que tous les paramètres template n'ont pas pris chacun une valeur spécifique. Il faut donc, lors de l'utilisation d'une fonction ou d'une classe template, fournir les valeurs pour tous les paramètres qui n'ont pas de valeur par défaut. Lorsque suffisamment de valeurs sont données, le code est généré pour ce jeu de valeurs. On appelle cette opération l'instanciation des template.

Plusieurs possibilités sont offertes pour parvenir à ce résultat : l'instanciation implicite et l'instanciation explicite.

Instanciation implicite​

L'instanciation implicite est utilisée par le compilateur lorsqu'il rencontre une expression qui utilise pour la première fois une fonction ou une classe template, et qu'il doit instancier pour continuer son travail. Le compilateur se base alors sur le contexte courant pour déterminer les types des paramètres template à utiliser. Si aucune ambiguïté n'a lieu, il génère le code pour ce jeu de paramètres.

La détermination des types des paramètres template peut se faire simplement ou être déduite de l'expression à compiler. Par exemple, les fonctions template sont instanciées en fonction du type de leurs paramètres. Si l'on reprend l'exemple de la fonction template Min, c'est son utilisation directe qui provoque l'instanciation implicite.

int i = Min(2, 3);         // T est déduit : int.
int j = Min<int>(2, 3.0); // T est forcé à int.

Dans la première ligne, le type int est déduit des arguments. Dans la seconde, les deux arguments ont des types différents et la déduction échouerait : le paramètre template est donc forcé à int, et 3.0 est converti en entier.

On prendra garde au fait que le compilateur utilise une politique minimaliste pour l'instanciation des template. Cela signifie qu'il ne créera que le code nécessaire pour compiler l'expression qui exige une instanciation implicite. Par exemple, la définition d'un objet d'une classe template dont tous les paramètres sont définis provoque l'instanciation de cette classe, mais la définition d'un pointeur sur cette classe ne le fait pas. L'instanciation aura lieu lorsqu'un déréférencement sera fait par l'intermédiaire de ce pointeur. De même, seules les fonctionnalités utilisées de la classe template seront effectivement définies dans le programme final.

Instanciation explicite​

L'instanciation explicite des template est une technique permettant au programmeur de forcer l'instanciation des template dans son programme. Pour réaliser une instanciation explicite, il faut spécifier explicitement tous les paramètres template à utiliser. Cela se fait simplement en donnant la déclaration du template, précédée par le mot-clé template.

template nom<valeur[, valeur[...]]>;

Par exemple :

template class Stack<int>;             // Instancie toute la classe Stack<int>.
template int Min<int>(int, int); // Instancie Min<int>.

Depuis C++11, la forme extern template class Stack<int>; indique à l'inverse qu'une instanciation explicite existe dans une autre unité de traduction, ce qui évite de la générer dans chaque fichier.