Ce que l'on aimerait faire
Il arrive souvent qu'on ait besoin d'opérations mathématiques dans nos programmes. Une opération toute simple est celle qui consiste à trouver le plus grand de deux nombres. Dans le cas des nombres entiers, on pourrait écrire une fonction comme suit :
Code : C++ - La fonction maximum | int maximum(int a,int b)
{
if(a>b)
return a;
else
return b;
}
|
Une telle fonction existe bien sûr dans la SL. Elle se trouve dans l'en-tête algorithm et s'appelle simplement max()
Cette fonction est très bien et elle n'a pas de problème. Cependant, si un utilisateur de votre fonction aimerait utiliser des
double à la place des
int, il risque d'avoir un problème. Il faudrait donc fournir également une version de cette fonction utilisant des nombres réels. Ce qui ne devrait pas vous poser de problème à ce stade du cours.
Pour être rigoureux, il faudrait également fournir une fonction de ce type pour les
char, les
unsigned int les nombres rationnels, etc. On se rend vite compte que la tâche est très répétitive.
Cependant, il y a un point commun à toutes ces fonctions, le
corps de la fonction est strictement identique. Quel que soit le type, le traitement que l'on effectue est le même. On se rend compte que l'algorithme utilisé dans la fonction est
générique.
Il serait donc intéressant de pouvoir écrire une seule fois la fonction en disant au compilateur : « Cette fonction est la même pour tous les types, fais le sale boulot de recopie du code toi-même. » Eh bien, ça tombe bien parce que c'est ce que permettent les
templates en C++ et c'est ce que nous allons apprendre à utiliser dans la suite.
Le terme français pour template est modèle. Le nom est bien choisi car il décrit précisément ce que nous allons faire. Nous allons écrire un modèle de fonction et le compilateur va utiliser ce modèle dans les différents cas qui nous intéressent.
Une première fonction template
Pour indiquer au compilateur que l'on veut faire une fonction générique, on va déclarer un « type variable » qui peut représenter n'importe quel autre type. On parle de type générique. Cela se fait de la manière suivante :
Code : C++ - Déclaration d'un type générique
Vous pouvez remarquer quatre choses importantes.
- Premièrement le mot-clé template qui prévient le compilateur que la prochaine chose dont on va lui parler sera générique.
- Deuxièmement, les symboles "<" et ">" que vous avez certainement déjà aperçus dans le chapitre sur les vector et sur la SL. C'est la marque de fabrique des templates.
- Troisièmement, le mot-clé typename qui indique au compilateur que T sera le nom que l'on va utiliser pour notre « type spécial » qui remplace n'importe quoi.
- Finalement, il n'y a PAS de point-virgule à la fin de la ligne.
On peut également utiliser le mot-clé class à la place de typename dans ce contexte. Il n'y a aucune différence. Cela donne : template<class T>. J'utiliserai typename dans la suite pour éviter les confusions.
Secret (cliquez pour afficher)Beaucoup de programmeurs utilisent
class à la place de
typename en invoquant la raison que cela fait 3 caractères de moins à taper...
La ligne de code précédente indique au compilateur que dans la suite,
T sera un type générique pouvant représenter n'importe quel autre type. On pourra donc utiliser ce
T dans notre fonction comme type pour les arguments et pour le type de retour.
Code : C++ - Ma première fonction template | template <typename T>
T maximum(const T& a, const T& b)
{
if(a>b)
return a;
else
return b;
}
|
Quand il va voir cela, le compilateur va automatiquement générer une série de fonctions maximum() pour tous les types dont vous avez besoin. Cela veut dire que si vous avez besoin de cette fonction pour des entiers, le compilateur va créer la fonction :
Code : C++ | int maximum(const int& a,const int& b)
{
if(a>b)
return a;
else
return b;
}
|
... et de même pour les
double,
char, etc. C'est le compilateur qui se farcit le travail de recopie ! Parfait, on peut aller faire la sieste pendant ce temps.
On peut écrire un petit programme de test :
Code : C++ - Test de la fonction maximum 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 | #include <iostream>
using namespace std;
template <typename T>
T maximum(const T& a,const T& b)
{
if(a>b)
return a;
else
return b;
}
int main()
{
double pi(3.14);
double e(2.71);
cout << maximum<double>(pi,e) << endl; // Utilise la "version double" de la fonction.
int cave(-1);
int dernierEtage(12);
cout << maximum<int>(cave,dernierEtage) << endl; // Utilise la "version int" de la fonction.
unsigned int a(43);
unsigned int b(87);
cout << maximum<unsigned int>(a,b) << endl; // Utilise la "version unsigned int" de la fonction.
return 0;
}
|
Et tout cela se passe sans que l'on ait besoin d'écrire plus de code. Il faut juste indiquer entre des chevrons quelle "version" de la fonction on souhaite utiliser, comme pour les
vector en somme : on devait indiquer quelle "version" du tableau on souhaitait utiliser.
Il n'est pas toujours utile d'indiquer entre chevrons quel type l'on souhaite utiliser pour les fonctions templates. Le compilateur est assez intelligent pour
deviner ce que vous souhaitez faire. Mais dans des cas compliqués ou si il y a plusieurs arguments de types différents, alors il devient nécessaire de spécifier la version.
Code : C++ | int main()
{
double pi(3.14);
double e(2.71);
cout << maximum(pi,e) << endl; // Utilise la "version double" de la fonction.
return 0;
}
|
Le compilateur voit dans ce cas que l'on souhaite utiliser la "version
double" de la fonction.
A vous de voir si votre compilateur comprend vos intentions.
Si vous êtes attentifs, vous avez peut-être remarqué que j'ai remplacé le passage par valeur pour les arguments par des
références constantes. En effet, on ne sait pas quel type l'utilisateur va utiliser avec notre fonction
maximum(). La taille en mémoire de ce type sera peut-être très grande ; on passe donc une référence constante pour éviter une copie coûteuse inutile.
Où mettre la fonction ?
Habituellement, un programme est subdivisé en plusieurs fichiers que l'on classe en deux catégories. Les fichiers de code (les
.cpp) et les fichiers d'en-tête (les
.h). Généralement, on met le prototype de la fonction dans un
.h et la définition dans le
.cpp comme on l'a vu
tout au début ce ce cours.
Pour les fonctions templates, c'est différent. TOUT doit obligatoirement se trouver dans le fichier
.h, sinon votre programme ne pourra pas compiler.
Je le répète encore une fois, car c'est une erreur classique,
le prototype ET la définition d'une fonction template doivent obligatoirement se trouver dans un fichier d'en-tête.
Tous les types sont-ils utilisables ?
J'ai dit plus haut que le compilateur allait générer toutes les fonctions nécessaires. Cependant, il y a quand même une contrainte ici : le type que l'on passe à la fonction doit posséder un
operator>. Par exemple, on ne peut pas utiliser cette fonction avec un
Personnage ou un
Magicien des chapitres précédents : ils ne possèdent pas de surcharge de
>. Tant mieux, puisque prendre le maximum de deux Personnages n'a pas de sens !
Les contraintes dépendent des fonctions que vous écrivez. Si vous utilisez l'opérateur + dans la fonction, alors il faut que l'objet passé en argument surcharge cet opérateur. Si vous effectuez une copie dans la fonction, alors l'objet doit posséder un constructeur de copie etc.