 |
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.
| |
| 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é.
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.
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.
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>(...).
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;
}
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.
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.
| |
| 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;
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> où
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.
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). |
|
|