Écrivez-le une fois, utilisez-le pour tous les types
Sur la page précédente, vous avez trié un vector<int> avec std::sort. Mais std::sort trie aussi un vector<string>, un vector<double> ou un tableau de vos propres structs - sans que personne n'écrive un sort distinct pour chacun. Ce n'est ni de la magie ni de la surcharge. C'est un template : un unique morceau de code que le compilateur réutilise pour n'importe quel type que vous lui donnez.
Sans les templates, vous seriez condamné à copier-coller la même logique pour chaque type. Voici la même fonction maximum écrite trois fois, exactement la duplication que les templates existent pour éliminer :
int maximum(int a, int b) { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }
Les corps sont identiques. Seuls les types diffèrent. Un template vous permet de dire « ceci fonctionne pour n'importe quel type T » et de l'écrire une seule fois.
Templates de fonction
Vous transformez une fonction en template en ajoutant template <typename T> devant elle et en utilisant T partout où un type concret irait normalement.
Remarquez que vous n'avez jamais écrit maximum<int> ni maximum<double>. Le compilateur examine les arguments et détermine ce que doit être T : c'est la déduction des arguments de template. Chaque type distinct avec lequel vous l'appelez amène le compilateur à instancier (générer) une fonction concrète distincte en coulisses.
Vous pouvez indiquer le type explicitement lorsque la déduction ne peut pas aider, à l'aide des chevrons :
Un piège courant se cache dans la déduction. Comme T doit être un seul type, mélanger les types d'arguments la casse :
maximum(3, 7.5); // ERREUR : T est-il int ou double ? Le compilateur refuse de deviner.
Vous pouvez corriger cela en étant explicite - maximum<double>(3, 7.5) - ou en donnant à chaque paramètre son propre paramètre de type, ce que nous ferons ensuite.
Paramètres de type multiples
Un template ne se limite pas à un seul type. Énumérez-en autant que nécessaire, séparés par des virgules. C'est ainsi que vous écrivez une fonction dont les paramètres peuvent être de types différents :
Lorsque le type de retour dépend des paramètres, laissez le compilateur le déterminer avec auto (C++14 et versions ultérieures), qui se marie naturellement avec les templates :
Templates de classe
Les templates ne servent pas qu'aux fonctions : des classes entières peuvent être templatisées. C'est exactement ainsi que fonctionnent les conteneurs standard : vector<int>, le map clé-valeur (map<string, int>) et pair<A, B> sont tous des templates de classe. Vous écrivez la structure de données une seule fois et elle stocke le type que vous lui passez en paramètre.
Voici une petite Box générique qui contient une valeur de n'importe quel type :
La différence essentielle avec les templates de fonction : avec un template de classe, vous devez généralement fournir le type entre chevrons - Box<int> - car dans les normes plus anciennes il n'y a aucun argument de constructeur à partir duquel le déduire. (C++17 a ajouté la déduction des arguments de template de classe, donc Box b(42); fonctionne aussi, mais être explicite est toujours sûr et se lit clairement.)
Les erreurs seront gigantesques - voici pourquoi
C'est la partie sur laquelle tout le monde bute, alors autant le dire clairement. Un template n'est entièrement vérifié que lorsqu'il est instancié avec un type réel. Vous pouvez écrire un template qui utilise <, et il compile très bien tout seul : l'erreur n'apparaît qu'au moment où vous l'instanciez avec un type qui n'a pas de <.
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b; // exige que T prenne en charge >
}
struct Point { int x, y; };
// maximum(Point{1,2}, Point{3,4});
// ERREUR : pas d'operator > pour Point. Le message nomme Point ET
// cite cette fonction entière, s'étalant souvent sur de nombreuses lignes.
Comme le compilateur substitue le type complet dans le template et signale les échecs depuis l'intérieur du code généré, une seule erreur peut produire un mur de sortie mentionnant les rouages internes de la bibliothèque. Deux conseils de survie :
- Lisez la première erreur, pas la dernière. Les erreurs suivantes ne sont généralement que les retombées de la première.
- Cherchez dans le message le nom de votre propre type (ici,
Point). Cela vous indique quelle instanciation a échoué.
La vraie solution consiste à vous assurer que votre type prend en charge tout ce dont le template a besoin - pour maximum, cela signifie la surcharge de l'operator> pour Point, ce qui fera l'objet d'une page ultérieure. Les concepts du C++20 moderne peuvent faire remonter ces erreurs plus tôt et les rendre lisibles, mais le modèle de substitution sous-jacent reste le même.
Suite : Les classes
Vous venez de construire un template de classe Box - une classe avec des données privées, un constructeur et des fonctions membres - tout en vous concentrant sur l'aspect templates. La page suivante ralentit le rythme et enseigne les classes correctement : comment regrouper les données avec les fonctions qui opèrent dessus, ce que public et private contrôlent réellement, et comment les fonctions membres accèdent à l'état propre de l'objet. Les templates et les classes se combinent en permanence dans le vrai C++, alors une bonne maîtrise des classes rend l'écriture de code générique bien plus facile.
Questions fréquentes
Qu'est-ce qu'un template en C++ ?
Un template est un modèle qui vous permet d'écrire une fonction ou une classe une seule fois et de laisser le compilateur générer une version pour chaque type avec lequel vous l'utilisez. Vous écrivez template <typename T>, puis vous utilisez T à la place du type réel. Le compilateur produit une version concrète : c'est ce qu'on appelle l'instanciation.
Quelle est la différence entre typename et class dans un template C++ ?
typename et class dans un template C++ ?Dans une liste de paramètres de template, template <typename T> et template <class T> signifient exactement la même chose. On préfère généralement typename aujourd'hui parce qu'il se lit de façon plus honnête : T peut être n'importe quel type, pas seulement une classe. Le choix du mot-clé n'a aucun effet sur le code généré.
Pourquoi les messages d'erreur des templates C++ sont-ils si longs ?
Les templates sont vérifiés lorsqu'ils sont instanciés avec un type réel, et non lorsqu'ils sont écrits. Si un type ne prend pas en charge une opération que vous avez utilisée (comme < pour le tri), l'erreur apparaît au plus profond du code de la bibliothèque, avec le type instancié complet écrit en toutes lettres, produisant des pages entières de sortie. Lisez la première erreur et cherchez-y le nom de votre type.