Skip to main content

Les 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. Rappelons qu'en C++, tous les types sont en fait des classes.

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 atypiques mais cela serait au détriment de la taille du code.

Le C++ permet de résoudre ces problèmes grâce aux paramètres génériques, que l'on appelle encore paramètre 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 êtres paramétrés 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. 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 égale 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 de 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. La ligne suivante provoquera donc une erreur de compilation template <class T=int, class V>.

Ils 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 [,...]>

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 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 de type constante 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

Ce sont donc tous les types qui peuvent être assimilés à des valeurs entière (entier, énumérés ou adresses).

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 fait exactement comme s'il s'agissait de types normaux, à 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.

Tous les paramètres template qui sont des types doivent être utilisés dans la liste des paramètres de la fonction, à moins qu'une instanciation explicite de la fonction ne soit utilisée. Cela permet au compilateur de réaliser l'identification des types génériques avec les types à utiliser lors de l'instanciation de la fonction.

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 définies localement avec la classe de stockage 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'il y a ambiguïté entre une fonction template et une fonction normale qui la surcharge, toutes les références sur le nom commun à ces fonctions se rapporteront à la fonction classique.

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 lise 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 référencer. Cette règle est cependant facultative lorsque la classe est référencée à l'intérieur d'une fonction membre.

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)
{
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ême 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 membre.

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 se fait donc exactement comme celle d'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)
{
// ...
}

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 surcharge B::f(int).
{
f<>(i); // Elle appelle de 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 de 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 qu'il 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 membres 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);

dans cet exemple, le paramètre template est forcé à int, et 3.0 est converti en entier.

On prendra garde au fait que le compilateur utiliser 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 types 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[...]]>;