Exemples

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.


Premier exemple de classe-template

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.


En-tĂȘte de la classe-template

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.

Utilisation des paramĂštres de template dans la classe

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

Un peu de pratique

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.


Premiers exemples de fonctions-template

Overloading

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

Polymorphisme

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.


Transfert de paramĂštres de template

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;
}

Un peu de pratique
  1. Implémentez une fonction qui prend deux conteneurs quelconques en paramÚtre et renvoie la somme de leurs tailles.
  2. ImplĂ©mentez une fonction concatĂ©nant deux chaĂźnes de caractĂšres, pouvant ĂȘtre de type const char*, std::string ou std::string_view. Le rĂ©sultat sera de type std::string.
  3. ImplĂ©mentez une fonction 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.