Nous allons maintenant rentrer dans le vif du sujet en vous présentant le code de différents templates.
Nous détaillerons ensuite chacun des éléments de la syntaxe, afin que vous puissiez les comprendre pour les réutiliser dans votre propre code.
Cet exemple consiste en lâimplĂ©mentation dâun tableau de taille statique (un peu comme la classe std::array
).
#include <algorithm>
#include <cassert>
#include <iostream>
template <typename ElementType, size_t Size>
class StaticArray
{
public:
// Rappel: l'implémentation par défaut du constructeur par défaut disparaßt lorsqu'on
// définit explicitement un autre constructeur (ce qui est le cas ici).
// Ecrire `Class() = default` permet de demander au compilateur de générer le constructeur
// par dĂ©faut de Class, mĂȘme si on a dĂ©finit un autre constructeur.
StaticArray() = default;
StaticArray(const ElementType& element)
{
fill(element);
}
ElementType& operator[](size_t index)
{
assert(index < Size);
return _elements[index];
}
const ElementType& operator[](size_t index) const
{
assert(index < Size);
return _elements[index];
}
void fill(const ElementType& element)
{
std::fill_n(_elements, Size, element);
}
size_t size() const { return Size; }
const ElementType* begin() const { return _elements; }
const ElementType* end() const { return _elements + Size; }
private:
// Rappel: en écrivant {} sur un initializer de tableau statique C, cela permet d'initialiser tous les éléments
// du tableau par défaut (0 pour les types primitifs).
ElementType _elements[Size] {};
};
int main()
{
StaticArray<int, 3> array_of_3_ints;
// [0, 0, 0]
array_of_3_ints[1] = 3;
for (const auto& e: array_of_3_ints)
{
std::cout << e << " ";
}
std::cout << std::endl;
// -> '0 3 0'
StaticArray<std::string, 5> array_of_5_strings { "toto" };
// ["toto", "toto", "toto", "toto", "toto"]
array_of_5_strings[1] = "est";
array_of_5_strings[2] = "vraiment";
array_of_5_strings[3] = "un";
array_of_5_strings[4] = "feignant";
for (const auto& e: array_of_5_strings)
{
std::cout << e << " ";
}
std::cout << std::endl;
// -> 'toto est vraiment un feignant'
return 0;
}
DĂ©cortiquons maintenant ce petit programme.
Lâen-tĂȘte se rĂ©sume Ă :
template <typename ElementType, size_t Size>
class StaticArray
{ ... };
Les deux premiÚres lignes permettent de définir une classe-template StaticArray
, paramétrée par un paramÚtre ElementType
et un paramĂštre Size
.
Tout comme les paramÚtres de fonction, les paramÚtres de template sont typés.
Ici, ElementType
est de type typename
et Size
est de type size_t
.
Pour générer une classe à partir de StaticArray
, il faudra donc dâabord fournir un nom de type.
Il peut sâagir dâun nom de classe ou structure (comme Animal
ou std::string
) ou bien dâun type primitif (comme int
, float
ou char
).
Il faudra Ă©galement fournir une valeur de type size_t
.
Attention, lorsque vous fournissez des arguments Ă un template, il faut que leurs valeurs soit Ă©valuables lors de la compilation.
Câest Ă©vident lorsque le paramĂštre est de type typename
, puisque tous les noms de types sont forcément connus à la compilation.
En revanche, lorsque vous avez des paramĂštres de type âvaleurâ (int
, char
ou size_t
comme ici), le compilateur doit ĂȘtre capable de calculer ce que vous lui envoyer.
StaticArray<int, 3> array;
// Pas de problÚme, on passe un litéral en tant que deuxiÚme paramÚtre.
// Le compilateur n'aura aucun souci pour évaluer sa valeur et générer la classe.
StaticArray<int, 3+5> array;
// Pas de problÚme non plus, car le compilateur est capable d'effectuer des opérations avec
// des litéraux.
size_t size = 3;
StaticArray<int, size> array;
// Ici en revanche, ça ne compilera pas. Bien qu'un humain soit clairement capable de dire
// qu'au moment d'instancier array, size vaut forcément 3, ce n'est pas le cas du compilateur.
constexpr size_t size = 3;
StaticArray<int, size> array;
// Ca fonctionne ici, car constexpr permet de dĂ©finir des variables dont la valeur est Ă©valuĂ©e Ă
// la compilation. Cela implique bien sûr que la variable est constante, mais aussi qu'on peut
// l'utiliser en tant qu'argument de template.
Vous pouvez utiliser les paramĂštres du template nâimporte oĂč dans le code de votre classe, aussi bien dans la dĂ©finition des attributs, que dans le contenu des fonctions-membre ou de leur signature.
Par exemple, ElementType
et Size
ont tous les deux servis Ă dĂ©finir lâattribut _elements
:
ElementType _elements[Size] {};
On définit un attribut _elements
qui est un tableau statique C de taille Size
et donc les éléments sont de type ElementType
.
Si cette ligne vous a un peu dĂ©routĂ©, vous devriez remarquer quâelle nâest en fait pas plus compliquĂ©e que int _elements[3] {};
On a juste dĂ©cidĂ© dâavoir un tableau dâElementType
plutĂŽt que dâentier, et de taille Size
plutĂŽt que 3
.
Le paramĂštre Size
a été utilisé dans la définition de operator[]
, pour vĂ©rifier que lâindice passĂ© Ă la fonction est infĂ©rieur Ă la taille du tableau :
assert(index < Size);
Toujours pour operator[]
, ElementType
a servi à spécifier le type de retour de chacune des surcharges :
ElementType& operator[](size_t index)
const ElementType& operator[](size_t index) const
Vous pouvez retrouver le code de StaticArray
sur
GodBolt.
Essayez maintenant de définir une toute petite structure templatée Triple
, contenant trois éléments first
, second
et third
qui nâont pas forcĂ©ment le mĂȘme type.
Il sâagira donc en quelque sorte dâune gĂ©nĂ©ralisation de std::pair
.
Votre classe devra comporter un constructeur par dĂ©faut, et un constructeur Ă trois paramĂštres permettant dâinitialiser chacun des Ă©lĂ©ments du triplet.
Vous trouverez une solution ici.
Un cas dâutilisation standard de fonction-template est dâĂ©viter dâavoir Ă Ă©crire tous les surcharges possibles et imaginables pour une fonction.
Voici par exemple comment Ă©crire une fonction-template add
qui accepte des paramĂštres de nâimporte quel type (du moment quâil est possible de les additionner avec operator+
), et retourne le rĂ©sultat converti dans nâimporte quel type Ă©galement.
template <typename Result, typename Param1, typename Param2>
Result add(const Param1& p1, const Param2& p2)
{
return static_cast<Result>(p1 + p2);
}
Et voici diffĂ©rentes maniĂšres dâutiliser cette fonction-template.
// En indiquant explicitement tous les paramĂštres de template.
auto r1 = add<int, float, float>(3.8, 6.5); // r1: int 10
// On peut aussi omettre le ou les derniers paramĂštres si ceux-ci peuvent ĂȘtre dĂ©duits des arguments passĂ©s Ă la fonction.
// Par exemple, ici, 3.8 et 6.5 sont des litéraux de type double, c'est donc add<int, double, double> qui est générée.
auto r2 = add<int>(3.8, 6.5); // r2: int 10
// Si on omet des paramĂštres qui ne peuvent pas ĂȘtre dĂ©duits automatiquement, alors l'appel ne compilera pas.
// Ici, le premier paramĂštre sert Ă donner le type de retour de la fonction et il n'est pas dĂ©ductible des arguments passĂ©s Ă
// la fonction. Le code suivant ne compile donc pas (mĂȘme en indiquant explicitement le type de la variable dans lequel on
// récupÚre le résultat).
int r3 = add(3.8, 6.5); // ne compile pas
On peut Ă©galement utiliser des templates pour faire du polymorphisme.
enum class Category { Bird, Mammal, Mythical, Insect };
struct Chicken
{
static constexpr Category category = Category::Bird;
};
struct Dog
{
static constexpr Category category = Category::Mammal;
};
struct Pegasus
{
static constexpr Category category = Category::Mythical;
bool can_fly() const { return true; }
};
struct Caterpillar
{
static constexpr Category category = Category::Insect;
bool can_fly() const { return _is_butterfly; }
void evolve() { _is_butterfly = true; }
bool _is_butterfly = false;
};
template <typename Animal>
bool has_wings(const Animal& animal)
{
if constexpr (Animal::category == Category::Bird)
{
return true;
}
else if constexpr (Animal::category == Category::Insect || Animal::category == Category::Mythical)
{
return animal.can_fly();
}
else
{
return false;
}
}
Dans lâexemple ci-dessus, on a une fonction-template has_wings
paramétrée par le type Animal
.
Ce type doit présenter un attribut statique category
, et dans le cas oĂč il vaut Insect
ou Mythical
, il faut pouvoir appeler une fonction can_fly() -> bool
sur une instance dâAnimal
.
int main()
{
Chicken chicken;
Dog dog;
Pegasus pegasus;
Caterpillar caterpillar;
std::cout << has_wings(chicken) << std::endl; // -> 1
std::cout << has_wings(dog) << std::endl; // -> 0
std::cout << has_wings(pegasus) << std::endl; // -> 1
std::cout << has_wings(caterpillar) << std::endl; // -> 0
caterpillar.evolve();
std::cout << has_wings(caterpillar) << std::endl; // -> 1
std::string animal = "animal";
std::cout << has_wings(animal) << std::endl; // ne compile pas, car std::string n'a pas d'attribut statique `Category` !
return 0;
}
Dans lâimplĂ©mentation de has_wings
, vous avez dĂ» vous demander ce que signifiaient les if constexpr
.
Lors de la génération du code, ce if
particulier permet de demander au compilateur de ne conserver et compiler que la branche qui serait Ă©valuĂ©e dans le cas dâun if
classique.
Par exemple, le compilateur générera le code suivant pour la fonction has_wings<Chicken>
:
bool has_wings(const Chicken& animal)
{
return true;
}
Si on avait utilisé des if
non constexpr, alors, la fonction générée aurait été la suivante et on aurait eu une erreur de compilation :
bool has_wings(const Chicken& animal)
{
if (Chicken::category == Category::Bird)
{
return true;
}
else if (Chicken::category == Category::Insect || Chicken::category == Category::Mythical)
{
return animal.can_fly(); // Erreur : la classe Chicken n'a pas de fonction-membre `can_fly`.
}
else
{
return false;
}
}
LâintĂ©rĂȘt dâutiliser des if constexpr
, câest donc de faire en sorte que le compilateur ne gĂ©nĂšre que le code vraiment nĂ©cessaire Ă chaque instantiation dâun template.
La contrainte, câest quâil faut que la condition de ce type de if
puisse ĂȘtre Ă©valuĂ©e au moment de la compilation.
Câest notamment pour cela que nous avons dĂ» spĂ©cifier lâattribut category
comme Ă©tant constexpr
dans les classes Chicken
, Dog
, Pegasus
et Caterpillar
.
Lorsquâon veut Ă©crire une fonction-template qui accepte en paramĂštre un objet dont le type est templatĂ©, alors il peut ĂȘtre intĂ©ressant de templater la fonction avec les mĂȘmes paramĂštres de template que cet objet.
On pourrait par exemple dĂ©finir lâoperator<<
de la classe-template StaticArray
définie plus haut de la maniÚre suivante :
template <typename ElementType, size_t Size>
std::ostream& operator<<(std::ostream& stream, const StaticArray<ElementType, Size>& array)
{
for (const auto& e: array)
{
stream << e << " ";
}
return stream;
}
int main()
{
StaticArray<int, 3> array_of_3_ints;
array_of_3_ints[1] = 3;
// Génération de operator<< <int, 3>(ostream&, const StaticArray<int, 3>&)
std::cout << array_of_3_ints << std::endl;
StaticArray<std::string, 5> array_of_5_strings { "toto" };
array_of_5_strings[1] = "est";
array_of_5_strings[2] = "vraiment";
array_of_5_strings[3] = "un";
array_of_5_strings[4] = "feignant";
// Génération de operator<< <string, 5>(ostream&, const StaticArray<string, 5>&)
std::cout << array_of_5_strings << std::endl;
return 0;
}
const char*
, std::string
ou std::string_view
.
Le résultat sera de type std::string
.convert_to_string
qui prend un paramÚtre de type quelconque, et si ce paramÚtre est un nombre (regardez les fonctions définies dans <type_traits>
), convertissez-le en std::string
Ă lâaide de std::to_string
.
Autrement, renvoyez le paramĂštre tel quel.
Modifiez ensuite votre fonction de concatĂ©nation pour quâelle puisse fonctionner avec des nombres Ă©galement.Vous trouverez une solution ici.