Syntaxe des templates Le mot-clef template dit au compilateur que la définition de classe qui suit manipulera un ou plusieurs types non spécifiés. Au moment où le code réel de la classe est généré à partir du template, ces types doivent être spécifiés afin que le compilateur puisse les substituer. Pour démontrer la syntaxe, voici un petit exemple qui produit un tableau dont les bornes sont vérifiées : //: C16:Array.cpp #include "../require.h" #include <iostream> using namespace std; template<class T> class Array { enum { size = 100 }; T A[size]; public: T& operator[](int index) { require(index >= 0 && index < size, "Index out of range"); return A[index]; } }; int main() { Array<int> ia; Array<float> fa; for(int i = 0; i < 20; i++) { ia[i] = i * i; fa[i] = float(i) * 1.414; } for(int j = 0; j < 20; j++) cout << j << ": " << ia[j] << ", " << fa[j] << endl; } ///:~ Vous pouvez voir que cela ressemble à une classe normale, sauf en ce qui concerne la ligne template<class T> qui dit que T est le paramètre de substitution, et qu'il représente un nom de type. Vous voyez également que T est utilisé dans la classe partout où vous verriez normalement le type spécifique stocké par le conteneur. Dans Array, les éléments sont insérés et extraits avec la même fonction : l'opérateur surchargé operator [ ]. Il renvoie une référence, afin qu'il puisse être utilisé des deux côtés d'un signe égal (c'est-à-dire àla fois comme lvalue et comme rvalue). Notez que si l'index est hors limite, la fonction require( ) est utilisée pour afficher un message. Comme operator[] est inline, vous pourriez utiliser cette approche pour garantir qu'aucune violation des limites du tableau ne se produit, puis retirer require( ) pour le code livré. Dans main( ), vous pouvez voir comme il est facile de créer des Array qui contiennent différents types. Quand vous dites Array<int> ia; Array<float> fa; le compilateur développe le template Array (c'est appelé instanciation) deux fois, pour créer deux nouvelles classes générées, que vous pouvez considérer comme Array_int et Array_float. (Différents compilateurs peuvent décorer les noms de façons différentes.) Ce sont des classes exactement comme celles que vous auriez produites si vous aviez réalisé la substitution à la main, sauf que le compilateur les crée pour vous quand vous définissez les objets ia et fa. Notez également les définitions de classes dupliquées sont soit évitées par le compilateur, soit funsionnées par l'éditeur de liens.
Définitions de fonctions non inline Bien sûr, vous voudrez parfois définir des fonctions non inline. Dans ce cas, le compilateur a besoin de voir la déclaration template avant la définition de la fonction membre. Voici l'exempleci-dessus, modifié pour montrer la définition de fonctions non inline : //: C16:Array2.cpp // Définition de template non-inline #include "../require.h" template<class T> class Array { enum { size = 100 }; T A[size]; public: T& operator[](int index); }; template<class T> T& Array<T>::operator[](int index) { require(index >= 0 && index < size, "Index out of range"); return A[index]; } int main() { Array<float> fa; fa[0] = 1.414; } ///:~ Toute référence au nom de la classe d'un template doit être accompagnée par sa liste d'arguments de template, comme dans Array<T>::operator[]. Vous pouvez imaginer qu'au niveau interne, le nom de classe est décoré avec les arguments de la liste des arguments du template pour produire un unique identifiant de nom de classe pour chaque instanciation du template. Fichiers d'en-tête Même si vous créez des définitions de fonctions non inline, vous voudrez généralement mettre toutes les déclarations et les définitions d'un template dans un fichier d'en-tête. Ceci paraît violer la règle normale des fichiers d'en-tête qui est “Ne mettez dedans rien qui alloue de l'espace de stockage”, (ce qui évite les erreurs de définitions multiples à l'édition de liens), mais les définitions de template sont spéciales. Tout ce qui est précédé par template<...> signifie que le compilateur n'allouera pas d'espace de stockage pour cela à ce moment, mais, à la place, attendra qu'il lui soit dit de le faire (par l'instanciation du template). En outre, quelque part dans le compilateur ou le linker il y a un mécanisme pour supprimer les définitions multiples d'un template identique. Donc vous placerez presque toujours la déclaration et la définition complètes dans le fichier d'en-tête, par commodité. Vous pourrez avoir parfois besoin de placer la définition du template dans un fichier cpp séparé pour satisfaire à des besoins spéciaux (par exemple, forcer les instanciations de templates à n'exister que dans un seul fichier dll de Windows). La plupart des compilateurs ont des mécanismes pour permettre cela ; vous aurez besoin de chercher dans la notice propre à votre compilateur pour ce faire. Certaines personnes pensent que placer tout le code source de votre implémentation dans un fichier d'en-tête permet à des gens de voler et modifier votre code si ils vous achètent une bibliothèque. Cela peut être un problème, mais cela dépend probablement de la façon dont vous regardez la question : achètent-ils un produit ou un service ? Si c'est un produit, alos vous devez faire tout ce que vous pouvez pour le protéger, et vous ne voulez probablement pas donner votre code source, mais seulement du code compilé. Mais beaucoup de gens considèrent les logiciels comme des services, et même plus que cela, un abonnement à un service. Le client veut votre expertise, il veut que vous continuiez à maintenir cet élément de code réutilisable afin qu'ils n'aient pas à le faire et qu'ils puissent se concentrer sur le travail qu'ils ont à faire. Je pense personnellement que la plupart des clients vous traiteront comme une ressource de valeur et ne voudront pas mettre leur relations avec vous en péril. Pour le peu de personnes qui veulent voler plutôt qu'acheter ou produire un travail original, ils ne peuvent probablement pas continuer avec vous de toute façon.
IntStack comme template Voici le conteneur et l'itérateur de IntStack.cpp, implémentés comme une classe de conteneur générique en utilisant les templates : //: C16:StackTemplate.h // Template stack simple #ifndef STACKTEMPLATE_H #define STACKTEMPLATE_H #include "../require.h" template<class T> class StackTemplate { enum { ssize = 100 }; T stack[ssize]; int top; public: StackTemplate() : top(0) {} void push(const T& i) { require(top < ssize, "Too many push()es"); stack[top++] = i; } T pop() { require(top > 0, "Too many pop()s"); return stack[--top]; } int size() { return top; } }; #endif // STACKTEMPLATE_H ///:~ Remarquez qu'un template fait quelques suppositions sur les objets qu'il contient. Par exemple, StackTemplate suppose qu'il existe une sorte d'opération d'assignation pour T dans la fonction push( ). Vous pourriez dire qu'un template “implique une interface” pour les types qu'il est capable de contenir. Une autre façon de le dire est que les templates fournissent une sorte de mécanisme de typage faible en C++, qui est ordinairement un langage fortement typé. Au lieu d'insister sur le fait qu'un objet soit d'un certain type pour être acceptable, le typage faible requiert seulement que les fonctions membres qu'ils veut appeler soient disponibles pour un objet particulier. Ainsi, le code faiblement typé peut être appliqué à n'importe quel objet qui peut accepter ces appels de fonctions membres, et est ainsi beaucoup plus flexibleToutes les méthodes en Smalltalk et Python sont faiblement typées, et ces langages n'ont donc pas besoin de mécanismes de template. De fait, vous obtenez des templates sans templates.. Voici l'exemple révisé pour tester le template : //: C16:StackTemplateTest.cpp // Teste le template stack simple //{L} fibonacci #include "fibonacci.h" #include "StackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(fibonacci(i)); for(int k = 0; k < 20; k++) cout << is.pop() << endl; ifstream in("StackTemplateTest.cpp"); assure(in, "StackTemplateTest.cpp"); string line; StackTemplate<string> strings; while(getline(in, line)) strings.push(line); while(strings.size() > 0) cout << strings.pop() << endl; } ///:~ La seule différence se trouve dans la création de is. Dans la liste d'arguments du template vous spécifiez le type d'objet que la pile et l'itérateur devraient contenir. Pour démontrer la généricité du template, un StackTemplate est également créé pour contenir des string. On le teste en lisant des lignes depuis le fichier du code source.
Constantes dans les templates Les arguments des templates ne sont pas restreints à des types classes ; vous pouvez également utiliser des type prédéfinis. La valeur de ces arguments devient alors des constantes à la compilation pour cette instanciation particulière du template. Vous pouvez même utiliser des valeurs par défaut pour ces arguments. L'exemple suivant vous permet de fixer la taille de la classe Array pendant l'instanciation, mais fournit également une valeur par défaut : //: C16:Array3.cpp // Types prédéfinis comme arguments de template #include "../require.h" #include <iostream> using namespace std; template<class T, int size = 100> class Array { T array[size]; public: T& operator[](int index) { require(index >= 0 && index < size, "Index out of range"); return array[index]; } int length() const { return size; } }; class Number { float f; public: Number(float ff = 0.0f) : f(ff) {} Number& operator=(const Number& n) { f = n.f; return *this; } operator float() const { return f; } friend ostream& operator<<(ostream& os, const Number& x) { return os << x.f; } }; template<class T, int size = 20> class Holder { Array<T, size>* np; public: Holder() : np(0) {} T& operator[](int i) { require(0 <= i && i < size); if(!np) np = new Array<T, size>; return np->operator[](i); } int length() const { return size; } ~Holder() { delete np; } }; int main() { Holder<Number> h; for(int i = 0; i < 20; i++) h[i] = i; for(int j = 0; j < 20; j++) cout << h[j] << endl; } ///:~ Comme précédemment, Array est un tableau vérifié d'objets et vous empêche d'indexer en dehors des limites. La classe Holder ressemble beaucoup à Array sauf qu'elle contient un pointeur vers un Array au lieu d'inclure objet de type Array. Ce pointeur n'est pas initialisé dans le constructeur ; l'initialisation est repoussée jusqu'au premier accès. On appelle cela l'initialisation paresseuse ; vous pourriez utiliser une technique comme celle-ci si vous créez beaucoup d'objets, mais n'accédez pas à tous, et que vous voulez économiser de l'espace de stockage. Vous remarquerez que la valeur size dans les deux templates n'est jamais stockée au sein de la classe, mais est utilisée comme si elle était une donnée membre dans les fonctions membres.