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

FAQ C++Consultez toutes les FAQ

Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021  Ajouter une question

 

Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.

Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.

Sur ce, nous vous souhaitons une bonne lecture.

SommaireLes templates (18)
précédent sommaire suivant
 

Les templates (modèles en français, ou encore patrons) sont la base de la généricité en C++. Il s'agit en fait de modèles génériques de code qui permettent de créer automatiquement des fonctions (dans le cas de fonctions templates) ou des classes (classes templates) à partir d'un ou plusieurs paramètres.
Le fait de fournir un paramètre à un modèle générique s'appelle la spécialisation. Elle aboutit en effet à la création d'un code spécialisé pour un type donné à partir d'un modèle générique.
Pour cette raison on surnomme aussi les templates des types paramétrés (parameterized types en anglais). Ces modèles manipulent généralement un type abstrait qui est remplacé par un vrai type C++ au moment de la spécialisation.
Ce type abstrait est fourni sous forme de paramètre template qui peut être un type C++, une valeur (entier, enum, pointeur…) ou même un autre template.
La spécialisation d'un template est transparente et invisible. Elle est effectuée lors de la compilation, de manière interne au compilateur, en fonction des arguments donnés au template (il n'y a pas de code source généré quelque part).
Par exemple, vous pouvez réaliser une fonction template renvoyant le plus grand de deux objets de même type pour peu que ce dernier possède un opérateur de comparaison operator > (la fonction standard std::max procède ainsi). Cette fonction template va accepter en argument le type des objets à comparer, appelé type T dans l'exemple suivant :

Code c++ : Sélectionner tout
1
2
3
4
5
6
// renvoie le plus grand entre A et B 
template<typename T> 
const T & Max( const T & A, const T & B ) 
{ 
    return A > B ? A : B; 
}
Si vous appelez cette fonction en fournissant deux int, le compilateur va spécialiser la fonction Max pour le type int, ce qui reviendrait à avoir écrit :

Code c++ : Sélectionner tout
1
2
3
4
const int & Max( const int & A, const int & B ) 
{ 
    return A > B ? A : B; 
}
Si vous faites de même avec deux float cette fois-ci, une nouvelle spécialisation de la fonction pour le type float va être générée.

Code c++ : Sélectionner tout
1
2
3
4
const float & Max( const float & A, const float & B ) 
{ 
    return A > B ? A : B; 
}
Tout se passe comme si vous aviez écrit deux fois la même fonction, une fois pour le type int et une fois pour le type float. Mais vous n'avez bien qu'une seule fonction template Max, qui opère sur un type abstrait déclaré au moyen du mot-clé typename.
Les templates permettent donc de réutiliser facilement du code source, sans devoir utiliser le préprocesseur, ce qui le rend plus lisible et plus rigoureux notamment envers les types manipulés.
Notez qu'il est possible de créer des fonctions membres templates. L'exemple suivant crée une classe permettant de construire une chaîne de caractères au moyen de sa fonction membre template Append.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream> 
#include <sstream> 
  
class StringBuilder 
{ 
public: 
    template<typename T> 
    void Append( const T & t ) 
    { 
        this->oss << t; 
    } 
  
    std::string GetString() const 
    { 
        return this->oss.str(); 
    } 
  
private: 
    std::ostringstream oss; 
}; 
  
int main() 
{ 
    StringBuilder sb; 
    sb.Append( 10 ); 
    sb.Append( '\t' ); 
    sb.Append( "coucou " ); 
    sb.Append( 54.12 ); 
    std::cout << sb.GetString() << '\n'; 
}
Pour que le code précédent compile sans la fonction membre template, il aurait fallu écrire quatre fonctions membres pour les 4 types utilisés : int, char, const char * et double. Ces quatre fonctions utiliseraient strictement le même code. Grâce à l'utilisation des templates, le compilateur a fait ce travail pour nous. Bien que l'on ait écrit une seule fonction nommée Append, celle-ci n'existe pas en réalité dans le code compilé, mais le compilateur en a généré (spécialisé) quatre. Il en aurait spécialisé dix si dix types différents avaient été utilisés.

Mis à jour le 22 novembre 2004 Aurelien.Regat-Barrel JolyLoic

Prenons comme exemple la fonction suivante qui renvoie le plus grand des deux entiers qui lui sont donnés :

Code c++ : Sélectionner tout
1
2
3
4
int Max( int A, int B ) 
{ 
    return ( A >= B ) ? ( A ) : ( B ); 
}
Cet exemple est un cas typique de fonction qu'il est intéressant de rendre générique au moyen des templates. Pour cela, il faut s'affranchir du type int que l'on va remplacer par un type abstrait nommé T grâce aux mots clés template et typename :

Code c++ : Sélectionner tout
1
2
3
4
5
template<typename T> 
T Max( T A, T B ) 
{ 
    return ( A >= B ) ? ( A ) : ( B ); 
}
Notez qu'on aurait pu utiliser des références constantes comme cela est fait dans la fonction standard std::max, mais il s'agit ici d'un exemple.
Le mot clé template indique que la fonction qui suit est une fonction template, et typename dans ce contexte sert à déclarer un nouveau type paramétré pour notre nouvelle fonction template. Il est aussi possible d'utiliser le mot-clé class à la place de typename pour la déclaration des paramètres du template.
Nous venons de créer une fonction template Max possédant un seul type paramétré nommé T.
Lorsque nous créons une instance de cette fonction Max de cette manière :

Code c++ : Sélectionner tout
Max<int>( 1, 2 );
Nous demandons au compilateur de spécialiser la fonction Max pour le type int. Ce dernier va en quelque sorte remplacer toutes les occurrences de T par int. Il va d'ailleurs à cette occasion vérifier la validité de l'utilisation de ce type dans le contexte de cette fonction. Avec int pas de problèmes, mais prenons l'exemple suivant :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
class Test 
{ 
}; 
  
Test a; 
Test b; 
  
Max<Test>( a, b );
La classe Test ne possédant pas d'opérateur de comparaison operator >=, la compilation va échouer sur l'utilisation de ce dernier. Les compilateurs récents émettent un message d'erreur assez explicite :

In function `T Max(T, T) [with T = Test]': no match for 'operator>=' in 'A >= B'

ou encore

error C2676: '>=' : 'Test' binaire ne définit pas cet opérateur ou une conversion vers un type acceptable pour l'opérateur prédéfini

Sachez enfin qu'il n'est pas toujours nécessaire de préciser le type du paramètre pour notre template, et que celui-ci peut être déterminé automatiquement par le compilateur (voir Qu'est-ce que la détermination automatique des paramètres templates ?).

Mis à jour le 18 avril 2005 Aurelien.Regat-Barrel

L'écriture de classes templates pose souvent des problèmes de syntaxe ou de conception, voici un exemple illustrant leur écriture :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>   
  
template<typename T> class Exemple; 
  
template<typename T> 
std::ostream& operator<<(std::ostream&, Exemple<T> const&); 
  
template <typename T>   
class Exemple   
{   
public :   
  
    Exemple(const T& Val = T());   
  
    template <typename U>   
    Exemple(const Exemple<U>& Copy);   
  
    const T& Get() const;   
  
    // Spécialisation template <> selon le type T 
    friend std::ostream& operator << <>(std::ostream& Stream, const Exemple<T>& Ex);   
  
private :   
  
    T Value;   
};   
  
template <typename T>   
Exemple<T>::Exemple(const T& Val) 
  : Value(Val)   
{   
  
}   
  
template <typename T>   
template <typename U>   
Exemple<T>::Exemple(const Exemple<U>& Copy) 
  : Value(static_cast<T>(Copy.Get()))   
{   
    // Attention, ceci n'est pas le constructeur par copie !   
}   
  
template <typename T>   
const T& Exemple<T>::Get() const   
{   
    return Value;   
}   
  
template <typename T>   
std::ostream& operator <<(std::ostream& Stream, const Exemple<T>& Ex)   
{   
    return Stream << Ex.Value;   
}   
  
int main()   
{   
    Exemple<int> A(3);   
    Exemple<float> B(A);   
  
    std::cout << A << std::endl;   
    std::cout << B << std::endl;   
  
    return 0;   
}

Mis à jour le 18 avril 2005 Laurent Gomila

Une fonction ou une classe template peut être spécialisée pour certains types de paramètres, c'est ce qu'on appelle la spécialisation. Cela permet entre autre d'avoir un comportement spécifique à certains types de paramètres, à des fins d'optimisation ou pour s'adapter à un comportement particulier par exemple.
Lors de l'utilisation d'un template avec un type donné, le compilateur recherche s'il existe une spécialisation du template pour ce type. S'il en trouve une il utilise cette version spécialisée, sinon il se rabat sur la version générique de base du template.
On peut spécialiser une fonction, une fonction membre template de classe, ou une classe toute entière.
Voici la syntaxe à utiliser pour effectuer une spécialisation

L'ordre est important : la version générique doit apparaître en premier.
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Version générique  
template <typename T> 
void QuiSuisJe( const T & x )  
{  
    std::cout << "Je ne sais pas" << std::endl;  
}  
  
// Spécialisation pour les int  
template <> 
void QuiSuisJe<int>( const int & x )  
{  
    std::cout << "Je suis un int" << std::endl;  
}  
  
// Spécialisation pour ma classe  
template <> 
void QuiSuisJe<MaClasse>( const MaClasse & x )  
{  
    std::cout << "Je suis un MaClasse" << std::endl;  
}  
  
// Test  
MaClasse Test1;  
int Test2;  
float Test3;  
  
QuiSuisJe( Test1 ); // "Je suis un MaClasse"  
QuiSuisJe( Test2 ); // "Je suis un int"  
QuiSuisJe( Test3 ); // "Je ne sais pas"
La spécialisation de classe est elle plus contraignante car il faut redéfinir la totalité de celle-ci.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template<typename T> 
struct Modele  
{  
    void QuiSuisJe()  
    {  
        std::cout << "Je suis un Modele<inconnu>" << std::endl;  
    }  
};  
  
template<> 
struct Modele<int>  
{  
    void QuiSuisJe()  
    {  
        std::cout << "Je suis un Modele<int>" << std::endl;  
    }  
  
    void CestQuoiCetteFonction()  
    {  
        // On peut tout à fait ajouter des fonctions 
        // en fait le contenu de la classe peut être totalement différent !  
    }  
};  
  
Modele<float> M1;  
Modele<int> M2;  
  
M1.QuiSuisJe(); // "Je suis un Modele<inconnu>"  
M2.QuiSuisJe(); // "Je suis un Modele<int>"  
  
M1.CestQuoiCetteFonction(); // Erreur : 'fonction inconnnue'  
M2.CestQuoiCetteFonction(); // OK
Un template ne peut être spécialisé qu'à l'intérieur d'un namespace, et pas dans une classe.

Mis à jour le 17 mars 2008 Laurent Gomila

Aucune, si ce n'est que template <typename> est préféré pour indiquer plus explicitement que l'argument attendu est bel et bien un type (qu'il s'agisse d'une classe ou de tout autre type).

Mis à jour le 6 juillet 2014 koala01

Lorsque vous appelez une fonction template, vous n'avez pas toujours besoin d'indiquer explicitement le type de vos paramètres templates : le compilateur est souvent capable de le faire pour vous.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
template <typename T> 
void Fonction( T x )  
{  
}  
  
Fonction<double>(5.2f);  
// Equivalent à  
Fonction( 5.2 );
Ceci n'est pas toujours possible, il existe certaines situations où l'on est obligé de spécifier explicitement le type des paramètres manipulés (lorsque le compilateur ne peut les déduire ou bien pour lever une ambiguïté par exemple).

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
template <typename T> 
T Fonction() 
{ 
    return T(); // renvoie le type  
}  
  
int x = Fonction(); // Erreur : 'impossible de déduire l'argument de modèle'  
int x = Fonction<int>(); // OK
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
template <typename T> 
void Fonction( T x1, T x2 )  
{  
}  
  
int x1 = 5; // premier argument de type int 
double x2 = 6.5; // second argument de type double 
  
Fonction( x1, x2 ); // Erreur : 'paramètre ambigu'  
Fonction<double>( x1, x2 ); // OK 
Fonction( static_cast<double>( x1 ), x2 ); // OK
La détermination automatique des paramètres ne peut s'appliquer que sur des fonctions templates. Pour les classes templates il faut systématiquement les expliciter.

Mis à jour le 22 novembre 2004 Laurent Gomila

Un template n'est pas une fonction normale qu'il suffit d'appeler, c'est un modèle qui va générer une fonction normale lors de l'instanciation. Schématiquement, l’instanciation d'un template se compose de trois phases :

  • on remplace dans le template les différents arguments template par leur vraie valeur (ce qui peut déclencher l'instanciation d'autres templates) ;
  • on compile le code résultant ;
  • on peut enfin se lier à la fonction générée.


Les deux premières phases nécessitent que le compilateur ait accès à la définition complète du template. Autrement dit, tout son code doit figurer dans le .h.

On peut cependant conserver la logique de la séparation interface/implémentation en la simulant de cette manière :
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// exemple.h 
  
#ifndef EXEMPLE_H 
#define EXEMPLE_H 
  
template <typename T> 
class Exemple 
{ 
public: 
    Exemple(); 
}; 
  
#include "exemple.tpp" // <-- astuce ici !!! 
#endif
Code c++ : Sélectionner tout
1
2
3
4
5
6
// exemple.tpp 
  
template <typename T> 
Exemple<T>::Exemple() 
{ 
}
L'astuce consiste à inclure à la fin du .h le fichier contenant le corps du template. On la trouve aussi parfois utilisée pour des fonctions inline.

Notez l'utilisation de l'extension .tpp au lieu du classique .cpp, afin de faire la distinction avec les fichiers cpp classiques (pouvant être compilés, contrairement au code template qui doit d'abord être spécialisé avant de pouvoir être compilé). Il n'y a pas vraiment de convention, on trouve de nombreuses autres extensions : .htt, .tcc, .tpl, tpp, inl… Libre à vous de choisir celle que vous préférez.

Mis à jour le 17 mars 2008 Aurelien.Regat-Barrel

En plus de l'utilisation qu'on lui connaît pour définir un type en tant que paramètre template, où il est possible aussi d'utiliser class :

Code c++ : Sélectionner tout
1
2
3
4
5
template <typename /* ou class */ T> 
class MaClasse 
{ 
    ... 
};

Le mot-clé typename possède une seconde utilité : il sert à indiquer au compilateur qu'un identifiant est un type, dans certains contextes manipulant des templates pour lesquels il ne peut pas le deviner automatiquement. (Nous utiliserons class ici pour introduire les paramètres templates type pour éviter la confusion avec la première utilisation, naturellement typename est aussi possible.)

Prenez cet exemple incorrect :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T> 
class MaClasse 
{ 
public : 
    typedef int MonType; 
}; 
  
template <class T> 
void MaFonction(T x) 
{ 
    MaClasse<T>::MonType t; 
    ... 
}

Dans ce cas vous savez que MaClasse<T>::MonType est bien un type, mais le compilateur lui ne peut pas le déduire. La raison en est la suivante : imaginez que l'on spécialise MaClasse (voir spécialisation) et que l'on définisse MonType autrement :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
template <> 
class MaClasse<int> 
{ 
public : 
    static const int MonType = 5; 
}; 
  
template <class T> 
void MaFonction(T x) 
{ 
    MaClasse<T>::MonType t; // Que se passe-t-il ici si T est int ?? 
    ... 
}

Bien que l'exemple ci-dessus compile sur certains compilateurs sans avoir recours au mot-clé typename, le standard exige sa présence, et les compilateurs modernes vont dans ce sens. Il convient donc de l'utiliser même si votre compilateur sait s'en passer.
La syntaxe correcte est donc :

Code c++ : Sélectionner tout
1
2
3
4
5
6
template <class T> 
void MaFonction(T x) 
{ 
    typename MaClasse<T>::MonType t; 
    ... 
}

Ce genre d'erreur peut arriver plus souvent que vous ne le pensez, par exemple si vous manipulez des conteneurs standards dans une classe template :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
template <class T> // à priori, on ne sait rien du type T 
class MaClasse 
{ 
public : 
    typedef          std::list<T>::iterator Iter;  // Erreur avec certains compilateurs (ou warning, ou rien) 
    typedef typename std::list<T>::iterator Iter2; // Ok 
};


Malheureusement non, dans le standard C++ actuel on ne peut pas écrire quelque chose comme cela :

Code c++ : Sélectionner tout
1
2
3
4
5
#include <vector> 
  
typedef std::vector Tableau; // erreur de compilation 
  
Tableau<int> t;
Une solution qui peut parfois convenir consiste à utiliser une struct template :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
#include <vector> 
  
template <typename T> 
struct Tableau 
{ 
    typedef std::vector<T> type; 
}; 
  
Tableau<int>::type t;

Mis à jour le 18 avril 2005 Aurelien.Regat-Barrel

Une classe de trait (trait class), généralement template, définit des caractéristiques ou des fonctions associées à un type donné. Cela permet donc d'ajouter de l'information à des types que l'on ne peut pas modifier.

Une classe de trait n'est généralement pas destinée à être instanciée, ses membres étant typiquement statiques.

Le template std::numeric_limits<T> de la STL est une classe de traits : elle permet d'ajouter aux types de base des informations telles que les valeurs min / max, l'epsilon, etc.

Voici un exemple d'une classe de traits qui fournit une valeur nulle appropriée pour chaque type :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T> struct ValeurNulle; 
  
template <> struct ValeurNulle<int>         {static int Zero()         {return 0;}}; 
template <> struct ValeurNulle<std::string> {static std::string Zero() {return "";}}; 
template <> struct ValeurNulle<MaClasse>    {static MaClasse Zero()    {return MaClasse(-1);}}; 
// ... 
  
template <typename T> 
void Fonction(T Valeur) 
{ 
    T Ret = ValeurNulle<T>::Zero(); 
    // ... 
}

Mis à jour le 17 novembre 2018 Laurent Gomila

Les classes de politique (policy classes) sont assez similaires aux classes de traits, mais contrairement à celles-ci qui ajoutent des informations à des types, les classes de politiques servent à définir des comportements.

"Les classes de politique ont beaucoup en commun avec les traits, mais en diffèrent du fait qu'elles mettent moins l'accent sur les types et plus sur les comportements".

Andrei Alexandrescu, Modern C++ Design

Voici par exemple une fonction qui accumule des éléments et en renvoie la somme, à la manière de std::accumulate :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
template <typename T> 
T Accumulation(const T* Debut, const T* Fin) 
{ 
    T Resultat = 0; 
    for ( ; Debut != Fin; ++Debut) 
        Resultat += *Debut; 
  
    return Resultat; 
}
Ici l'accumulation sera quoiqu'il arrive une somme. En utilisant une classe de politique pour personnaliser l'opération effectuée, nous pouvons rendre cette fonction beaucoup plus générique :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T> 
struct Addition 
{ 
    static void Accumuler(T& Resultat, const T& Valeur) 
    { 
        Resultat += Valeur; 
    } 
}; 
  
template <typename T, typename Operation> 
T Accumulation(const T* Debut, const T* Fin) 
{ 
    T Resultat = 0; 
    for ( ; Debut < Fin; ++Debut) 
        Operation::Accumuler(Resultat, *Debut); 
  
    return Resultat; 
}
On voit ici une propriété typique des classes de politique : Addition est « orthogonale » aux autres paramètres templates de la fonction, c'est-à-dire ici qu'elle ne dépend pas du type T qu'elle manipule. Celui-ci peut être int tout comme std::string, notre classe de politique n'y verra aucune différence.

Pour modifier le comportement de la fonction Accumulation pour par exemple multiplier les éléments, il suffirait d'écrire une classe politique Multiplication qui remplacerait += par *=, et la passer en paramètre à Accumulation.
On pourrait également imaginer utiliser Accumulation pour extraire un minimum, ou pour faire encore beaucoup d'autres choses.

Une fonction qui prend en paramètre une classe de politique aura généralement une valeur par défaut assez évidente (par exemple ici la politique Addition). Cependant, les fonctions n'acceptant pas les paramètres templates par défaut (cela sera certainement corrigé dans une future norme du langage), il faudra remplacer votre fonction non membre par une fonction statique encapsulée dans une classe. Bien sûr ensuite rien ne vous empêche de fournir des fonctions qui encapsulent l'appel à cette fonction membre.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T, typename Operation = Addition<T> > 
struct Accumulation 
{ 
    static T Accumule(const T* Debut, const T* Fin) 
    { 
        T Resultat = 0; 
        for ( ; Debut < Fin; ++Debut) 
            Operation::Accumuler(Resultat, *Debut); 
  
        return Resultat; 
    } 
}; 
  
template <typename T> 
T Accumule(const T* Debut, const T* Fin) 
{ 
    return Accumulation<T>::Accumule(Debut, Fin); 
} 
  
template <typename T, typename Operation> 
T Accumule(const T* Debut, const T* Fin) 
{ 
    return Accumulation<T, Operation>::Accumule(Debut, Fin); 
}
Enfin, pour faire le lien entre politiques et traits, on peut remarquer que notre fonction d'accumulation possède quelques défauts. Par exemple, la valeur zéro du type T ne sera pas forcément 0 (ce sera par exemple "" pour les std::string).
Ainsi nous pouvons utiliser la classe de traits définie dans Qu'est-ce qu'une classe de trait ? Comment l'utiliser ? pour l'améliorer :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
template <typename T, typename Operation = Addition<T> > 
struct Accumulation 
{ 
    static T Accumule(const T* Debut, const T* Fin) 
    { 
        T Resultat = ValeurNulle<T>::Zero(); 
        for ( ; Debut < Fin; ++Debut) 
            Operation::Accumuler(Resultat, *Debut); 
  
        return Resultat; 
    } 
};
Les classes de politique sont utilisées intensivement dans la bibliothèque Loki, et de ce fait très bien décrites dans le livre Modern C++ Design d'Andrei Alexandrescu.
Les classes de traits et de politique sont également décrites et comparées dans C++ templates - the complete guide de David Vandevoorde et Nicolai M. Josuttis.

Mis à jour le 17 mars 2008 Laurent Gomila

Un exemple sera plus parlant que des mots :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T> 
class Mere 
{ 
    /* ... */ 
}; 
  
// soit la classe fille doit aussi être template comme ici 
template <typename T> 
class Fille1 : public /*ou private/protected */ Mere<T> 
{ 
    /* ... */ 
}; 
  
// soit on fixe le paramètre template 
class Fille2 : public Mere<int> // où n'importe quel type à la place de int 
{ 
    /* ... */ 
}; 
  
// ou encore 
class Fille3 : public /* ... */ Mere<Fille3>  
{ 
    /* ... */ 
};
Soit dit en passant, le dernier héritage (Fille3) est une application du principe de Curiously Recurring Template Pattern (CRTP).

Mis à jour le 15 octobre 2009 Alp

Le CRTP (Curiously Recurring Template Pattern) correspond simplement à la situation suivante, à laquelle on se retrouve souvent confronté lorsque l'on conçoit des architectures logicielles C++ génériques :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
template <class Derived> 
class Base 
{ 
    /* ... */ 
}; 
  
class Fille : public Base<Fille> 
{ 
    /* ... */ 
};
Pour un exemple de situation qui tire parti du CRTP, il y a le classique compteur d'instances de classes, qui définit un modèle de classe counter, de sorte que counter<X> et counter<Y> soient deux classes (instances du modèle counter) différentes. Ainsi, les variables statiques ne seront pas partagées entre counter<X> et counter<Y>. La seule chose qu'il y a à faire est de définir X et Y comme héritant de counter<X> et counter<Y>.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <typename T> 
struct counter 
{ 
    counter() 
    { 
        objects_created++; 
        objects_alive++; 
    } 
  
    counter(const counter& other)  
    {  
        objects_created++;  
        objects_alive++; 
    } 
  
    virtual ~counter() 
    { 
        --objects_alive; 
    } 
    static int objects_created; 
    static int objects_alive; 
}; 
template <typename T> int counter<T>::objects_created( 0 ); 
template <typename T> int counter<T>::objects_alive( 0 ); 
  
class X : counter<X> 
{ 
    // ... 
}; 
  
class Y : counter<Y> 
{ 
    // ... 
};
Imaginons avoir une implémentation d'une classe widget, paramétrée par ce qui sera la véritable classe représentant un composant graphique donné.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
template <class derived_widget> 
class widget 
{ 
public: 
    // ...  
    void show() { static_cast<derived_widget*>(this)->do_show(); } 
    // ... 
};
Il s'agit désormais de définir des widget bien précis et concrets, comme button ou textfield.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class button : public widget<button> 
{ 
    // ... 
private: 
    friend class widget<button>; // on donne à widget<button> l'accès à notre do_show() qui est private 
    void do_show() { std::cout << "Button" << std::endl; } 
}; 
  
class textfield : public widget<textfield> 
{ 
    // ... 
private: 
    friend class widget<textfield>; 
    void do_show() { std::cout << "Textfield" << std::endl; } 
};
Ainsi, nous avons l'équivalent d'une fonction virtuelle, ici show(), que l'on aurait mis dans la classe widget sans pour autant avoir le surcoût à l'exécution entrainé par l'utilisation de la virtualité. Pourtant, appeler show() sur un widget<button> ou un widget<textfield> affichera bien ce que l'on veut, sans avoir déclaré show comme virtuelle. Dans le cas de cet exemple simplifié le nom de la fonction d'origine a été modifié sinon les fonctions auraient été appelées directement.

Code c++ : Sélectionner tout
1
2
3
4
button b("Cliquez ici"); 
textfield t("Entrez du texte ici"); 
b.show(); // affiche "Button" 
t.show(); // affiche "Textfield"
Cela permet donc de simuler le polymorphisme d'héritage, en disposant de fonctions que l'on pourrait croire virtuelles. Cela s'avèrera toutefois gênant si vous voulez stocker, ici, des widget<T>, avec différents types pour T. Il vous faudra alors ruser, et notamment regarder le principe de Type Erasure.

Mis à jour le 15 octobre 2009 Alp

SFINAE, acronyme de Substitution Failure Is Not An Error, est un principe C++ qui entre en jeu lors de la résolution des surcharges de fonctions.
Le principe est assez simple. Lorsque vous disposez d'un modèle de fonction (function template), si l'une des instanciations (remplacement d'un paramètre par un type ou une valeur précise) conduit à un type d'argument ou un type de retour incorrect, alors le compilateur, au lieu d'indiquer une erreur, passera sous silence cela si une autre fonction (template ou non) du même nom colle à l'appel.
Le code classique qui accompagne une introduction à SFINAE :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Test  
{ 
    typedef int type; 
}; 
  
template < typename T >  
void f( typename T::type ) {} // definition #1 
  
template < typename T >  
void f( T ) {}                // definition #2 
  
f< Test > ( 10 ); //appelle #1  
f< int > ( 10 );  //appelle #2 sans erreur grace a SFINAE
Ici, aucun problème pour l'appel 1. Pour l'appel 2, si l'on remplace T par int dans la définition #1, on a alors un argument de type int::Type, ce qui est invalide, int n'étant ni un namespace, ni une structure/classe, mais un type fondamental. Toutefois, au lieu de nous indiquer une erreur tout simplement, le compilateur voit une autre fonction du même nom dont la signature colle à l'appel, et c'est celle-ci qu'il choisit d'appeler. Voilà le principe de SFINAE.
Concrètement, qu'est-ce que cela signifie ? Vous savez probablement que l'on ne peut pas spécialiser partiellement une fonction template. En particulier, il est hors de question de pouvoir spécialiser les fonctions selon les propriétés que les types des arguments qu'on leur fournit ont. Justement, avec SFINAE, il est désormais possible de le faire. Selon qu'une classe/structure A possède par exemple un type A::type, nous pouvons donc appeler une certaine fonction ou une autre, de même nom, mais qui ne demande pas d'avoir cette propriété.

Mis à jour le 15 octobre 2009 Alp

Il s'agit d'utiliser les techniques relatives aux classes de politique qui sont exposées dans l'article Classes de Traits et de Politiques en C++.
Si Foo est la classe dont nous voulons rendre la structure variable, nous allons devoir la paramétrer par une politique. Par exemple, si nous voulons qu'une implémentation de la politique fournisse une interface plus complète, permettant plus d'opérations, nous le pouvons tout à fait ! On peut ainsi rajouter des fonctions en combinant les politiques à l'héritage, comme on peut le voir dans l'exemple ci-dessous.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <class PolicyT> 
class Host : public PolicyT 
{ 
  public: 
    void foo() { std::cout << 42 << std::endl; } 
}; 
  
class PolicyTImpl1 
{ 
  public: 
    void bar() { std::cout << "Forty-Two" << std::endl; } 
}; 
  
class PolicyTImpl2 
{ 
  public: 
    void bar() { std::cout << "Chuck Norris" << std::endl; } 
    void foobar() { std::cout << "C++" << std::endl; } 
}; 
  
// ... 
Host<PolicyTImpl1> h1; 
h1.foo(); // affiche 42 
h1.bar(); // affiche "Forty-Two" 
h1.foobar(); // ne compile pas 
  
Host<PolicyTImpl2> h2; 
h2.foo(); // affiche 42 
h2.bar(); // affiche "Chuck Norris" 
h2.foobar(); // affiche "C++"
Ici, sur base d'une même classe, nous avons pu exposer des éléments variables en fonction de la politique donnée lors de l'instanciation. Ce genre de pratiques s'avère très utile lorsque l'on paramètre par exemple une classe par des politiques pouvant donner des optimisations selon la plateforme.
Pour rendre la compréhension plus facile, nous avons toutefois dû prendre le problème à l'envers. En effet, ce genre de technique n'est utile que pour certains problèmes. Il n'est utile de faire varier une partie de la classe (points de variation de la classe) que pour résoudre un problème, il ne faut pas chercher un problème à résoudre avec cette technique, qui n'a dans le cas contraire aucun sens.
Un exemple de situation où cela peut-être bénéfique… Imaginons devoir réaliser un programme de calcul scientifique qui doit être multiplateforme. Imaginons de plus que pour un système d'exploitation A, on dispose d'une bibliothèque classique ainsi que d'une bibliothèque utilisant des appels bien plus rapides pour les calculs, spécifique à ce système toutefois. Le système d'exploitation B lui ne dispose que de la première bibliothèque. L'objectif est donc de n'exposer pour B que les opérations fournies par la bibliothèque classique, et d'exposer au choix l'une ou l'autre pour le système d'exploitation A. Cela ressemblerait au code suivant en utilisant la technique exposée ci-dessus.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template <class ComputingLibraryPolicy> 
class Computing : public ComputingLibraryPolicy 
{ 
public: 
    // ... 
}; 
  
class FastComputingLibrary 
{ 
public: 
    float sin(float); 
    float cos(float); 
    float fast_sin(float); 
    float fast_cos(float); 
    // ... 
}; 
  
class ComputingLibrary 
{ 
public: 
  float sin(float); 
  float cos(float); 
}; 
  
// ... 
  
#ifdef SYS_EXPLOITATION_A 
typedef Computing<FastComputingLibrary> computing_type; 
#else 
typedef Computing<ComputingLibrary> computing_type; 
#endif 
  
// ... 
  
computing_type comp; 
comp.fastsin(0); // compilera sur l'OS A uniquement 
comp.sin(0); // compilera sur les deux plateformes
On a ainsi pu optimiser selon la plateforme simplement en introduisant de la variabilité sur les fonctions utilisées, de manière élégante, via les politiques.

Mis à jour le 15 octobre 2009 Alp

Imaginez que vous ayez conçu une classe qui encapsule un calcul très lourd, au point que vous ayez mis sur pieds deux implémentations, l'une monothreadée, l'autre multithreadée. Il serait dommage de les faire hériter d'une classe abstraite et d'en hériter pour chacune des versions, induisant un coût à cause de la virtualité, qui est ici superflue. Vous avez une possibilité qui vous permettra de tout gérer à la compilation, en utilisant les templates. Nous allons illustrer avec une fonction qui mesure le temps pris par le calcul.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <class Computation> 
void do_something()  
{  
    std::time_t start = time(NULL); 
    Computation::compute(); 
    std::time_t end = time(NULL); 
    std::cout << end - start << " seconds." << std::endl; 
} 
  
struct SingleThreadedComputation 
{ 
    static void compute() 
    {  
        // implémentation monothread 
    } 
}; 
  
struct MultiThreadedComputation 
{ 
    static void compute() 
    {  
        // implémentation multithread 
    } 
}; 
  
// par exemple : 
#ifdef STCOMPUTATION 
do_something<SingleThreadedComputation>(); 
#elif defined MTCOMPUTATION 
do_something<MultiThreadedComputation>(); 
#endif 
  
// comportement que l'on peut choisir soit avec un #define,  
// soit avec l'option de compilation -DSTCOMPUTATION ou -DMTCOMPUTATION
Ainsi, on a factorisé la variabilité (multithreading ou pas) de notre calcul dans une fonction paramétrée, Compute, sans ajouter la surcharge induite par l'utilisation de la virtualité. Le « défaut » est que la variabilité est statique, c'est-à-dire fixée à la compilation, tandis que le polymorphisme lié à l'héritage nous permet d'avoir une variabilité à l'exécution.
Cette façon de faire s'approche de ce que l'on appelle le Policy Based Design, qui permet de paramétrer de manière très flexible le comportement, dès la compilation, avec une utilisation intelligente des templates.
C'est une sorte d'équivalent du Design Pattern Strategy, à la sauce C++ et templates.

Mis à jour le 15 octobre 2009 3DArchi Alp

Dans le cadre d'utilisation des templates, le compilateur ne peut pas toujours deviner la nature d'un élément dépendant d'un paramètre template.
Par exemple dans le code suivant :

Code c++ : Sélectionner tout
1
2
3
4
template<typename T> 
void bar() { 
    // T::element 
}

T::element peut être, selon la classe qui sera utilisée pour instancier bar :
  • une variable statique ;
  • un type pleinement déterminé ;
  • une classe template.

Il est donc nécessaire d'aider le compilateur à choisir, et la manière de faire diffère selon l'élément.
  1. Dans le premier cas, il n'y a rien de particulier à faire.
  2. Dans le deuxième cas, il faut faire précéder l'expression de typename pour indiquer qu'il s'agit d'un type.
    Code c++ : Sélectionner tout
    typename T::type d; // Déclare une variable de type T::type

    Note : C++20 a relâché cette contrainte dans certaines situations.
  3. Troisième cas un peu plus complexe. Il faut tout d'abord ajouter le mot clé template avant B pour signifier que T::B est une classe template. Le compilateur sait donc que T::template B<int> est une instanciation de template et non autre chose. Sauf que l'instanciation d'une classe template est un type comme un autre. On se retrouve alors dans le cas précédent ! La bonne syntaxe est alors la suivante :
    Code c++ : Sélectionner tout
    1
    2
    // Instancie la classe template T::B avec le type int 
    typename T::template B<int> b;
  4. Quatrième cas dans le même principe : nous avons une fonction template à la place d'une classe template, on ajoute donc template devant le nom de la fonction pour signifier que a.foo est une fonction template.
    Code c++ : Sélectionner tout
    a.template foo<U>();

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream> 
  
struct A 
{ 
    static const int value = 5; // 1 
  
    using type = double; // 2 
  
    template <typename T> struct B // 3 
    { 
        void fct() const { std::cout << "B::fct" << std::endl; } 
    }; 
  
    template <typename T> // 4 
    void foo() const { std::cout << "A::foo" << std::endl; } 
}; 
  
  
template <class U, class T> void bar(const T& a) 
{ 
    std::cout << T::value << std::endl; // 1 
  
    typename T::type d = 0.5; // 2 
    std::cout << 2*d << std::endl; 
    /* sans typename : 
    main.cpp:23:18: error: need 'typename' before 'T:: type' because 'T' is a dependent scope 
    main.cpp:23:18: error: dependent-name 'T:: type' is parsed as a non-type, but instantiation yields a type 
    main.cpp:23:18: note: say 'typename T:: type' if a type is meant */ 
  
    typename T::template B<int> b; // 3 
    b.fct(); 
    /* sans template : 
    main.cpp:30:17: error: non-template 'B' used as template 
    main.cpp:30:17: note: use 'T::template B' to indicate that it is a template 
    main.cpp:30:17: error: declaration does not declare anything [-fpermissive] */ 
  
    a.template foo<U>(); // 4 
    /* sans template : 
    main.cpp:37:14: error: expected primary-expression before '>' token 
    main.cpp:37:16: error: expected primary-expression before ')' token */ 
} 
  
int main(void) 
{ 
    bar<bool>( A() ); 
    return 0; 
}

Mis à jour le 14 novembre 2021 Davidbrcz

Lorsque qu'une classe Fille hérite d'une classe Mere qui a un paramètre template ou une classe dépendant d'un tel paramètre, on doit dire explicitement au compilateur que l'on fait référence aux variables et fonctions membres de Mere. Pour ce faire, on dispose de trois méthodes :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template <typename T> 
class Mere 
{ 
protected : 
  
    T membre; 
}; 
  
template <typename T> 
class Fille : public Mere<T> 
{ 
public :  
    // On souhaite utiliser la variable 'membre' définie dans la 
    // classe 'Mere<T>' : 
  
    // 1) On peut utiliser 'this->' pour préciser qu'il s'agit d'un 
    // membre de cette classe (le compilateur peut alors 
    // regarder dans la classe mère s'il existe bel et bien). 
    T MereMembre1() 
    { 
        return this->membre; 
    } 
  
    // 2) On peut écrire explicitement 'Mere<T>::'. 
    // Attention cependant pour les fonctions membres : cette 
    // syntaxe appellera toujours la version de 'Mere<T>', 
    // même si cette méthode est virtuelle et ré-implémentée 
    // ailleurs. 
    T MereMembre2() 
    { 
        return Mere<T>::membre; 
    } 
  
    // 3) Enfin, on peut utiliser une directive 'using'. Cette 
    // dernière méthode est la plus verbeuse dans notre cas 
    // simple. Cependant, une fois la directive écrite, toutes les 
    // références à 'membre' dans le reste du code de cette 
    // classe n'auront plus à être qualifiées explicitement. 
    using Mere<T>::membre; 
    T MereMembre3() 
    { 
        return membre; 
    } 
};
Si l'on ne spécifie rien, le compilateur n'a pas moyen de savoir si membre est effectivement une variable membre de Mere<T> (n'oubliez pas que, comme dans le cas de À quoi sert le mot-clé typename ?, elle peut être spécialisée pour un type T particulier et ne pas fournir cette variable membre), ou bien une variable globale qui porterait le même nom.

Par exemple, le code suivant affiche "4", ce qui n'est probablement pas le résultat escompté : comme on n'a pas qualifié correctement l'utilisation de membre, c'est la variable globale qui va être systématiquement choisie pour chaque appel à MereMembre() :
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream> 
  
template<typename T> 
class Mere 
{ 
public : 
  
    T membre; 
}; 
  
int membre = 4;   
  
template<typename T> 
class Fille : public Mere<T> 
{ 
public : 
  
    T MereMembre() 
    { 
        return membre; 
    } 
}; 
  
int main() 
{ 
    Fille<int> f; 
    f.membre = 5; 
    std::cout << f.MereMembre() << std::endl; 
}

Mis à jour le 6 juillet 2014 Kalith

Proposer une nouvelle réponse sur la FAQ

Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour ça


Réponse à la question

Liens sous la question
précédent sommaire suivant
 

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 © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.