Je tiens à préciser que ce cours est plutôt orienté code. En effet je préfère me baser sur du code plutot que sur des paragraphes et des paragraphes, question d'attrait. De plus, j'essaye de présenter au lecteur les problèmes auxquels nous trouvons par la suite les solutions, grâce aux templates.
Sommaire:
Avant de rentrer dans le vif des templates, il est important de voir et comprendre pourquoi les templates ont été créés. Au lieu de tourner autour du pot, nous allons directement voir un code ... un code écrit avant l'avènement des templates. Voici le code, où l'on voit des déclarations de plusieurs fonctions :
Code de l'ancien monde : Déclarations des fonctions
int additionner(int,int); float additionner(float,float); double additionner(double,double);
Vecteur additionner(Vecteur,Vecteur);
// ...
On dirait bien que ce fichier définit toutes les fonctions permettant d'additionner, et ce pour
un grand nombre de types différentes.
Oui, à première vue ce code parait un peu lourd. A seconde vue, ... également.
Bon, ne tombons pas dans les à prioris, allons voir l'implémentation de ces fonctions. La voici :
Code de l'ancien monde : Implémentation des fonctions
int additionner(int a, int b) { return a+b; } void additionner(float f, float g)
{
return f+g;
}
// oui oui je vais tous les faire ...
void additionner(double d, double e)
{
return d+e;
}
Vecteur additionner(Vecteur u, Vecteur v)
// on suppose que les Vecteur sont de petits objets, on se permet de les prendre par copie
{
return u+v;
}
Alors, je sais pas vous hein, mais je trouve que ce code est très répétitif. Il y a juste le nom des types qui changent.
Vous savez ca serait sympa de pouvoir passer en paramètre le type au compilateur, et lui il génère le truc genre:
Brouillon d'idée
< ici le truc pour le compilateur avec MonType >
MonType additionner(MonType a, MonType b)
{
return a+b;
}
Ouah, ca m'a l'air très sympa comme idée, ca serait super! Donc au moment où j'ai eu cette idée, j'ai contacté Bjarne Stroustrup
Et puis là je me suis réveillé, j'ai ouvert mon livre de C++, et j'ai vu que cela existait déjà.
En effet, cela s'appelle les Templates(modèles). Je vais simplement vous montrer ce que donne notre exemple avec les templates.
Voici :
Le nouveau monde : les templates et notre fonction
template < class MonType >
MonType additionner(MonType a, MonType b)
{
return a+b;
}
Moui, c'est sympa, ils se sont pas mal débrouillés. Et au niveau de l'utilisation, on l'appelle comment cette fonction?
C'est très simple! Voici l'appel de additionner pour différents types:
Le nouveau monde : appel de la nouvelle fonction
int ia=2,ib=4;
double da=2.005, db=1.995;
int ires = additionner< int >(ia,ib);
double dres = additionner< double >(da,db);
// nous verrons plus tard qu'en fait, il y a certains cas ou il n'est pas
// nécessaire de préciser le type entre < >, c'est-à-dire faire additionner(ia,ib),
// et c'est le cas ici.
// en effet il y a des cas où le compilateur se débrouille très bien tout seul.
// mais nous verrons ceci plus tard
Nous venons de voir l'un des motifs qui a entrainé la création des templates.
Cependant ce n'est pas le seul! Bien qu'il rejoingne un peu le précédent, je tiens quand même à en parler.
Disons que nous voulons créér une classe pour contenir un pointeur, et elle s'occupera de veiller à ce que
le pointeur soit bien construit, soit bien détruit, etc ... On appelle ca un pointeur intelligent(pas besoin d'appeler delete).
Pour bien comprendre le concept je vous invite à aller voir Qu'est-ce qu'un pointeur intelligent.
Comment peut-on faire pour, avec une seule classe, pouvoir faire des pointeurs
intelligents de tous les types d'objets(on parle d'objets et pas des types fondamentaux int,char,...).
Allons voir dans l'ancien monde, ce monde sans templates. Oh mais il y a une solution!
Ca serait de faire une classe Object. Et à partir de cette classe Object, on fait hériter toutes les classes
qui seront utilisées avec un pointeur intelligent. Mais, j'y pense, lorsqu'on veut obtenir le pointeur,
on est obligé de retourner un Object*? Ce qui nous empêche d'utiliser le pointeur. Ah mais j'y pense, un simple
cast suffit alors pour récupérer le pointeur comme nous le voulons, c'est-à-dire :
Ancien monde : classe AutoPtr
class Object { ... };
class AutoPtr
{
Object* ptr;
public:
AutoPtr() : ptr(new Object)
{ }
Object* getPtr() const
{
return ptr;
}
~AutoPtr()
{
delete ptr;
}
};
// ...
AutoPtr s;
MonObjet* mo = (MonObjet*)(s.get()); // mais il y a un problème
// car ptr est un Object de toute manière, il a donc que l'espace d'un Object en mémoire
// il aurait fallu faire new MonObjet!
Alors, problème. Il y a plusieurs solution : passer une fonction pour construire en argument au constructeur,
ou bien faire des fonctions dans AutoPtr du genre initAsMonObjet, initAsMonWidget, .....
Cependant la première solution n'est possible qu'en passant par des pointeurs de retour en void*,
afin de pouvoir avoir un prototype de fonction fixe, ce qui est très sale, et pas pratique, car il faudra
pour chaque type supplémentaire qu'on veut supporter. Les deux solutions sont très lourdes, et pèseront
pendant l'exécution du programme. Mais comment faire?! Les templates, hé oui.
En plus de pouvoir faire des fonctions templates, on peut également faire des classes templates(des struct également hein).
Voici une classe simple de pointeur "intelligent"(à la sauce auto_ptr):
Le nouveau monde : classe template AutoPtr
template < class T > class AutoPtr
{
T* ptr;
public:
AutoPtr(T* other) : ptr(other)
{ }
T* getPtr() const
{
return ptr;
}
~AutoPtr()
{
delete ptr;
}
};
Voici donc le problème réglé en deux coups de templates! Et niveau utilisation on a ceci:
Le nouveau monde: utilisation de la nouvelle classe AutoPtr
class A
{
public:
A(int);
};
// ...
{
AutoPtr< A > p(new A(23));
} // hop, p est détruit, ainsi que le pointeur qu'il contient!
Dans cette introduction nous avons donc vu deux exemples
montrant l'efficacité des templates vis à vis d'autres techniques.
Bien sur c'est loin d'être tout. Nous allons maintenant voir exactement la syntaxe
et les différentes possibilités avec les templates.
Il est maintenant temps de voir la théorie. Au lieu d'une longue phrase, je vais vous
donner la syntaxe d'une fonction template:
template < class A, class B, class C, ... >
type_retour nom(arguments)
{
...
}
Vous pouvez voir que c'est bien la syntaxe de la fonction additionner que nous avons vue plus haut.
Pour les classes templates, cela ce passe comme ceci:
template < class A, class B, class C, ... >
class MaClasse
{
// ...
public:
// ...
};
On retrouve effectivement cette syntaxe dans la classe SmartPtr. Bravo, vous savez créér
des fonctions et classes templates! Maintenant, voyons un peu quelques exemples pour voir les "variantes" :
Exemple de code templates : plusieurs paramètres
template < class Premier, class Second > class Paire
{
Premier prem;
Second sec;
public:
Paire(Premier p,Second s) : prem(p), sec(s) { }
Premier& getPrem() const { return prem; }
Second& getSec() const { return sec; }
};
// utilisation :
Paire< int, std::string > p(5,"cinq");
Il y a quelque que je ne vous ai pas encore dit cependant. C'est le fait qu'au lieu de passer
des types en paramètres, on peut passer des constantes connues à la compilation(int,char et leurs dérivés).
Imaginons, une classe encapsulant un tableau d'une certaine taille, on peut éviter l'allocation dynamique
avec les templates, par exemple :
Exemple de code templates : constantes en paramètre
template < int N > class Tableau
{
int tab[N];
public:
// ...
};
// plus loin :
Tableau< 8 > tab; // tableau de 8 éléments, mais de taille fixe.
// Cependant ceci est INTERDIT :
int getTaille() { return 18; }
Tableau< getTaille() > tab;
// on peut également prendre des pointeurs de fonctions en paramètre :
template < class T, void (*f)(T) > class MaClasse;
Attention, le paramètre doit être connu à la compilation. Même si getTaille est
inline, cela ne compile pas(sous GCC du moins, je suppose donc que c'est dans la norme).
Logique car une fonction inline n'entrainera pas forcément un résultat connu à la compilation...
Par contre je trouve ça très pratique pour faire des classes réutilisables, comme des terrains de jeux!
En effet, on prend deux int : largeur et longueur. Ca nous permet de faire des terrains pour des jeux 2d,
pour des jeux de table(échecs, dames, ...), morpion, puissance4, sudoku, et j'en passe! Mais les templates c'est fabuleux,
ca aide à créér du code réutilisable
Bien, maintenant, comment faire si l'on désire "templater" un paramètre qui est une classe template?
Par cette phrase, je veux dire : si l'on a une classe template, qu'on veut la passer en paramètre mais en
"templatant" ses paramètres, il suffit de faire comme ça :
Ici, il faut mettre autant de "class" entre < > que de paramètres que demande la classe template MaClasse.
En effet, examinez donc ce code, extrait
d'un très bon article sur les paramètres templates de classes templates:
Exemple de code templates : paramètres templates de classes templates
template < typename T, template < typename > class Cont >
// ici Cont est un conteneur bien sur
class Stack
{
//...
private:
Cont< T > s_;
};
Ah, nous pouvons également voir comment faire une fonction template dans une classe normale:
Exemple de code templates : fonction template dans classe normale
class A
{
public:
template < class T > T doubleIt(T t);
};
// voyons par la meme occasion comment faire pour l'implémenter hors de la classe
template < class T > T A::doubleIt(T t)
{
return t*2;
}
Il est aussi intéressant de voir comment implémenter les fonctions d'une classe template en dehors de la classe.
Au passage, les compilateurs ne supportent pas de déclarer une fonction/classe template dans un fichier mais de l'implémenter
dans un autre. En effet, il y a le mot clé export mais il n'est pas du tout supporté par tous les compilateurs.
Exemple d'utilisation des templates : implémentation de fonctions hors de la classe templates
template < class T, class U, class V >
class Triplet
{
T t;
U u;
V v;
public:
T getPrem() const;
U getSec() const;
V getTro() const;
};
template < class T, class U, class V > T Triplet::getPrem() const
{
return t;
}
template < class T, class U, class V > T Triplet::getSec() const
{
return u;
}
template < class T, class U, class V > T Triplet::getTro() const
{
return v;
}
Comme nous pouvons fournir des valeurs par défaut pour les arguments de fonctions, on peut également fournir
des paramètres par défaut pour nos paramètres templates, et c'est de plus très simple. Voici la syntaxe:
Ici T et U sont des paramètres normaux, alors que V possède un paramètre par défaut qui est ClasseParDefaut, et alors que n
possède une valeur par défaut qui est 10. Pour instancier la classe A, voici les possibilités:
Paramètres par défaut : instanciation
A< std::string, std::ifstream, std::ofstream, 25 > a1;
A< std::string, std::ifstream, std::ofstream > a2;
A< std::string, std::ifstream > a3;
A< std::string, std::ifstream, 10 > a4; // interdit, on ne peut pas "sauter" un paramètre
// parce qu'il a une valeur par défaut
Une dernière chose à ce sujet : même si vous mettez des valeurs par défaut à tous les paramètres, vous devez
quand même faire A < > moninstance; du fait que vous ne pouvez pas faire deviner au compilo que A est une classe template.
Ceci étant vu, vous maitrisez donc les paramètres par défaut également. Il reste cependant
un gros morceau dans les bases : la spécialisation.
Cependant, avant la spécialisation j'aimerais vous parler de quelque chose dont j'ai parlé au tout début.
Je vous ai dit qu'il y avait des cas où lorsque l'on appelle une fonction template, il n'y a pas besoin de
préciser les paramètres entre < >, qu'ils sont détectés automatiquement. C'est très simple :
Les types déduits sont ceux des objets passés en argument de la fonction. Exemple :
Détection des paramètres
template< class A > void f(A a,A b);
template< class A, class B > void g(A a, B b);
// ...
f(2,3); // A = int ici, détecté tout seul, car A est le type des arguments
f(1.55,1); // erreur du compilo, a et b doivent être de même type
g(2,3); // ok, A = int, B = int
g(1.55,1); // ok, A = double, B = int
Cependant, si l'on fait une fonction template dont on "templatise" le type de retour, ce type ne peut être
détecté, il faut le préciser dans les paramètres. Cela ne marche qu'avec les types des arguments.
Je vais vous dévoiler ce qui se cache derrière le nom de spécialisation.
Vous savez, quand vous écrivez une fonction ou une classe template, par exemple template < class T > void f();,
T peut être n'importe quoi, et le code généré sera le même chaque fois pour tous les types, en remplçant bien sur
T par le nom du type. Mais par exemple, si je veux que le code soit totalement différent si je passe int* en paramètre?
C'est là qu'intervient la spécialisation. Elle permet de définir des cas plus ou moins particuliers pour les fonctions/classes
templates. En effet, on peut dire par exemple pour int, mais également pour les pointeurs en général, etc. C'est bien beau
mais bon on va voir au niveau de la syntaxe comment ça se passe.
Voici une spécialisation pour les int d'une fonction template < class T > void f():
Spécialisation : cas simple
template < class T > void g(T t) { // ca fait quelque chose }
void h(int a) { // ca fait autre chose }
template < class T > void f(T t) // la fonction normale
{
g(t);
}
// tadam, la spécialisation
template < > void f< int >(int a)
{
h(a);
}
// plus loin dans le code :
std::string s = "salut";
f(s); // utilise la fonction normale
int a = 34;
f(a); // utilise la spécialisation pour les int
Lorsque vous voulez spécialiser, il faut d'abord définir la fonction générale, puis au fur et à mesure
spécialiser de plus en plus la fonction. Vous avez donc vu avec f() la syntaxe pour spécialiser.
Voyons maintenant comment spécialiser une classe pour les pointeurs, puis pour les int, en laissant un second
paramètre template non spécialisé :
Spécialisation : un peu plus compliqué
template < class U,class V >
class A
{
};
template < class U,class V >
class A< U,V* >
{
};
template < class U >
class A< U,int >
{
};
Voyons un peu les différents cas. Tout d'abord, comment spécialiser une fonction membre pour un cas précis de la classe :
Spécialisation : Fonction membre
template< class T >
class MaClasse
{
public:
void f();
};
template< class T > void MaClasse< T >::f()
{
// quelque chose
}
template < > void MaClasse< int >::f()
{
// autre chose!
}
Maintenant, voyons comment spécialiser une fonction dans une classe normale :
Spécialisation : fonction template dans classe normale
class A
{
public:
template< class T > void f(T t);
};
template< class T > void A::f(T t)
{
// quelque chose
}
template< > void A::f< int >(int t)
{
// autre chose
}
Je pense que nous avons fait le tour de la spécialisation. Il nous reste une seule chose à voir dans
ce chapitre, c'est une certaine utilisation du mot clé typename, qui, soit dit en passant,
peut être utilisé à la place du mot class lorsque l'on fait template< class T>.
Voyons donc ce fameux mot clé typename. Tout d'abord, voyons le code suivant :
Exemple de code
template< class T >
class MaClasse
{
T::abc monobjet;
};
A priori, rien de faux, pas d'erreurs... Seulement si!
Regardez les deux structs suivantes et vous allez comprendre :
Exemple de code, la suite
struct A
{
typedef int abc;
};
struct B
{
int abc;
};
Ahhh, ca y est vous avez compris! Imaginez que l'on passe A comme paramètre à MaClasse. Tout va bien.
MaClasse< A > déclarera monobjet comme int. Mais si l'on passe B, il y aura une erreur.
C'est pourquoi le code de MaClasse va déclencher une erreur! Comment peut-il savoir à l'avance si abc est
un type ou une variable.
C'est là qu'intervient le mot clé typename. Il permet d'indiquer dans ces situations où
le compilateur ne peut déterminer si cela sera un type ou une variable, que c'est un type. Voici son utilisation avec MaClasse :
Mot clé typename
template< class T >
class MaClasse
{
typename T::abc monobjet; // ok, le compilo sait que abc est un nom de type
};
Nous voila donc à la fin du chapitre sur les bases des templates. Dans le prochain chapitre, nous parlerons
principalement des techniques pratiques se basant sur tout ce que l'on a vu jusqu'à présent.
N'oubliez pas le principe le plus important de l'utilisation des templates : créér des fonctions et classes les plus génériques possible
ayant le moins de dépendances possibles.