Introduction aux templates C++
Voir aussi : code source - vidéo youtube - vidéo peertube
En C++, un template permet de paramétrer du code (fonction, structure, classe) selon un “type générique”. On peut ensuite spécifier un “type concret” que doit prendre ce paramètre de type et ainsi générer le code correspondant. On peut bien-sûr spécifier différents types concrets et ainsi générer différents codes, à partir d’un même template.
Les templates C++ permettent d’écrire du code générique, ce qui est parfois appelé méta-programmation. Il s’agit d’un outil très important en C++ mais assez difficile à maitriser.
Ce premier article sur les templates C++ en présente la motivation et les fonctionnalités de base.
Les conversions implicites
Soit la fonction mul2
suivante. Elle prend en paramètre un int
et retourne
un int
. On peut bien sûr appeler cette fonction sur un int
mais également
sur un double
, que le compilateur va alors convertir implicitement en int
.
// fonction sur des int
int mul2(int x) {
return x*2;
}
int main() {
// ok, mul2 est appelé sur un int
int n1 = 2;
int r1 = mul2(n1);
<< n1 << " x 2 = " << r1 << endl;
cout
// mul2 est appelé sur un double qui est
// converti automatiquement en int
double n2 = 2.1;
double r2 = mul2(n2);
<< n2 << " x 2 = " << r2 << endl;
cout
return 0;
}
Ici le double
de valeur 2.1 est donc converti implicitement en un int
de
valeur 2 avant d’être passé à la fonction mul2
. Il en résulte une perte de
précision.
$ ./mul2-0.out
2 x 2 = 4
2.1 x 2 = 4
La surcharge de fonction
Pour gérer les double
plus précisemment, on peut surcharger la fonction
mul2
, c’est-à-dire en écrire plusieurs versions, pour des paramètres
différents. La version de la fonction à utiliser est déterminer automatiquement
par le compilateur selon le type des valeurs passées en paramètres.
// fonction sur des int
int mul2(int x) {
return x*2;
}
// surcharge de la fonction, sur des double
double mul2(double x) {
return x*2;
}
int main() {
// appel de int mul2(int)
int n1 = 2;
int r1 = mul2(n1);
<< n1 << " x 2 = " << r1 << endl;
cout
// appel de double mul2(double)
double n2 = 2.1;
double r2 = mul2(n2);
<< n2 << " x 2 = " << r2 << endl;
cout
return 0;
}
On obtient bien le résultat attendu mais il nous a fallu pour cela écrire du code supplémentaire, quasiment identique au code déjà existant.
$ ./mul2-1.out
2 x 2 = 4
2.1 x 2 = 4.2
Les templates de fonctions
Au lieu d’écrire notre fonction mul2
pour des int
ou pour des double
(ou
pour les deux), on peut écrire la fonction pour un type T
(par exemple) et on
indiquera ensuite si ce T
correspond à int
ou à double
. T
est appelé
“paramètre de type” ou “type générique”. mul2
n’est plus une fonction
mais un “template de fonction”.
// template de fonction
template <typename T>
(T x) {
T mul2return x*2;
}
int main() {
// instanciation du template pour int
int n1 = 2;
int r1 = mul2<int>(n1);
<< n1 << " x 2 = " << r1 << endl;
cout
// instanciation du template pour double
double n2 = 2.1;
double r2 = mul2<double>(n2);
<< n2 << " x 2 = " << r2 << endl;
cout
return 0;
}
Ici, le template de fonction mul2
est instancié pour le type int
(mul2<int>
) et pour le type double
(mul2<double>
). Les templates offrent
ainsi le double avantage d’avoir du code utilisable pour différents types tout
en évitant la duplication de code.
$ ./mul2-2.out
2 x 2 = 4
2.1 x 2 = 4.2
De plus, la cohérence des types est vérifiée dès la compilation. Par exemple,
on peut utiliser le template mul2
pour le type short
mais pas pour le type
string
car le corps de mul2
utilise l’opération *
(multiplication) or
celle-ci n’est pas supportée pour le type string
.
short n3 = 2;
short r3 = mul2(n3);
<< n3 << " x 2 = " << r3 << endl;
cout
= "2.1";
string n4 auto r4 = mul2<string>(n4);
<< n4 << " x 2 = " << r4 << endl; cout
Ainsi, si on essaie d’instancier mul2
pour le type string
le compilateur
détectera et signalera l’erreur.
g++ -std=c++17 -Wall -Wextra -O2 -o mul2-2.out mul2-2.cpp
mul2-2.cpp: Dans l'instanciation de « T mul2(T) [avec T = std::__cxx11::basic_string<char>] » :
mul2-2.cpp:27:30: requis depuis ici
mul2-2.cpp:9:13: error: no match for « operator* » (operand types are « std::__cxx11::basic_string<char> » and « int »)
return x*2;
~^~
Les propriétés de type (type traits)
Les type traits sont
des propriétés que l’on peut vérifier sur le type concret demandé pour un
template. Par exemple, si on veut interdire notre template de fonction mul2
pour les double
et ne l’autoriser que pour des entiers (int
, short
, etc),
alors on peut ajouter une assertion sur le type.
// template de fonction avec restriction aux entiers
template <typename T>
(T x) {
T mul2static_assert(is_integral<T>::value); // assertion sur le type T
return x*2;
}
int main() {
// instanciation du template pour int
int n1 = 2;
int r1 = mul2<int>(n1);
<< n1 << " x 2 = " << r1 << endl;
cout
// instanciation du template pour double
double n2 = 2.1;
double r2 = mul2<double>(n2);
<< n2 << " x 2 = " << r2 << endl;
cout
return 0;
}
Cette assertion est vérifiée lors de la compilation. Par exemple, si on essaie
d’instancier le template pour le type double
, on obtient l’erreur de
compilation suivante.
g++ -std=c++17 -Wall -Wextra -O2 -o mul2-3.out mul2-3.cpp
mul2-3.cpp: Dans l'instanciation de « T mul2(T) [avec T = double] » :
mul2-3.cpp:21:24: requis depuis ici
mul2-3.cpp:9:19: error: l'assertion statique a échoué
static_assert(is_integral<T>::value);
^~~~~~~~~~~~~~
Conclusion
Les templates C++ permettent d’écrire du code plus concis tout en profitant des vérifications de types faites par le compilateur. Les fonctionnalités décrites dans cet article couvrent déjà de nombreux cas d’utilisation des templates. D’autres fonctionnalités, plus avancés, seront abordées dans un article suivant.