Logo de SAASF

II - 4. Les templates en C++

Si j'ai bien fait mon travail, il n'y a plus grand chose que vous ne comprenez pas, parmis tout ce que nous avons vu. Cependant, il reste un petit quelque chose que nous devons voir. Si vous vous rappelez bien, quand nous avons parlé des vecteurs dans les structures de données, j'avais employé le terme de "template". Je l'ai aussi employé pour les pointeurs partagés, dans le cours sur les pointeurs. Nous allons voir de quoi il s'agit dans ce petit cours.
Contenu

A. Les "templates"

a. Que ce qu'est un "template" ?

Pour l'instant, tout ce que nous avons vu (ou presque) est utilisé par le compiler pour être converti en langage machine (voir le cours sur les projets C++ pour plus d'informations). Donc, le code C++ est directement converti, sans modification. Or, dans certains cas, cela n'est pas possible. Par exemple, imagineons créer une classe, représentant un vecteur d'un quelconque type. Pour rappel, il faut définir le type dans la classe. Cependant, si nous avons besoin de créer un nouveau type, allons nous rajouter une modification de notre classe, juste pour ce type ? Ce n'est pas une option viable. À la place, nous allons ajouter la possibilité de générer du code C++ (dans notre cas, les classes spécifiques nécessaires pour notre liste) pendant la compilation (avant la conversion en code machine), grâce au système de template.

En fait, un template permet d'utiliser un bout de code, avec une partie que l'on peut spécifier plus tard dans le code. Dans le cas du vecteur, le "plus tard dans le code" représente au moment de la définition d'un vecteur. Pour les templates, la spécification du bout de code doit se situer entre chevrons, comme ça :

std::vector<int> numbers;
std::shared_ptr<std::vector<double>> shared_ptr_towards_vector; // Pointeur partagé vers un vecteur de "double"
Il faut imaginer ça comme si une nouvelle forme de la classe "vector" utilisant le type "int" se crée dans le code (le linker s'occupe des possibles erreurs de redéfinition). En général, les templates s'utilisent pour les classes (comme nous l'avons vu), ou pour les fonctions. Cependant, il faut faire attention à ce que le type que vous spécifiez entre chevron puisse être entièrement utilisé dans la classe / fonction (sans erreur de type "méthode n'est pas déclarée dans type", car aucune vérification n'est effectuée avant la compilation). Pour cela, il faut soit utiliser les objets pour des fonctions / classes très génériques, soit utiliser convenablement l'héritage de membres.

Dans ce cas là, nous avons utiliser les templates avec des types (l'utilisation la plus courant des templates). Cependant, selon la façon dont est définie le template, vous pouvez mettre d'autre valeur litérale dedans, comme des nombres (int ou double). Néanmoins, ce cas est assez rare. Un exemple de classe l'utilisant est la classe bitset, permettant d'utiliser un ensemble de bit, dont la taille est définie par template :

std::bitset<32> number;

b. Crée un template

Créer sa propre utilisation des templates est assez simple. Pour commencer soft, nous allons premièrement voir l'implémentation via une fonction. Les templates se déclarent juste avant la déclaration / définition de la fonction, avec le mot clé template couplé à des instructions entre chevrons. Les instructions entre chevrons dépendent de ce que vous voulez utiliser avec la template. Pour utiliser un type spécifiable, il faut utiliser l'instruction "typename nom_type_specifiable". D'ailleurs, le mot clé "typename" peut être remplacé par le mot clé "class", sans que cela ne change quoi que ce soit. Nous pouvons l'utiliser dans les paramètres :

template <typename T> // Utilisation de "typename"
void print(T object) {
    std::cout << T << std::endl;
}
Dans ce cas là, la spécification du type lors de l'appel de la fonction via les chevrons n'est pas utile (le compiler peut le faire tout seul). Vous pouvez aussi l'utiliser pour un type de retour :
template <class T> // Utilisation de "class" (aucune différence avec "typename")
T number() {
    return 5;
}
Dans ce cas là, la spécification est nécessaire, et le type spécifier doit pouvoir être de valeur 5 (donc un int, un double...). Vous pouvez aussi utiliser plusieurs types :
template <typename T, typename T_2>
T_2 add_number(T& vecteur, T_2 number) {
    number *= 2;
    vecteur.push_back(number);
    return number;
}
Dans ce cas là, nous utilisons une référence vers le type "T", qui doit pouvoir utiliser la méthode "push_back" avec un type "T_2" (comme un std::vector<T_2>), et "T_2" qui doit pouvoir être multiplié par 2. Comme ces deux types sont utilsés en argument, aucun des deux n'a besoin d'être spécifié. Cependant, dans cette exemple :
template <typename T, typename T_2>
T_2 add_number(T& vecteur) {
    double number = 2;
    vecteur.push_back(number);
    return number;
}
"T_2" n'est pas automatique, et doit donc être défini. Or, comme "T_2" est déclaré après "T", il faut définir les deux (comme pour des arguments par défauts de fonctions). D'ailleurs, tout comme les fonctions, nous pouvons définir des types par défaut à un template :
template <typename T, typename T_2 = double>
T_2 add_number(T& vecteur) {
    double number = 2;
    vecteur.push_back(number);
    return number;
}
Faite donc bien attention à comment vous disposer vos templates.

L'utilisation des templates avec des classes est extrêmement similaire à celle dans les fonctions :

template <typename T>
class Super_Vector {

public:
    Super_Vector(){}
    std::vector<T>& datas() {return a_datas;};

private:
    std::vector<T> a_datas;
};
Avec ça, nous créeons un super vecteur, utilisable avec n'importe quel type de données.

Il existe un cas spécial, nécessitant quelques précautions supplémentaires : utiliser en template un type, défini avec un template. Imagineons la classe "Ultra_Vector", qui prend en template un type simialire à "Super_Vector" ou à "std::vector" :

template <typename T, typename T_2>
class Ultra_Vector {

public:
    Ultra_Vector(){}
    T_2<T>& datas() {return a_datas;};

private:
    T_2<T> a_datas;
};
Ce code ne marche pas, car T_2 n'est pas déclaré comme un type utilisant un template (et donc des erreurs apparaissent aux lignes contenant T_2<T>). Il faut donc le ré-indiquer dans le template, en spécifiant que ce type utilise un template avec "typename".
template <typename T, template<typename> typename T_2>
class Ultra_Vector {

public:
    Ultra_Vector(){}
    T_2<T>& datas() {return a_datas;};

private:
    T_2<T> a_datas;
};
Dans ce cas là, indiquer le type utilisé dans T_2 est inutile lors de l'utilisation ultérieure du template (la classe s'en chargera elle même, avec le type T).

Pour utiliser un template avec une autre valeur, comme un nombre, il vous faut remplacer "typename" par le type de la valeur :

template <int T>
void repeat(std::string to_repeat) {
    for(int i = 0;i<T;i++) {
        std::cout << to_repeat << std::endl;
    }
}
Cependant, cela ne marche pas avec tous les types.

c. Simplifier l'utilisation des types

D'un point de vue direct, les templates permettent de créer des classes simplement, de manière extrêmement varié. Le problème : l'utilisation de la classe / template adéquat pour une instanciation de variable peut s'avérer long et / ou compliqué. Pour bien comprendre cette partie, intéressons nous à un trype que nous avons vu et revu : "std::string". Sur le site de documentation du C++ présente juste ici, ce type ne répond pas au nom de "string", mais de "template<class CharT, class Traits = std::char_traits<CharT>, class Allocator = std::allocator<CharT>> class basic_string". En effet, le type "string" n'existe pas, mais est juste une appellation du type "basic_string", où le template "CharT" représente le type "char". Tout cela est écrit en bas de la déclaration du type (vous avez aussi les wstring, u8string...).

Cette appellation du type "basic_string<char>" en "string" est possible grâce à un outil assez proche des templates : le typedef. Un typedef permet de donner une autre appellation à un type complexe, nécessitant des templates spécifiques (ou non). Le nouveau nom du type est en suite directement utilisable comme le nom originale dans le code. Son utilisation est assez simple. Vous devez utiliser le mot clé "typedef", suivi du type à renommer, et du nouveau nom de ce type. Définissons la classe "Basic_Calculator" pour l'utiliser :

template <typename Number_Type>
class Basic_Calculator {

public:
    Basic_Calculator() {};

    Number_Type add(Number_Type number_1, Number_Type number_2) {
        a_last_value = number_1 + number_2;
        return a_last_value;
    };

private:
        Number_Type a_last_value;
};
Maitnenant, nous aimerions créer un type simple pour des calculatrices 8, 16, 32, et 64 bits :
typedef Basic_Calculator<char> Calculator_8_Bits;
typedef Basic_Calculator<short> Calculator_16_Bits;
typedef Basic_Calculator<int> Calculator_32_Bits;
typedef Basic_Calculator<long long> Calculator;
Vous pouvez aussi créer un nom spécial pour un pointeur vers un type ou vers un tableau statique de type, en ajoutant "*" ou "[taille_du_tableau]" sur le nouveau nom du type :
typedef Basic_Calculator<long long> Calculator, *Calculator_ptr, Calculator_array[10];
En général, les typedefs ne sont utilisés que pour simplifier la tâche des développeurs utilisant un type régulièrement.