| ||||
auteurs : Aurélien Regat-Barrel, JolyLoic | ||||
Les templates (modèles en français, ou encore patrons) sont la base de la généricité en C++. Il s'agit en fait de modèles génériques de code qui permettent de créer automatiquement des fonctions (dans le cas de fonctions templates) ou des classes (classes templates) à partir d'un ou plusieurs paramètres.
Le fait de fournir un paramètre à un modèle générique s'appelle la spécialisation. Elle aboutit en effet à la création d'un code spécialisé pour un type donné à partir d'un modèle générique.
Pour cette raison on surnomme aussi les templates des types paramétrés (parameterized types en anglais).
Ces modèles manipulent généralement un type abstrait qui est remplacé par un vrai type C++ au moment de la spécialisation. Ce type abstrait est fourni sous forme de paramètre template qui peut être un type C++, une valeur (entier, enum, pointeur, ...) ou même un autre template. La spécialisation d'un template est transparente et invisible. Elle est effectuée lors de la compilation, de manière interne au compilateur, en fonction des arguments donnés au template (il n'y a pas de code source généré quelque part). Par exemple, vous pouvez réaliser une fonction template renvoyant le plus grand de deux objets de même type pour peu que ce dernier possède un opérateur de comparaison operator > (la fonction standard std::max procède ainsi). Cette fonction template va accepter en argument le type des objets à comparer, appelé type T dans l'exemple suivant :
Si vous appelez cette fonction en fournissant deux int, le compilateur va spécialiser la fonction Max pour le type int, ce qui reviendrait à avoir écrit :
Si vous faites de même avec deux float cette fois-ci, une nouvelle spécialisation de la fonction pour le type float va être générée.
Tout se passe comme si vous aviez écrit deux fois la même fonction, une fois pour le type int et une fois pour le type float. Mais vous n'avez bien qu'une seule fonction template Max, qui opère sur un type abstrait déclaré au moyen du mot-clé typename. Les templates permettent donc de réutiliser facilement du code source, sans devoir utiliser le préprocesseur, ce qui le rend plus lisible et plus rigoureux notamment envers les types manipulés. Notez qu'il est possible de créer des fonctions membres templates. L'exemple suivant crée une classe permettant de construire une chaîne de caractères au moyen de sa fonction membre template Append.
Pour que le code précédent compile sans la fonction membre template, il aurait fallu écrire 4 fonctions membres pour les 4 types utilisés : int, char, const char * et double.
Ces 4 fonctions utiliseraient strictement le même code.
Grâce à l'utilisation des templates, le compilateur a fait ce travail pour nous.
Bien que l'on ait écrit une seule fonction nommée Append, celle-ci n'existe pas en réalité dans le code compilé, mais le compilateur en a généré (spécialisé) quatre. Il en aurait spécialisé dix si dix types différents avaient été utilisés.
|
| ||||
auteur : Aurélien Regat-Barrel | ||||
Prenons comme exemple la fonction suivante qui renvoie le plus grand des deux entiers qui lui sont donnés :
Cet exemple est un cas typique de fonction qu'il est intéressant de rendre générique au moyen des templates. Pour cela, il faut s'affranchir du type int que l'on va remplacer par un type abstrait nommé T grâce aux mots clés template et typename :
Notez qu'on aurait pu utiliser des références constantes comme cela est fait dans la fonction standard std::max, mais il s'agit ici d'un exemple. Le mot clé template indique que la fonction qui suit est une fonction template, et typename dans ce contexte sert à déclarer un nouveau type paramétré pour notre nouvelle fonction template. Il est aussi possible d'utiliser le mot-clé class à la place de typename pour la déclaration des paramètres du template. Nous venons de créer une fonction template Max possédant un seul type paramétré nommé T. Lorsque nous créons une instance de cette fonction Max de cette manière :
Nous demandons au compilateur de spécialiser la fonction Max pour le type int. Ce dernier va en quelque sorte remplacer toutes les occurrences de T par int. Il va d'ailleurs à cette occasion vérifier la validité de l'utilisation de ce type dans le contexte de cette fonction. Avec int pas de problèmes, mais prenons l'exemple suivant :
La classe Test ne possédant pas d'opérateur de comparaison operator >=, la compilation va échouer sur l'utilisation de ce dernier. Les compilateurs récents émettent un message d'erreur assez explicite :
In function `T Max(T, T) [with T = Test]': no match for 'operator>=' in 'A >= B'
ou encore
error C2676: '>=' : 'Test' binaire ne définit pas cet opérateur ou une conversion vers un type acceptable pour l'opérateur prédéfini
Sachez enfin qu'il n'est pas toujours nécessaire de préciser le type du paramètre pour notre template, et que celui-ci peut être déterminé automatiquement par le compilateur (voir Qu'est-ce que la détermination automatique des paramètres templates ?).
|
| ||
auteur : Laurent Gomila | ||
L'écriture de classes templates pose souvent des problèmes de syntaxe ou de conception, voici un exemple illustrant leur écriture :
|
| ||
auteur : Laurent Gomila | ||
Une fonction ou une classe template peut être spécialisée pour certains types de paramètres, c'est ce qu'on appelle la spécialisation. Cela permet entre autre d'avoir un comportement spécifique à certains types de paramètres, à des fins d'optimisation ou pour s'adapter à un comportement particulier par exemple.
Lors de l'utilisation d'un template avec un type donné, le compilateur recherche s'il existe une spécialisation du template pour ce type. S'il en trouve une il utilise cette version spécialisée, sinon il se rabat sur la version générique de base du template.
On peut spécialiser une fonction, une fonction membre template de classe, ou une classe toute entière. Voici la syntaxe à utiliser pour effectuer une spécialisation (attention l'ordre est important : la version générique doit apparaître en premier) :
La spécialisation de classe est elle plus contraignante car il faut redéfinir la totalité de celle-ci.
Attention, un template ne peut être spécialisé qu'à l'intérieur d'un namespace, et pas dans une classe.
|
| |||
auteur : Laurent Gomila | |||
Lorsque vous appelez une fonction template, vous n'avez pas toujours besoin d'indiquer explicitement le type de vos paramètres templates : le compilateur est souvent capable de le faire pour vous.
Ceci n'est pas toujours possible, il existe certaines situations où l'on est obligé de spécifier explicitement le type des paramètres manipulés (lorsque le compilateur ne peut les déduire ou bien pour lever une ambiguïté par exemple).
La détermination automatique des paramètres ne peut s'appliquer que sur des fonctions templates. Pour les classes templates il faut systématiquement les expliciter. |
| ||
auteur : Aurélien Regat-Barrel | ||
Le standard C++ permet de séparer la déclaration d'une classe / fonction template de son implémentation au moyen du mot-clé
export. En théorie, il est donc possible de déclarer sa classe / fonction template dans un fichier .h, et de
l'implémenter dans un .cpp, comme on le fait traditionnelement avec les fonctions / classes non template.
Mais en pratique, c'est une fonctionnalité que seuls (à ce jour) quelques compilateurs basés sur le
front-end d'EDG implémentent (Comeau, Intel...).
Qui plus est, il s'agit d'une fonctionnalité du langage qui a été controversée à un moment ce qui explique
le délai de mise en place dans certains compilateurs. On peut donc considérer que même lorsque c'est possible, il n'est pas encore raisonnable de séparer l'implémentation d'un template de sa déclaration dans l'état actuel des choses. Autrement dit, tout son code doit figurer dans le .h.
On peut cependant conserver la logique de la séparation interface/implémentation en la simulant de cette manière:
L'astuce consiste à inclure à la fin du .h le fichier contenant le corps du template.
Notez l'utilisation de l'extension .tpp au lieu du classique .cpp, afin de faire la distinction avec les fichiers cpp
classiques (pouvant être compilés, contrairement au code template qui doit d'abord être spécialisé avant de pouvoir être
compilé). Il n'y a pas vraiment de convention, on trouve de nombreuses autres extensions : .htt, .tcc, .tpl, ...
Libre à vous de choisir celle que vous préférez.
|
| |||||
auteurs : Laurent Gomila, Aurélien Regat-Barrel, Luc Hermitte | |||||
En plus de l'utilisation qu'on lui connaît pour définir un type en tant que paramètre template,
ou il est possible aussi d'utiliser class :
Le mot-clé typename possède une seconde utilité : il sert à indiquer au compilateur qu'un identifiant est un type,
dans certains contextes manipulant des templates pour lesquels il ne peut pas le deviner automatiquement. (Nous utiliserons
class ici pour introduire les parametres template type pour eviter la confusion avec la premiere utilisation,
naturellement typename est aussi possible)
Prenez cet exemple incorrect :
Dans ce cas vous savez que T::MonType est bien un type, mais le compilo lui ne peut pas le déduire. La raison en est la
suivante : imaginez que l'on spécialise MaClasse (voir Q/R spécialisation) et que l'on définisse MonType autrement :
Bien que l'exemple ci-dessus compile sur certains compilateurs sans avoir recours au mot-clé typename, le standard exige sa
présence, et les compilateurs modernes vont dans ce sens. Il convient donc de l'utiliser même si votre compilateur sait
s'en passer. La syntaxe correcte est donc :
Ce genre d'erreur peut arriver plus souvent que vous ne le pensez, par exemple si vous manipulez des conteneurs standards
dans une classe template :
|
| ||
auteur : Aurélien Regat-Barrel | ||
Malheureusement non, dans le standard C++ actuel on ne peut pas écrire quelque chose comme cela :
Une solution qui peut parfois convenir consiste à utiliser une struct template :
| ||
lien : Merci aux contributeurs du newsgroup fr.comp.lang.c++ |
| ||
auteur : Laurent Gomila | ||
Une classe de trait (trait class), généralement template, définit des caractéristiques ou des fonctions associées
à un type donné. Cela permet donc d'ajouter de l'information à des types que l'on ne peut pas modifier.
Une classe de trait n'est généralement pas destinée à être instanciée, ses membres étant typiquement statiques.
Le template std::numeric_limits<T> de la STL est une classe de traits : elle permet d'ajouter aux types de base
des informations telles que les valeurs min / max, l'epsilon, etc.
Voici un exemple d'une classe de traits qui fournit une valeur nulle appropriée pour chaque type :
| ||
lien : Qu'est-ce qu'une classe de politique ? Comment l'utiliser ? lien : Character Types and Character Traits |
| |||||
auteur : Laurent Gomila | |||||
Les classes de politique (policy classes) sont assez similaires aux classes de traits,
mais contrairement à celles-ci qui ajoutent des informations à des types, les classes de politiques servent à définir des
comportements.
Voici par exemple une fonction qui accumule des éléments et en renvoie la somme, à la manière de std::accumulate :
Ici l'accumulation sera quoiqu'il arrive une somme. En utilisant une classe de politique pour personnaliser l'opération
effectuée, nous pouvons rendre cette fonction beaucoup plus générique :
On voit ici une propriété typique des classes de politique : Addition est "orthogonale" aux autres paramètres
templates de la fonction, c'est-à-dire ici qu'elle ne dépend pas du type T qu'elle manipule. Celui-ci peut être int tout
comme std::string, notre classe de politique n'y verra aucune différence.
Pour modifier le comportement de la fonction Accumulation pour par exemple multiplier les éléments, il suffirait
d'écrire une classe politique Multiplication qui remplacerait += par *=, et la passer en paramètre à
Accumulation. On pourrait également imaginer utiliser Accumulation pour extraire un minimum, ou pour faire encore beaucoup d'autres choses.
Une fonction qui prend en paramètre une classe de politique aura généralement une valeur par défaut assez évidente
(par exemple ici la politique Addition). Cependant, les fonctions n'acceptant pas les paramètres templates par
défaut (cela sera certainement corrigé dans une future norme du langage), il faudra remplacer votre fonction non membre par
une fonction statique encapsulée dans une classe. Bien sûr ensuite rien ne vous empêche de fournir des fonctions qui
encapsulent l'appel à cette fonction membre.
Enfin, pour faire le lien entre politiques et traits, on peut remarquer que notre fonction d'accumulation possède quelques
défauts. Par exemple, la valeur zéro du type T ne sera pas forcément 0 (ce sera par exemple "" pour les std::string).
Ainsi nous pouvons utiliser la classe de traits définie dans Qu'est-ce qu'une classe de trait ? Comment l'utiliser ? pour l'améliorer :
Les classes de politique sont utilisées intensivement dans la bibliothèque Loki, et de ce fait très bien décrites dans
le livre Modern C++ Design d'Andrei Alexandrescu.
Les classes de traits et de politique sont également décrites et comparées dans C++ templates - the complete guide
de David Vandevoorde et Nicolai M. Josuttis.
|
Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.