4. PATRONS DE COMPOSANT ET CLASSES GENERIQUES
 

Les patrons de composant sont l'une des particularités du langage C++. Ils sont souvent appelés templates (tiré du mot-clé utilisé dans le langage). Ce concept permet de paramétrer, dans un composant (une fonction ou une classe), le type de certaines données manipulées. Contrairement aux arguments d'une fonction ou d'une méthode, les inconnues ne sont pas des valeurs mais des types. Par la suite, nous ferons la différence entre le terme paramètre, employé pour désigner un paramètre d'un patron et donc un type, et le terme argument, employé pour désigner un paramètre d'une méthode et donc une variable.

Ce concept ne fait pas partie intégrante du paradigme objet, car il est tout à fait possible de définir le patron d'une fonction, notion qui n'est pas objet. Les patrons de composant ont d'ailleurs donné naissance à un nouveau type de programmation dite générique. Néanmoins, il est très courant de combiner la programmation générique avec l'approche orientée objet, en définissant des patrons de classe qu'on nomme classes génériques. Dans ce contexte, il est plus judicieux de considérer les patrons comme une extension de la programmation orientée objet.

Après avoir présenté les notions de base sur les patrons de fonction et de classe en C++, nous nous penchons sur des aspects plus particuliers comme la définition de paramètres par défaut, ainsi que l'instanciation partielle d'un patron, qui permet de spécialiser ce dernier pour certains paramètres.

 
CLASSE GENERIQUE ET META-CLASSE
 

Un patron de classe (respectivement de fonction) est un modèle de classe (respectivement de fonction), de la même manière qu'une classe est un modèle d'objet. La différence entre une classe générique et une classe simple se trouve dans l'instanciation du modèle qui fournit une classe et non pas un objet. On est d'ailleurs tenté de dire qu'une classe générique est une méta-classe (i.e. une classe qui est un modèle de classe), mais en C++ notamment, une classe générique n'est pas traitée comme une classe, il s'agit simplement d'un modèle. Le terme méta-classe est alors plutôt réservé aux langages comme Java qui considèrent les classes comme des objets, ce qui signifie qu'il existe des classes dont l'instance unique est une classe, et ce sont elles les méta-classes.

 
PATRON DE FONCTION
 
 
De la macrocommande au template
 

Pour illustrer ce qu'est un patron de fonction, nous avons choisi l'exemple de la fonction max qui est sensée retourner le maximum de deux valeurs. Avec une approche classique, on est obligé de définir cette fonction pour chaque type de donnée.

inline int    max(int a,int b) { return (a>b ? a : b); }
inline double max(double a,double b) { return (a>b ? a : b); }

Le principal défaut réside dans la réécriture manuelle du code pour chaque type de donnée. Une autre solution en C consiste à utiliser une macrocommande.

#define max(a,b) ((a)>(b) ? (a) : (b))

On évite ainsi de réécrire manuellement le code pour chaque type de donnée, mais on introduit d'autres problèmes. Tout d'abord, contrairement à la version précédente, aucune vérification de type n'est effectuée. On peut alors comparer des éléments de nature différente. Ensuite, il peut se produire des effets de bord comme dans l'exemple suivant.

int x = ...;
int y = ...;
...
int z = max(x++,y++);

Grâce aux patrons, on peut éviter tous ces défauts. L'exemple suivant définit un modèle de la fonction max dont le paramètre T est le type des objets qui sont comparés.

template <typename T>
inline const T & max(const T & a,const T & b)
{ return (a>b ? a : b); }

 
Instanciation d'un template
 

Il faut bien comprendre que le code précédent n'est pas une fonction, mais un modèle. Ainsi, tel quel, il n'y a pas de compilation du code. Cela ne se produira que lorsqu'on instanciera le modèle, c'est-à-dire lorsqu'on précisera le véritable type des paramètres. Ainsi, du modèle max, on peut définir la fonction max<int> qui est l'instanciation du modèle pour T = int. Imaginons deux entiers (de type int) i et j, le code suivant instancie la fonction max<int>.

std::cout << max<int>(i,j) << std::endl;

Il y a tout de même deux remarques concernant l'instanciation d'un patron. Tout d'abord, comme il s'agit d'un modèle, un patron doit toujours se trouver dans un header, sinon il ne pourra pas être instancié chaque fois qu'on en a besoin. Ensuite, l'instanciation du patron correspond simplement à remplacer le paramètre T par un type donné, dans notre exemple int. Une fois ce code généré, il est compilé comme s'il avait été écrit directement par un programmeur. Il n'y a donc aucune différence (excepté l'automatisme) avec un copier/coller à la main du code. Cela signifie que pour chaque jeu de paramètres, une fonction est créée, même si le modèle n'est écrit qu'une seule fois. Néanmoins, si l'on fait appel au modèle plusieurs fois avec le même jeu de paramètres, la fonction associée n'est présente qu'une seule fois dans le programme (bien qu'elle puisse être compilée plusieurs fois, à cause de la compilation séparée).

En résumé, un patron (de classe ou de fonction) doit toujours être défini dans un header, ce qui peut conduire à une augmentation significative du temps de compilation. En outre, le patron est instancié pour chaque jeu (différent) de paramètres avec lequel il est appelé, ce qui peut également induire une augmentation significative de la taille du code généré.

 
Polymorphisme statique
 

Il n'est pas toujours nécessaire de préciser les paramètres d'un patron pour l'instancier. En effet, dans l'exemple précédent, les arguments i et j sont tous les deux de type int, le compilateur peut alors, en analysant le patron max, déduire que T est de type int. Le code suivant est donc valide et équivalent au précédent.

std::cout << max(i,j) << std::endl;

Ce mécanisme est appelé polymorphisme statique, puisqu'on écrit simplement le nom du patron et le compilateur décide seul de la fonction à instancier. Mais ce procédé implicite peut causer quelques problèmes, comme le montre l'exemple suivant.

int i,j;
long k,l;
Chaine a,b,c;
...
std::cout << max(i,j) << std::endl;
std::cout << max(k,l) << std::endl;
std::cout << max<long>(i,l) << std::endl;
std::cout << max(long(i),l) << std::endl;
std::cout << max(a,b) << std::endl;

Aux deux premières lignes, les fonctions max<int> et max<long> sont appelées implicitement. En revanche, si on ne précise pas le paramètre du patron à la troisième ligne, e.g. max(i,l), quelle instanciation le compilateur va-t'il choisir, max<int> ou max<long> ? Comme la règle de conversion int vers long est implicite, le compilateur choisira certainement la seconde instanciation, mais pour éviter toute confusion, il est plus sage de préciser l'instanciation à ce moment-là. Il est également possible d'expliciter la conversion int vers long (cf. la quatrième ligne), le polymorphisme statique peut alors opérer correctement.

 
Notion de concept
 

Dans le cas de max<Chaine> (cf. dernière ligne de l'exemple précédent), la compilation de la fonction requiert un opérateur de comparaison > pour la classe Chaine, ce que le compilateur ne peut pas vérifier à l'avance. Cela va donc se traduire par une erreur de compilation de la fonction max<Chaine> sur la ligne qui tente d'appeler l'opérateur >. Ce message est malheureusement assez déroutant si la personne qui utilise le patron n'est pas celle qui l'a écrit, car l'erreur va pointer le code de la fonction.

Pour faciliter l'usage des patrons, il est très important, au moment de la définition d'un template, de spécifier quelle interface un paramètre doit respecter pour que le patron puisse être instancié. Dans notre exemple, il faudrait idéalement pouvoir indiquer que T doit posséder l'opérateur >. L'interface nécessaire à un paramètre pour l'instanciation de son template est plus souvent appelée concept. En C++, cette notion est implicite et il incombe au programmeur de bien documenter ses patrons pour informer au mieux l'utilisateur. Dans d'autres langages, comme notamment Generic C# et Java Generics, les extensions aux classes génériques de C# et Java, il est prévu des mots-clé pour spécifier directement dans le code les concepts que doivent respecter les paramètres des patrons.

 
Opérateurs logiques
 

Les opérateurs logiques <, >, <=, >=, == et != sont souvent surchargés pour permettre la comparaison de deux objets d'une classe donnée. Mais c'est un peu fastidieux d'écrire les 6 opérateurs à chaque fois. En y réfléchissant, avec seulement == et <, on peut construire tous les autres.

template <class T>
inline bool operator > (const T & a,const T & b)
{ return (b<a); }

template <class T>
inline bool operator <=(const T & a,const T & b)
{ return (!(b<a)); }

template <class T>
inline bool operator >=(const T & a,const T & b)
{ return (!(a<b)); }

template <class T>
inline bool operator !=(const T & a,const T & b)
{ return (!(a==b)); }

Pour la classe Chaine par exemple, il suffit d'implémenter == et <, et automatiquement on dispose des 6 opérateurs logiques. Notez qu'une majorité de compilateurs, notamment la famille des GCC, fournit maintenant les quatre opérateurs >, <=, >= et != automatiquement.

 
PATRON DE CLASSE, CLASSE GENERIQUE
 

De manière identique aux fonctions, il est possible de définir des patrons de classe, qu'on nomme communément classes génériques. Nous avons choisi d'illustrer ce concept avec l'exemple d'un vecteur d'éléments où le paramètre du template est justement le type des éléments. Voici l'interface de la classe générique (notez les similitudes avec la classe Chaine, qui est très proche de la classe Vecteur<char>).

template <typename T> class Vecteur {
 protected:
  T * _t; // Tableau d'éléments.
  int _n; // Nombre d'éléments.

 public:
  Vecteur(int = 10);
  Vecteur(const Vecteur &);

  Vecteur & operator=(const Vecteur &);
  const T & operator[](int) const;
  T &       operator[](int);
};

Supposons maintenant qu'on souhaite définir l'une des méthodes à l'extérieur de la définition du patron, par exemple l'opérateur =.

template <typename T>
Vecteur<T> & Vecteur<T>::operator=(const Vecteur & v) {
 int i = 0;

 if (this!=&v) {
  if (_t!=0) delete [] _t;
  _n=v._n;
  _t=new T[_n];
  while (i<_n) _t[i]=v[i++];
 }

 return *this;
}

Il faut donc préciser à nouveau le paramétrage. En outre, vous remarquerez qu'avant les :: qui indiquent l'entrée dans la classe Vecteur<T>, l'utilisation du patron Vecteur doit être précisée avec le ou les paramètres pour l'instancier, c'est-à-dire Vecteur<T>. Une fois à l'intérieur du patron (i.e. une fois passés les ::), l'instanciation du patron en Vecteur<T> est implicite. Notez également que le nom des constructeurs et du destructeur n'est jamais suivi de <T> (qu'ils soient définis à l'intérieur ou à l'extérieur du patron), il s'agit toujours de Vecteur(...) et ~Vecteur(void), et non de Vecteur<T>(...) et ~Vecteur<T>(...).

 
Tout dans le header
 

Nous l'avons déjà dit, la définition des patrons doit se trouver intégralement dans les headers, ce qui peut soulever quelques difficultés. Voici l'exemple plus complet du patron Vecteur auquel on ajoute un patron Iterateur. Un itérateur est un pointeur au sens objet qui référence un élément d'un conteneur (cf. les designs patterns du GoF). Voici tout d'abord le fichier vecteur.hpp.

#ifndef _VECTEUR_H_
#define _VECTEUR_H_

 #include "iterateur.hpp"

 template <typename T> class Iterateur;

 template <typename T> class Vecteur {
  protected:
   T * _t; // Tableau d'éléments.
   int _n; // Nombre d'éléments.

  public:
   Vecteur(int = 10);
   Vecteur(const Vecteur &);

   Vecteur & operator=(const Vecteur &);
   const T & operator[](int) const;
   T &       operator[](int);

   Iterateur<T> begin() const;
   Iterateur<T> end() const;
 };

#endif

Comme tout header qui se respecte, il possède un gardien. Son rôle est primordial ici puisque l'inclusion entre les fichiers iterateur.hpp et vecteur.hpp est cyclique (chacun inclut l'autre). Les gardiens permettent de casser cette boucle après l'inclusion de chaque fichier. Ainsi, si dans un programme on inclut vecteur.hpp, alors le préprocesseur inclura d'abord iterateur.hpp (le gardien de vecteur.hpp a été activé à sa première inclusion), puis il continuera vecteur.hpp. A l'opposé, si on inclut iterateur.hpp dans un programme, alors le préprocesseur inclura d'abord vecteur.hpp avant de poursuivre iterateur.hpp.

Dans la seconde situation, cela signifie que le patron Vecteur sera défini avant le patron Iterateur, ce qui pose un problème puisque la définition de Vecteur fait appel au patron Iterateur. Pour éviter tout problème, une pré-déclaration de Iterateur doit être effectuée (cf. la ligne juste après le include), cette déclaration est dite avancée (ou forward declaration en anglais). Elle permet de manipuler Iterateur avant sa véritable déclaration (cette notion est similaire à celle de prototype d'une fonction).

La déclaration avancée peut également être utilisée pour une classe simple, mais dans ce cas, seule la manipulation d'un pointeur ou d'une référence de cette classe est autorisée et il est impossible d'appeler une méthode à partir d'une déclaration avancée (le compilateur ne peut pas vérifier et donc encore moins compiler). Cela implique de déporter la définition des méthodes qui manipulent la classe en déclaration avancée dans le fichier source .cpp associé et d'inclure alors tous les headers nécessaires pour compléter la déclaration avancée.

Mais pour les patrons, ce problème ne se pose pas, puisqu'il ne s'agit que de modèles, l'instanciation et la compilation se faisant plus tard. A l'image de vecteur.hpp, voici le fichier iterateur.hpp.

#ifndef _ITERATEUR_H_
#define _ITERATEUR_H_

 #include "vecteur.hpp"

 template <typename T> class Iterateur;
 template <typename T> class Vecteur;

 template <typename T>
 bool operator==(const Iterateur<T> &,const Iterateur<T> &);

 template <typename T> class Iterateur {
  protected:
   T * _p; // Pointeur sur un élément du vecteur.

   Iterateur(T * p) : _p(p) {}

  public:
   Iterateur(void) : _p(0) {}

   T &         operator*(void) const { return *_p; }
   Iterateur & operator++(void);
   Iterateur   operator++(int);

  friend class Vecteur<T>;

  friend bool operator==<T>(const Iterateur<T> &,
                            const Iterateur<T> &);
 };

#endif

Grâce aux méthodes begin et end, un vecteur peut créer des itérateurs pointant sur le début ou la fin de son tableau. Pour éviter une mauvaise initialisation des itérateurs, la construction d'un itérateur à partir d'un pointeur est protégée. La classe Iterateur<T> déclare alors la classe Vecteur<T> amie (attention, je n'ai pas écrit "le patron Iterateur déclare alors le patron Vecteur ami"). Ainsi, seul un vecteur peut créer des itérateurs initialisés à partir d'un pointeur.

Le patron Iterateur défini ici est inspiré des itérateurs de la STL: les opérateurs ++ déplacent l'itérateur et l'opérateur * renvoie l'élément pointé. Une dernière remarque, il est utile de pouvoir comparer deux itérateurs, l'opérateur == doit donc être surchargé. Il s'agit là aussi d'un patron. Celui-ci a besoin de comparer les attributs de deux itérateurs, c'est pourquoi il doit être ami de la classe Iterateur.

Remarquez que l'amitié est donnée ici à l'instance operator==<T> uniquement et non pas au patron. Tous les compilateurs ne comprennent pas la syntaxe utilisée ici pour déclarer l'amitié avec l'instance d'un patron. Ils utilisent alors une syntaxe plus simple qui indique simplement que la fonction amie est en fait une instance d'un patron, mais sans préciser de quelle instance il s'agit. Ils supposent que la signature de la fonction suffira à déterminer de quelle instance il s'agit. Voici comment serait alors définie l'amitié de la classe Iterateur<T> pour l'opérateur ==.

friend bool operator==<>(const Iterateur<T> &,
                         const Iterateur<T> &);

La première écriture semble plus logique, dans la mesure où certaines instanciations ne peuvent pas être déduites totalement à partir de la signature d'une méthode (e.g. lorsque le type de retour de la fonction est un paramètre du patron). Néanmoins, par expérience, il resort que la seconde syntaxe est à l'heure actuelle celle qui est la plus tolérée.

Pour conclure, voici un petit exemple qui remplit un vecteur d'entiers avec des nombres pairs en utilisant les patrons définis précédemment.

Vecteur<int> v(10);
Iterateur<int> courant = v.begin();
Iterateur<int> fin = v.end();
int i = 1;

while (courant!=fin) {
 i*=2;
 *(courant++)=i;
}

 
ATTRIBUT STATIQUE
 

A l'image des variables globales, les attributs statiques doivent être initialisés dans un fichier source .cpp pour éviter tout conflit lors des inclusions. Mais, concernant les classes génériques, toute leur définition doit se trouver dans un header, même l'initialisation des attributs statiques. Prenons l'exemple suivant.

template <typename VEHICULE> class Usine {
 protected:
  static int _nb_instances;
  ...

 public:
  Usine(void) { ++_nb_instances; }
  ~Usine(void) { --_nb_instances; }

 ...
};

template <typename VEHICULE> int Usine<T>::_nb_instances = 0;

L'attribut statique sert ici à compter le nombre d'instances de chaque classe Usine<...> (remarque: il y a bien un compteur par instanciation du patron Usine). Pour l'initialiser, c'est la même approche que pour une classe, seulement on est dans le header et il faut préciser le paramétrage du patron.

 
PARAMETRE PAR DEFAUT
 

Il est possible de préciser, comme pour les arguments d'une fonction, un type par défaut pour le paramètre d'un patron. Prenons l'exemple d'un arbre binaire de recherche dans lequel on stocke des éléments en leur associant à une clé, et supposons une classe générique avec comme paramètres le type des éléments stockés et le type des clés.

template <typename ELEMENT,typename CLE> class Arbre;

Mais nous remarquons que la plupart du temps, les utilisateurs utilisent le type int pour les clés. Il est alors possible de spécifier ce type comme défaut pour le paramètre CLE. Voici la syntaxe.

template <typename ELEMENT,typename CLE = int> class Arbre;

Les deux lignes suivantes sont alors équivalentes. Il faut simplement remarquer, à l'image des arguments d'une méthode, que les paramètres ne peuvent être omis qu'en partant de la fin.

Arbre<Chaine,int> a;
Arbre<Chaine> b;

Il est également possible de fournir, non pas un type, mais une constante à un patron. Supposons la classe générique Vecteur, pour laquelle nous souhaitons regrouper dans une même classe tous les vecteurs de même taille. Il est alors possible de définir la taille d'un vecteur comme étant un paramètre du patron. Par conséquent, la taille d'un vecteur devient statique.

template <typename T,int N = 10> class Vecteur {
 protected:
  T t[N];

 ...
};

Les vecteurs déclarés ci-dessous appartiennent à des classes différentes.

Vecteur<int,10> v1;
Vecteur<int,8> v2;

Considérons maintenant un vecteur creux, c'est-à-dire qu'au lieu de stocker dans un tableau tous les éléments d'un vecteur, on considère qu'une certaine valeur est le fond (i.e. la valeur par défaut) et on ne stocke que les valeurs différentes du fond, en mémorisant la valeur même et sa position dans le vecteur. Imaginons maintenant qu'on souhaite écrire un patron dont la valeur de fond est un paramètre. Voici la déclaration de la classe générique.

template <typename T,T FOND> class VecteurCreux;

FOND est de type T et représente la valeur de fond. Attention, seules les constantes d'un certain type sont autorisées. Il s'agit normalement des types primitifs, i.e. les entiers, les flottants et les pointeurs. Cependant, il semblerait que l'usage ait été limité seulement aux types entiers, utiliser des flottants étant devenu obsolète (cf. GCC récent). En ce qui concerne les pointeurs, tous ne conviennent pas, il faut que le compilateur puisse s'assurer que le pointeur est global et constant, c'est-à-dire qu'il sera valide tout au long du programme (car l'instanciation du patron est globale). Tout ceci rend l'utilisation de paramètres constants dans un patron assez rare.

 
INSTANCIATION PARTIELLE
 
 
Spécialisation d'un template
 

Un patron de composant permet de définir une classe ou une fonction générique, c'est-à-dire qui reste la même (à un certain niveau d'abstraction naturellement) quels que soient ses paramètres. Cependant, il peut arriver que, pour un type précis, le modèle général du patron ne convienne pas. Il est alors possible de définir une classe ou une fonction différente pour ce type donné. Prenons l'exemple suivant.

template <typename T1,typename T2> class Paire {
 protected:
  T1 _val1;
  T2 _val2;

 public:
  paire(void);

  void afficher(std::ostream & o)
  { o << _val1.toString() << "," << _val2.toString(); }
};

Les concepts de T1 et T2 doivent implémenter une méthode toString qui transforme un objet T1 ou T2 en chaîne de caractères. Pour les types primitifs, cette méthode n'est pas disponible, et il est impossible de la rajouter. Il faut donc réécrire la classe Paire tout spécialement pour int par exemple, de la manière suivante.

template <> class Paire<int,int> {
 protected:
  int _val1;
  int _val2;

 public:
  paire(void);

  void afficher(std::ostream & o)
  { o << _val1 << "," << _val2; }
};

On utilise ici le mécanisme de l'instanciation partielle, sauf que dans cet exemple, tous les paramètres sont instanciés (il s'agit donc d'une véritable instanciation). Le code précédent décrit donc une classe simple (Paire<int,int>) et non plus une classe générique (Paire<T1,T2>). Avec cette spécialisation du template, notre problème n'est pas résolu, puisqu'il répond seulement au cas où T1 et T2 sont de type int. Mais que se passe-t-il quand T1 est entier et pas T2 (ou inversement) ? Il faut donc là aussi spécialiser le patron.

template <typename T2> class Paire<int,T2> {
 protected:
  int _val1;
  T2 _val2;

 public:
  paire(void);

  void afficher(std::ostream & o)
  { o << _val1 << "," << _val2.toString(); }
};

Cette fois-ci, il s'agit d'une instanciation partielle de Paire<T1,T2> en posant T1 = int. Contrairement à l'instanciation Paire<int,int>, le code ci-dessus est encore un patron de classe, puisqu'il reste un paramètre (T2). Enfin, pour résoudre totalement notre problème, il faudrait aussi écrire l'instanciation partielle Paire<T1,int>.

template <typename T1> class Paire<T1,int> {
 protected:
  T1 _val1;
  int _val2;

 public:
  paire(void);

  void afficher(std::ostream & o)
  { o << _val1.toString() << "," << _val2; }
};

Notez que les trois instanciations sont nécessaires pour considérer tous les cas. Dans l'exemple suivant, chaque cas utilise l'une des instanciations sans qu'il n'y ait la moindre d'ambiguïté. On aurait pu en effet penser que les instanciations Paire<T1,int> et Paire<int,T2> entrent en conflit avec Paire<int,int> lorsque leur dernier paramètre est instancié par int, ce qui n'est pas le cas.

Paire<int,int>    p1;
Paire<int,Chaine> p2;
Paire<Chaine,int> p3;

 
Template récursif
 

L'instanciation partielle peut également être utilisée pour définir une récursivité statique, comme le montre l'exemple suivant.

template <int N> class Factorielle {
 public: enum { valeur = N*Factorielle<N-1>::valeur };
};

template <> class Factorielle<1> {
 public: enum { valeur = 1 };
};

Comme pour toute récursivité, il faut s'assurer de son arrêt. C'est le rôle ici de l'instanciation partielle Factorielle<1> qui stoppe les appels récursifs. Si l'on instancie Factorielle<5> par exemple, alors Factorielle<4>, Factorielle<3>, Factorielle<2> et Factorielle<1> sont également instanciées.

Comme tout se passe à la compilation, l'intérêt d'une telle récursivité est d'éviter le calcul à l'exécution. Mais on s'aperçoit qu'une classe Factorielle sera créée pour chaque nombre qu'on lui fournira en paramètre. L'autre défaut de cette approche est que l'appel doit être déterminé à la compilation. Ainsi, la classe Factorielle<5> est valide, alors que Factorielle<n>n est une variable n'a pas de sens en C++.

Nous avons choisi d'écrire la récursivité de la fonction factorielle par une classe générique, mais il est également possible de la définir comme un patron de fonction. Néanmoins, tous les défauts énumérés précédemment persistent.

template <int N> int factorielle(void)
{ return N*factorielle<N-1>(); }

template <> int factorielle<1>(void) { return 1; }

Dans le cas d'une récursivité dynamique, la profondeur, i.e. le nombre d'appels empilés, est limité par la taille de la mémoire centrale, et en particulier par celle de la pile où le programme s'exécute. Pour une récursivité statique, la limitation est effectuée par le compilateur qui n'autorise par défaut qu'une profondeur très faible (e.g. 16 appels). Très rapidement, il peut donc être nécessaire d'augmenter ce seuil. Avec un compilateur de la famille des GCC, l'option -ftemplate-depth-x permet de monter la limite à x appels. Notez que ce seuil ne limite pas simplement la récursivité statique, mais également l'utilisation d'un template pour en définir un autre. Ce qui signifie qu'un usage intensif des templates, même sans récursivité, peut conduire le programmeur à augmenter cette limite.

 
PATRON DE METHODE
 

Pour conclure ce chapitre, voici un dernier petit détail sur les patrons: il est possible de définir un patron de méthode. Cela signifie qu'une classe peut avoir un nombre indéterminé de méthodes au moment de son écriture, puisqu'à chaque nouvelle instanciation du patron, une méthode sera ajoutée à la classe. Néanmoins, comme toujours avec les templates en C++, tout se passe à la compilation, cela signifie qu'au moment de l'exécution, l'interface de la classe est figée. Pour illustrer, voici l'exemple d'une classe qui encapsule simplement un entier.

class Entier {
 protected:
  long _val;

 public:
  template <typename T> Entier(const T & v) : _val(long(v)) {}

 ...
};

Le constructeur ici est un patron de méthode. L'idée est de pouvoir fournir un constructeur d'objets Entier pour toute classe ou type primitif qui autorise la conversion vers un long. Telle quelle, la classe est compilée sans constructeur. Ce n'est qu'au moment où l'on tente de construire des objets Entier que des constructeurs sont instanciés. Par exemple, le code Entier('c') implique l'instanciation, et donc la compilation, du constructeur Entier(const char &).

Il reste un détail de syntaxe à préciser. Supposons que le type de l'attribut _val soit un paramètre de la classe Entier.

template <typename E> class Entier {
 protected:
  E _val;

 public:
  template <typename T> Entier(const T &);

 ...
};

On peut se demander alors comment écrire le constructeur à l'extérieur du patron. Voici la syntaxe, mais attention, tous les compilateurs ne la supporte pas (notamment Visual C++ 7 qui impose une définition à l'intérieur du patron).

template <typename E> template <typename T>
Entier<E>::Entier(const T & v) : _val(T(v)) {}

Néanmoins, cette syntaxe semble être le standard qui devrait apparaître prochainement dans tous les compilateurs (cf. la famille des GCC).

 
 
Copyright (c) 1999-2016 - Bruno Bachelet - bruno@nawouak.net - http://www.nawouak.net
La permission est accordée de copier, distribuer et/ou modifier ce document sous les termes de la licence GNU Free Documentation License, Version 1.1 ou toute version ultérieure publiée par la fondation Free Software Foundation. Voir cette licence pour plus de détails (http://www.gnu.org).