IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours de C/C++


précédentsommairesuivant

12. Les template

12.1. 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 de leur type) avec les classes abstraites. Par ailleurs, il est faisable de faire des fonctions travaillant sur de nombreux types grâce à la surcharge. Je rappelle 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è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 ê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 la 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.

12.2. 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.

12.2.1. Déclaration des types template

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

 
Sélectionnez
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 égal et de la valeur. Ici, la valeur par défaut doit évidemment être un type déjà déclaré.

Exemple 12-1. Déclaration de paramètres template
Sélectionnez
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. La ligne suivante provoquera donc une erreur de compilation :

 
Sélectionnez
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 :

 
Sélectionnez
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 avant la déclaration template.

Exemple 12-2. Déclaration de paramètre template template
Sélectionnez
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.

Pour plus de détails sur la déclaration des classes template, voir la Section 12.3.2.

12.2.2. Déclaration des constantes template

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

 
Sélectionnez
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 (char, wchar_t, int, long, short et leurs versions signées et non signées) 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ères (entiers, énumérés ou adresses).

Exemple 12-3. Déclaration de paramètres template de type constante
Sélectionnez
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.

Note : Les paramètres constants de type référence ne peuvent pas être initialisés avec une donnée immédiate ou une donnée temporaire lors de l'instanciation du template. Voir la Section 12.4 pour plus de détails sur l'instanciation des template.

12.3. 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 dans la fonction ou la classe template comme des constantes locales.

12.3.1. Fonctions template

La déclaration et la définition des fonctions template se fait exactement comme si la fonction était une fonction normale, à 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 :

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

paramètre_template est la liste des paramètres template et paramètres_fonction est la liste des paramètres de la fonction 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. Voir la Section 12.4 pour plus de détails à ce sujet.

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.

Exemple 12-4. Définition de fonction template
Sélectionnez
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.

12.3.2. 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 :

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

paramètres_template est la liste des paramètres template utilisés par la classe template 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 :

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

paramètre_template représente la liste des paramètres template de la classe template classe, nom représente le nom de la méthode à définir, et paramètres_méthode ses paramètres.

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 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 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. Voir la Section 12.3.3 pour plus de détails à ce sujet.

Exemple 12-5. Définition d'une pile template
Sélectionnez
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êmes template ou non.

12.3.3. 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.

Exemple 12-6. Fonction membre template
Sélectionnez
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 définition de la fonction membre template se fait donc exactement comme celle d'une fonction template classique.

Exemple 12-7. Fonction membre template d'une classe template
Sélectionnez
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.

Exemple 12-8. Fonction membre template et fonction membre virtuelle
Sélectionnez
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 la fonction 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.

Exemple 12-9. Surcharge de fonction membre par une fonction membre template
Sélectionnez
#include <iostream>
 
using namespace std;
 
struct A
{
    void f(int);
 
    template <class T>
    void f(T)
    {
        cout << "Template" << endl;
    }
};
 
// Fonction non template :
void A::f(int)
{
    cout << "Non template" << endl;
}
 
// Fonction template :
template <>
void A::f<int>(int)
{
    cout << "Spécialisation f<int>" << endl;
}
 
int main(void)
{
    A a;
    a.f(1);    // Appel de la version non-template de f.
    a.f('c');  // Appel de la version template de f.
    a.f<>(1); // Appel de la version template spécialisée de f.
    return 0;
}

Pour plus de détails sur la spécialisation des template, voir la Section 12.5.

12.4. 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.

12.4.1. 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 l'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 définie dans l'Exemple 12-4, c'est son utilisation directe qui provoque une instanciation implicite.

Exemple 12-10. Instanciation implicite de fonction template
Sélectionnez
int i=Min(2,3);

Dans cet exemple, la fonction Min est appelée avec les paramètres 2 et 3. Comme ces entiers sont tous les deux de type int, la fonction template Min est instanciée pour le type int. Partout dans la définition de Min, le type générique T est donc remplacé par le type int.

Si l'on appelle une fonction template avec un jeu de paramètres qui provoque une ambiguïté, le compilateur signale une erreur. Cette erreur peut être levée en surchargeant la fonction template par une fonction qui accepte les mêmes paramètres. Par example, la fonction template Min ne peut pas être instanciée dans le code suivant :

 
Sélectionnez
int i=Min(2,3.0);

parce que le compilateur ne peut pas déterminer si le type générique T doit prendre la valeur int ou double. Il y a donc une erreur, sauf si une fonction Min(int, double) est définie quelque part. Pour résoudre ce type de problème, on devra spécifier manuellement les paramètres template de la fonction, lors de l'appel. Ainsi, la ligne précédente compile si on la réécrit comme suit :

 
Sélectionnez
int i=Min<int>(2,3.0);

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 utilise une politique minimaliste pour l'instanciation implicite 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.

Par exemple, dans le programme suivant :

 
Sélectionnez
#include <iostream>
 
using namespace std;
 
template <class T>
class A
{
public:
     void f(void);
     void g(void);
};
 
// Définition de la méthode A<T>::f() :
template <class T>
void A<T>::f(void)
{
     cout << "A<T>::f() appelée" << endl;
}
 
// On ne définit pas la méthode A<T>::g()...
 
int main(void)
{
     A<char> a;  // Instanciation de A<char>.
     a.f();       // Instanciation de A<char>::f().
     return 0;
}

seule la méthode f de la classe template A est instanciée, car c'est la seule méthode utilisée à cet endroit. Ce programme pourra donc parfaitement être compilé, même si la méthode g n'a pas été définie.

12.4.2. 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 :

 
Sélectionnez
template nom<valeur[, valeur[...]] >;

Par exemple, pour forcer l'instanciation d'une pile telle que celle définie dans l'Exemple 12-5, il faudra préciser le type des éléments entre crochets après le nom de la classe :

 
Sélectionnez
template Stack<int>;  // Instancie la classe Stack<int>.

Cette syntaxe peut être simplifiée pour les fonctions template, à condition que tous les paramètres template puissent être déduits par le compilateur des types des paramètres utilisés dans la déclaration de la fonction. Ainsi, il est possible de forcer l'instanciation de la fonction template Min de la manière suivante :

 
Sélectionnez
template int Min(int, int);

Dans cet exemple, la fonction template Min est instanciée pour le type int, puisque ses paramètres sont de ce type.

Lorsqu'une fonction ou une classe template a des valeurs par défaut pour ses paramètres template, il n'est pas nécessaire de donner une valeur pour ces paramètres. Si toutes les valeurs par défaut sont utilisées, la liste des valeurs peut être vide (mais les signes d'infériorité et de supériorité doivent malgré tout être présents).

Exemple 12-11. Instanciation explicite de classe template
Sélectionnez
template<class T = char>
class Chaine;
 
template Chaine<>;  // Instanciation explicite de Chaine<char>.

12.4.3. Problèmes soulevés par l'instanciation des template

Les template doivent impérativement être définis lors de leur instanciation pour que le compilateur puisse générer le code de l'instance. Cela signifie que les fichiers d'en-tête doivent contenir non seulement la déclaration, mais également la définition complète des template. Cela a plusieurs inconvénients. Le premier est bien entendu que l'on ne peut pas considérer les template comme les fonctions et les classes normales du langage, pour lesquels il est possible de séparer la déclaration de la définition dans des fichiers séparés. Le deuxième inconvénient est que les instances des template sont compilées plusieurs fois, ce qui diminue d'autant plus les performances des compilateurs. Enfin, ce qui est le plus grave, c'est que les instances des template sont en multiples exemplaires dans les fichiers objets générés par le compilateur, et accroissent donc la taille des fichiers exécutables à l'issue de l'édition de liens. Cela n'est pas gênant pour les petits programmes, mais peut devenir rédhibitoire pour les programmes assez gros.

Le premier problème n'est pas trop gênant, car il réduit le nombre de fichiers sources, ce qui n'est en général pas une mauvaise chose. Notez également que les template ne peuvent pas être considérés comme des fichiers sources classiques, puisque sans instanciation, ils ne génèrent aucun code machine (ce sont des classes de classes, ou « métaclasses »). Mais ce problème peut devenir ennuyant dans le cas de bibliothèques template écrites et vendues par des sociétés désireuses de conserver leur savoir-faire. Pour résoudre ce problème, le langage donne la possibilité d'exporter les définitions des template dans des fichiers complémentaires. Nous verrons la manière de procéder dans la Section 12.7.

Le deuxième problème peut être résolu avec l'exportation des template, ou par tout autre technique d'optimisation des compilateurs. Actuellement, la plupart des compilateurs sont capables de générer des fichiers d'en-tête précompilés, qui contiennent le résultat de l'analyse des fichiers d'en-tête déjà lus. Cette technique permet de diminuer considérablement les temps de compilation, mais nécessite souvent d'utiliser toujours le même fichier d'en-tête au début des fichiers sources.

Le troisième problème est en général résolu par des techniques variées, qui nécessitent des traitements complexes dans l'éditeur de liens ou le compilateur. La technique la plus simple, utilisée par la plupart des compilateurs actuels, passe par une modification de l'éditeur de liens pour qu'il regroupe les différentes instances des mêmes template. D'autres compilateurs, plus rares, gèrent une base de données dans laquelle les instances de template générées lors de la compilation sont stockées. Lors de l'édition de liens, les instances de cette base sont ajoutées à la ligne de commande de l'éditeur de liens afin de résoudre les symboles non définis. Enfin, certains compilateurs permettent de désactiver les instanciations implicites des template. Cela permet de laisser au programmeur la responsabilité de les instancier manuellement, à l'aide d'instanciations explicites. Ainsi, les template peuvent n'être définies que dans un seul fichier source, réservé à cet effet. Cette dernière solution est de loin la plus sûre, et il est donc recommandé d'écrire un tel fichier pour chaque programme.

Ce paragraphe vous a présenté trois des principaux problèmes soulevés par l'utilisation des template, ainsi que les solutions les plus courantes qui y ont été apportées. Il est vivement recommandé de consulter la documentation fournie avec l'environnement de développement utilisé, afin à la fois de réduire les temps de compilation et d'optimiser les exécutables générés.

12.5. Spécialisation des template

Jusqu'à présent, nous avons défini les classes et les fonctions template d'une manière unique, pour tous les types et toutes les valeurs des paramètres template. Cependant, il peut être intéressant de définir une version particulière d'une classe ou d'une fonction pour un jeu particulier de paramètres template.

Par exemple, la pile de l'Exemple 12-5 peut être implémentée beaucoup plus efficacement si elle stocke des pointeurs plutôt que des objets, sauf si les objets sont petits (ou appartiennent à un des types prédéfinis du langage). Il peut être intéressant de manipuler les pointeurs de manière transparente au niveau de la pile, pour que la méthode pop renvoie toujours un objet, que la pile stocke des pointeurs ou des objets. Afin de réaliser cela, il faut donner une deuxième version de la pile pour les pointeurs.

Le C++ permet tout cela : lorsqu'une fonction ou une classe template a été définie, il est possible de la spécialiser pour un certain jeu de paramètres template. Il existe deux types de spécialisation : les spécialisations totales, qui sont les spécialisations pour lesquelles il n'y a plus aucun paramètre template (ils ont tous une valeur bien déterminée), et les spécialisations partielles, pour lesquelles seuls quelques paramètres template ont une valeur fixée.

12.5.1. Spécialisation totale

Les spécialisations totales nécessitent de fournir les valeurs des paramètres template, séparées par des virgules et entre les signes d'infériorité et de supériorité, après le nom de la fonction ou de la classe template. Il faut faire précéder la définition de cette fonction ou de cette classe par la ligne suivante :

 
Sélectionnez
template <>

qui permet de signaler que la liste des paramètres template pour cette spécialisation est vide (et donc que la spécialisation est totale).

Par exemple, si la fonction Min définie dans l'Exemple 12-4 doit être utilisée sur une structure Structure et se baser sur un des champs de cette structure pour effectuer les comparaisons, elle pourra être spécialisée de la manière suivante :

Exemple 12-12. Spécialisation totale
Sélectionnez
struct Structure
{
    int Clef;     // Clef permettant de retrouver des données.
    void *pData;  // Pointeur sur les données.
};
 
template <>
Structure Min<Structure>(Structure s1, Structure s2)
{
    if (s1.Clef>s2.Clef)
        return s1;
    else
        return s2;
}

Note : Pour quelques compilateurs, la ligne déclarant la liste vide des paramètres template ne doit pas être écrite. On doit donc faire des spécialisations totale sans le mot clé template. Ce comportement n'est pas celui spécifié par la norme, et le code écrit pour ces compilateurs n'est donc pas portable.

12.5.2. Spécialisation partielle

Les spécialisations partielles permettent de définir l'implémentation d'une fonction ou d'une classe template pour certaines valeurs de leurs paramètres template et de garder d'autres paramètres indéfinis. Il est même possible de changer la nature d'un paramètre template (c'est-à-dire préciser s'il s'agit d'un pointeur ou non) et de forcer le compilateur à prendre une implémentation plutôt qu'une autre selon que la valeur utilisée pour ce paramètre est elle-même un pointeur ou non.

Comme pour les spécialisations totales, il est nécessaire de déclarer la liste des paramètres template utilisés par la spécialisation. Cependant, à la différence des spécialisations totales, cette liste ne peut plus être vide.

Comme pour les spécialisations totales, la définition de la classe ou de la fonction template doit utiliser les signes d'infériorité et de supériorité pour donner la liste des valeurs des paramètres template pour la spécialisation.

Exemple 12-13. Spécialisation partielle
Sélectionnez
// Définition d'une classe template :
template <class T1, class T2, int I>
class A
{
};
 
// Spécialisation n°1 de la classe :
template <class T, int I>
class A<T, T*, I>
{
};
 
// Spécialisation n°2 de la classe :
template <class T1, class T2, int I>
class A<T1*, T2, I>
{
};
 
// Spécialisation n°3 de la classe :
template <class T>
class A<int, T*, 5>
{
};
 
// Spécialisation n°4 de la classe :
template <class T1, class T2, int I>
class A<T1, T2*, I>
{
};

On notera que le nombre des paramètres template déclarés à la suite du mot clé template peut varier, mais que le nombre de valeurs fournies pour la spécialisation est toujours constant (dans l'exemple précédent, il y en a trois).

Les valeurs utilisées dans les identificateurs template des spécialisations doivent respecter les règles suivantes :
  • une valeur ne peut pas être exprimée en fonction d'un paramètre template de la spécialisation ;
 
Sélectionnez
template <int I, int J>
struct B
{
};
 
template <int I>
struct B<I, I*2>   // Erreur !
{                   // Spécialisation incorrecte !
};
  • le type d'une des valeurs de la spécialisation ne peut pas dépendre d'un autre paramètre ;
 
Sélectionnez
template <class T, T t>
struct C
{
};
 
template <class T>
struct C<T, 1>;    // Erreur !
                    // Spécialisation incorrecte !
  • la liste des arguments de la spécialisation ne doit pas être identique à la liste implicite de la déclaration template correspondante.

Enfin, la liste des paramètres template de la déclaration d'une spécialisation ne doit pas contenir des valeurs par défaut. On ne pourrait d'ailleurs les utiliser en aucune manière.

12.5.3. Spécialisation d'une méthode d'une classe template

La spécialisation partielle d'une classe peut parfois être assez lourde à employer, en particulier si la structure de données qu'elle contient ne change pas entre les versions spécialisées. Dans ce cas, il peut être plus simple de ne spécialiser que certaines méthodes de la classe et non la classe complète. Cela permet de conserver la définition des méthodes qui n'ont pas lieu d'être modifiées pour les différents types, et d'éviter d'avoir à redéfinir les données membres de la classe à l'identique.

La syntaxe permettant de spécialiser une méthode d'une classe template est très simple. Il suffit en effet de considérer la méthode comme une fonction template normale, et de la spécialiser en précisant les paramètres template à utiliser pour cette spécialisation.

Exemple 12-14. Spécialisation de fonction membre de classe template
Sélectionnez
#include <iostream>
 
using namespace std;
 
template <class T>
class Item
{
    T item;
public:
    Item(T);
    void set(T);
    T get(void) const;
    void print(void) const;
};
 
template <class T>
Item<T>::Item(T i)               // Constructeur
{
    item = i;
}
 
// Accesseurs :
 
template <class T>
void Item<T>::set(T i)
{
    item = i;
}
 
template <class T>
T Item<T>::get(void) const
{
    return item;
}
 
// Fonction d'affichage générique :
 
template <class T>
void Item<T>::print(void) const
{
    cout << item << endl;
}
 
// Fonction d'affichage spécialisée explicitement pour le type int *
// et la méthode print :
template <>
void Item<int *>::print(void) const
{
    cout << *item << endl;
}

12.6. Mot-clé typename

Nous avons déjà vu que le mot clé typename pouvait être utilisé pour introduire les types génériques dans les déclarations template. Cependant, il peut être utilisé dans un autre contexte pour introduire les identificateurs de types inconnus dans les template. En effet, un type générique peut très bien être une classe définie par l'utilisateur, à l'intérieur de laquelle des types sont définis. Afin de pouvoir utiliser ces types dans les définitions des template, il est nécessaire d'utiliser le mot clé typename pour les introduire, car a priori le compilateur ne sait pas que le type générique contient la définition d'un autre type. Ce mot clé doit être placé avant le nom complet du type :

 
Sélectionnez
typename identificateur

Le mot clé typename est donc utilisé pour signaler au compilateur que l'identificateur identificateur est un type.

Exemple 12-15. Mot-clé typename
Sélectionnez
class A
{
public:
    typedef int Y;    // Y est un type défini dans la classe A.
};
 
template <class T>
class X
{
    typename T::Y i;  // La classe template X suppose que le
                      // type générique T définisse un type Y.
};
 
X<A> x;              // A peut servir à instancier une classe
                      // à partir de la classe template X.

12.7. Fonctions exportées

Comme on l'a vu, les fonctions et classes template sont toutes instanciées lorsqu'elles sont rencontrées pour la première fois par le compilateur ou lorsque la liste de leurs paramètres est fournie explicitement.

Cette règle a une conséquence majeure : la définition complète des fonctions et des classes template doit être incluse dans chacun des fichiers dans lequel elles sont utilisées. En général, les déclarations et les définitions des fonctions et des classes template sont donc regroupées ensemble dans les fichiers d'en-tête (et le code ne se trouve pas dans un fichier C++). Cela est à la fois très lent (la définition doit être relue par le compilateur à chaque fois qu'un template est utilisé) et ne permet pas de protéger le savoir faire des entreprises qui éditent des bibliothèques template, puisque leur code est accessible à tout le monde.

Afin de résoudre ces problèmes, le C++ permet de « compiler » les fonctions et les classes template, et ainsi d'éviter l'inclusion systématique de leur définition dans les fichiers sources. Cette « compilation » se fait à l'aide du mot clé export.

Pour parvenir à ce résultat, vous devez déclarer « export » les fonctions et les classes template concernées. La déclaration d'une classe template export revient à déclarer export toutes ses fonctions membres non inline, toutes ses données statiques, toutes ses classes membres et toutes ses fonctions membres template non statiques. Si une fonction template est déclarée comme étant inline, elle ne peut pas être de type export.

Les fonctions et les classes template qui sont définies dans un espace de nommage anonyme ne peuvent pas être déclarées export. Voir le Chapitre 11 plus de détails sur les espaces de nommage.

Exemple 12-16. Mot-clé export
Sélectionnez
export template <class T>
void f(T);          // Fonction dont le code n'est pas fourni
                    // dans les fichiers qui l'utilisent.

Dans cet exemple, la fonction f est déclarée export. Sa définition est fournie dans un autre fichier, et n'a pas besoin d'être fournie pour que f soit utilisable.

Les définitions des fonctions et des classes déclarées export doivent elles aussi utiliser le mot clé export. Ainsi, la définition de f pourra ressembler aux lignes suivantes :

 
Sélectionnez
export template <class T>
void f(T p)
{
    // Corps de la fonction.
    return ;
}

Note : Aucun compilateur ne gère le mot clé export à ce jour.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2012 developpez.com Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.