Les templates (1/2)
Le code template peut désigner une variable template, une fonction template, un alias template ou une classe template : la variable, la fonction, l'alias ou la classe va donc avoir un ou plusieurs paramètres templates, c'est-à-dire des types ou des entiers connus à la compilation. Les templates sont des modèles qui vont recopier le code écrit à chaque fois que les paramètres templates sont différents.
Cette instanciation de code à la compilation permet aux templates de former un langage Turing-Complet, s'exécutant à la compilation. On exploitera cette propriété surtout dans la seconde partie sur les templates.
Du code template peuvent être définies à l'intérieur d'autres classes template, mais pas à l'intérieur de fonctions.
Duck typing
Les templates fonctionnent selon le même principe, ce qui nous permet de supposer n'importe quelles propriétés sur les paramètres templates (présence de variables statiques, de méthodes, ...) :
Si les propriétés requises sur les types ne sont pas vérifiées, une erreur sera émise à la compilation. Ce duck typing permet donc d'avoir un code concis mais néanmoins vérifié à la compilation.
De plus, le duck typing est renforcé par la déduction de types possible, comme avec auto
. Les fonctions templates peuvent déduire leurs paramètres templates si leurs arguments le permet, et il en va de même pour les classes template grâce à leur constructeur depuis C++17 :
Spécialisations
Un template est spécialisé lorsqu'il est redéfini pour un ou plusieurs paramètres templates. Les classes, les fonctions et les variables templates peuvent être spécialisés, mais les fonctions template ne peuvent pas être partiellement spécialisées (c'est-à-dire spécialiser la fonction avec au moins un paramètre template non fixé).
std::vector<bool>
est une spécialisation de std::vector<T>
pour stocker les booléens dans un seul bit. Cependant, son utilisation est différente des autres std::vector<T>
, notamment à cause d'un type proxy rajouté. Il s'agit d'un exemple connu de mauvais design.
Dans le cas des fonctions, les overloads sont une meilleure alternative aux spécialisations. Si on souhaite quand même des spécialisations, il faut alors éviter de les mélanger aux overloads.
Traits et concepts
Le duck typing peut mener à des erreurs de compilation parfois illisibles, car elles arrivent lorsque le compilateur rencontre un problème dans l'implémentation des templates. On peut alors vérifier la validité des paramètres templates à la main, avec notamment std::enable_if
(que l'on verra dans la seconde partie) ou static_assert
.
static_assert
est une assertion qui s'exécutera à la compilation : il ne modifiera pas le code source, mais arrêta la compilation avec le message donné si sa condition n'est pas évaluée à true
. Cette instruction est souvent utilisée avec des traits, comme ceux disponibles dans <type_traits>
: il s'agit de classes qui associent à un ou plusieurs types des propriétés à la compilation.
static_assert
peut être utilisé dans l'espace global, ou dans la définition d'une classe.
La librairie standard ne fournit pas de traits pour vérifier l'appartenance à une classe à ses concepts, ils n'existent donc pour l'instant qu'en documentation. En C++20, les Concepts vont arriver dans le langage : ils permettront de définir de nouveaux concepts à la compilation, et de les utiliser pour contraindre facilement des types templates. Actuellement, aucun trait n'existe dans la librairie standard pour vérifier des concepts comme des itérateurs ou des collections : il faut donc se référer à la documentation.
Variadic templates
Les paramètres templates peuvent être des listes d'arguments (des objets ou des types). Par exemple, les classes de la librairie standard std::tuple
ou std::function
sont basées dessus. sizeof...(Param)
permet d'obtenir le nombre d'éléments contenu dans la liste d'argument. Plusieurs opérations permettent d'utiliser ces listes, avec les trois petit points.
Souvent, ces listes doivent être utilisées via de la récursion, ce qui ressemble à l'utilisation de langages fonctionnels :
Constexpr
Le mot-clé relativement récent constexpr
permet d'assurer qu'une valeur est connue à la compilation. Cela permet d'avoir du code s'exécutant à la compilation plus compréhensible que les templates. Son usage est toutefois plus limité.
if constexpr
est un branchement ayant lieu à la compilation. Cela permet de renvoyer des types différents dans les blocs if
/ else
, ou d'écrire du code non valide pour les paramètres templates dans le bloc qui n'est pas emprunté.
Templates vs interfaces
Les templates et les interfaces permettent de traiter plusieurs types de la même façon, mais de manière très différentes.
Concernant les performances, les templates n'ajoutent aucun surcoût, alors que les interfaces modifient les objets qui l'implémentent et ajoutent des indirections pour appeler les fonctions virtuelles. De plus, leur utilisation va souvent amener à allouer de façon dynamique ces objets. Cependant :
Les templates sont plus lents à compiler et dupliquent du code.
Ils nécessitent d'être définis pour être générés où ils sont utilisés.
Concernant les abstractions, les interfaces permettent de ne pas connaître le type exact de l'objet à la compilation. Cela permet par exemple de ne pas propager les types dont dépend une classe dans sa signature. Cependant :
Les classes existantes ne peuvent pas implémenter d'autres interfaces.
Les types concrets ne plus plus être utilisés qu'à travers leurs fonctions virtuelles.
Pour aller plus loin
Il peut être intéressant d'optimiser la génération de code en passant un paramètre de fonction en paramètre template, afin que la fonction puisse connaître sa valeur à la compilation. C'est vraiment efficace que si cela permet au compilateur de faire de la constant propagation (pour connaître d'autres valeurs à la compilation).
Challenge
(***) Ecrire une fonction template to_string(T const& value)
qui permet d'afficher différents types (nombres, collections, tuples voire aggrégats). Exemple :
Last updated