Liste des programmes :
Liste des tables :
Combien de fois avez vous réécrit le même algorithme en ne changeant qu'un type de donnée de base ou une fonction de test / comparaison ?
Les fonctions / classes patrons ou template permettent de répondre à certains besoins de réutilisabilité avec un paradigme orthogonal à ceux du modèle objet qu'il complète agréablement.
Afin de bien saisir le concept des fonctions template nous allons regarder un exemple classique : celui de la fonction max.
Il est souvent nécessaire de récupérer le maximum de deux quantités. Hors, le type de ces quantités ne cesse de varier, int, double, chaînes de caractères, objets, etc.
Quelles sont alors les possibilités qui vous sont offertes ?
Profitant du fait que le C++ vous autorise à surcharger les fonctions (utiliser le même nom avec des signatures d'arguments différentes), vous sautez sur l'aubaine et écrivez autant de versions de la fonction max que nécessaire.
Par exemple, pour les types suivants :
Surcharge pour les int | Surcharge pour les double |
---|---|
int max(int g, int d) { return ((g > d) ? g : d); } |
double max(double g, double d) { return ((g > d) ? g : d); } |
Surcharge pour les long | |
long max(long g, long d) { return ((g > d) ? g : d); } |
Table 6.1 Surcharges multiples pour la fonction max
Autant vous le dire tout de suite : cette solution est la pire. Elle consiste à écrire max sous la forme suivante :
#define max(a, b) (((a) > (b)) ? (a) : (b))
Programme 6.1 Définition de max à l'aide d'une macro
C'est lumineux non ? personnellement, cette abondance de parenthèses, ca me donnerait plutôt la nausée. Soyons tout de même honnètes et pesons le pour et le contre.
Il est clair que ces deux techniques n'apportent pas les garanties de fiabilité et efficacité souhaitées. Les fonctions template nous apportent une réponse élégante à notre problème.
Voici le code template de déclaration d'une fonction max typique :
template <class T> T max(const T& g, const T& d) { return ((g > d) ? g : d); }
Programme 6.2 La fonction max en template
La directive template <class T> indique que la fonction est paramétrée par le type T. Ensuite, vous utilisez T comme un type normal dans la fonction.
La fonction max est ensuite instanciée automatiquement lors de ses appels, par exemple, examinons le code suivant :
int main(int, char **) { int i; int j; double a; double b; // code omis pour simplicité ;) cout << max(i,j) << endl; cout << max(a,b) << endl; return 0; }
Programme 6.3 Instanciation automatique de la fonction template max
La ligne cout << max(i,j) << endl; créée la version int max(const int &, const int&) à partir du template, alors que, vous vous en doutez cout << max(a,b) << endl; créée la version double max(const double &, const double &).
Une chose très importante : il n'y a pas de conversion automatiques entre les types, par exemple :
int main(int, char **) { int i; double b; // code omis pour simplicité ;) cout << max(i,b) << endl; return 0; }
produira une erreur alors que si l'on opérait avec une fonction « normale », i serait promu depuis int vers double. C'est un mécanisme important destiné à garantir contre certaines erreurs crasses. Du coup, si vous avez besoin d'une conversion d'ordinaire implicite, vous être priés de la faire vous même, par exemple :
Prenons un exemple intéressant. Supposons que vous vouliez utiliser la fonction max pour comparer deux chaînes de caractères. Conscients que l'instanciation du template précédent ne ferait que comparer la valeur de deux pointeurs, vous écrivez votre propre version :
const char *max(const char *l, const char *g) { if (strcmp(l, g) > 0) return l; else return g; }
Programme 6.4 Exemple de surcharge d'une fonction par ailleurs déclarée en template
Et maintenant, vous avez à la fois le template et la fonction dédiée, si vous tentez :
char chaine1[]="toto"; char chaine2[]="tata"; cout << max (chaine1, chaine2) << endl;
Quelle fonction va être utilisée ?
Je vous rassure : c'est la version dédiée. Lorsqu'il a le choix entre une version dédiée dont le prototype colle directement à l'appel et l'instanciation d'un patron, le compilateur choisit systématiquement le version dédiée. Ce dernier fait est important à connaître lorsque l'on cherche à générer, par exemple, des opérateurs relationnels manquant.
Vous en aviez sans doute l'intuition, mais à partir des opérateurs < et == ou > et ==, on peut générer toute la panoplie ! Considérez par exemple le template suivant :
template <class T> bool operator != (const T& g, const T& d) { return ! ( g == d); }
Programme 6.5 Génération automatique d'opérateurs relationnels
En l'absence de version explicite de l'opérateur != et si l'opérateur == est définir pour un type quelconque, ce template est capable de générer automatiquement l'opérateur !=. Ces template sont fournis dans le fichier utility de la librairie standard du C++.
Après avoir étudié les fonctions template, il est temps de s'attaquer aux classes paramétrées. Pour ce faire, nous allons suivre un exemple classique : celui de la classe Pile.
Nous étudierons les points suivants :
Notre but est d'écrire une classe pile générique, c'est à dire pouvant accepter tout type de données. Pour ce faire nous allons la paramétrer par un type, à l'instar de la fonction max de la section précédente. La pile sera basée sur un tableau d'éléments alloués dans le constructeur.
Les opérations fournies seront les suivantes :
#ifndef __PILE_HXX__ #define __PILE_HXX__ template <typename T> class Pile { public: Pile(int taille=128) { tab = new T[taille]; tos = tab-1; nbElements=0; } bool empty(void) { return (nbElements==0); } void push(const T& val) { ++tos; *tos=val; ++nbElements; } void pop() { --tos; --nbElements; } const T& top(void) const { return *tos; } T& top(void) { return *tos; } ~Pile(void) { delete [] tab; } private: int nbElements; T *tab; T *tos; }; #endif
Programme 6.6 Première version de la classe Pile
D'accord, la méthode pop ne détruit pas rééllement l'élément mais modifie simplement l'emplacement du pointeur de pile. La destruction des éléments se fait à la destruction de la pile.
Avez vous noté que nous faisons des hypothèses sur les méthodes fournies par la classe T d'instanciation ?
C'est une pratique courante que de demander des prérequis sur la classe paramètre. TOUTEFOIS, il est indispensable des les documenter.
Contrairement aux fonctions template, l'instanciation des classes n'est pas automatique. Vous devez la faire explicitement. L'exemple suivant créé une pile d'entiers, la remplit avec une dizaine d'éléments qui sont ensuite affichés à l'écran.
#include "Pile.hxx" #include <iostream> using namespace std; typedef Pile<int> PileInt; int main(int, char **) { PileInt pile; for (int i=0;i<10;i++) pile.push(2*i); while (!pile.empty()) { cout << pile.top() << endl; pile.pop(); } return 0; };
Programme 6.7 Instanciation de la classe Pile
Attention, il n'y a pas que des types que l'on puisse passer en paramètre à des template. On peut également passer des constantes. Par exemple, il est possible de passer en paramètre la taille du tableau avec le code suivant :
#ifndef __PILECONS_HXX__ #define __PILECONS_HXX__ template <typename T, int taille=128> class PileCons { public: Pile() { tos = tab-1; nbElements=0; } bool empty(void) { return (nbElements==0); } void push(const T& val) { ++tos; *tos=val; ++nbElements; } void pop() { --tos; --nbElements; } const T& top(void) const { return *tos; } T& top(void) { return *tos; } private: int nbElements; T tab[taille]; T *tos; }; #endif
Programme 6.8 La classe Pile avec un paramètre template constant
Les grosses différences sont les suivantes :
Hormis ceci, le reste du code est exactement le meme ! yeah !
Contrairement au code « normal » où la déclaration d'une classe se fait dans un fichier et la définition des méthodes déportées dans un autre, on doit écrire le code des méthodes à la suite de la déclaration de la classe dans le cas des template. En effet, le code template n'est qu'un moule. Aussi, lorsque vous instanciez le template, le compilateur doit avoir accès à l'intégralité du code afin de générer la version qui correspond au type passé en paramètre.
Par exemple, vous n'aurez pas manqué de noter que la classe Pile manque cruellement d'opérateur d'affectation et de constructeur de recopie. Rajoutons les ! à ce sujet nous devons rajouter une variable d'instance pour la taille du tableau et je vais utiliser le système des méthodes clonage et destruction, lesquelles vont être passées en externe.
Le code devient alors :
#ifndef __PILE_HXX__ #define __PILE_HXX__ template <typename T> class Pile { public: Pile(int taille=128) : taille_(taille), nbElements(0) { tab = new T[taille]; tos = tab-1; nbElements=0; } Pile(const Pile &unePile) { clonage(unePile); } bool empty(void) const { return (nbElements==0); } void push(const T& val) { ++tos; *tos=val; ++nbElements; } void pop() { --tos; --nbElements; } const T& top(void) const { return *tos; } T& top(void) { return *tos; } ~Pile(void) { destruction(); } Pile &operator=(const Pile &unePile) { if (this != &unePile) { destruction(); clonage(unePile); } return *this; } private: int taille_; int nbElements; T *tab; T *tos; void clonage(const Pile &); void destruction(void); }; template <class T> void Pile<T>::clonage(const Pile<T> &unePile) { taille_=unePile.taille_; nbElements=unePile.nbElements; tab = new T[taille_]; memcpy(tab, unePile.tab, sizeof(T) * nbElements); tos = tab + nbElements - 1; } template <class T> void Pile<T>::destruction(void) { delete [] tab; tab=0; tos=0; } #endif
Programme 6.9 Définition des méthodes déportées
Vous remarquez donc :
Hormis ça, rien de particulier !
Supposons que vous vouliez associer à votre classe pile un opérateur d'affichage sur flux. Hors, il n'existe pas de parcours non destructif de la pile. Il vous reste donc 2 solutions :
A rajouter dans la classe :
friend ostream &operator << <> (ostream &o, const Pile &p);
Programme 6.10 Déclaration d'une méthode friend d'une classe template
La partie la plus déroutante concerne le <> juste avant la signature d'arguments. Il signifie que tous les types correspondant au template en cours appairassant dans la signature d'arguments (Pile dans notre cas) sont à associer à un template.
Le corps de l'opérateur est :
template <class T> ostream &operator<< (ostream &o, const Pile<T> &unePile) { T* courant=unePile.tab; while (courant <= unePile.tos) { o << *courant << endl; ++courant; } return o; }
Programme 6.11 Définition d'une méthode friend d'une classe template
Vous remarquez que l'on doit respécifier template <class T> et que le paramètre de type Pile est lui aussi précisé comme étant paramétré par T (Pile<T>). Comme cet opérateur est une fonction globale, il n'a pas à être instancié explicitement et vous pouvez donc utiliser impunément cout << unePile << endl; dans votre code source !
Le code final est :
#ifndef __PILE_HXX__ #define __PILE_HXX__ #include <iostream> using namespace std; template <typename T> class Pile { public: Pile(int taille=128) : taille_(taille), nbElements(0) { tab = new T[taille]; tos = tab-1; nbElements=0; } Pile(const Pile &unePile) { clonage(unePile); } bool empty(void) const { return (nbElements==0); } void push(const T& val) { ++tos; *tos=val; ++nbElements; } void pop() { --tos; --nbElements; } const T& top(void) const { return *tos; } T& top(void) { return *tos; } ~Pile(void) { destruction(); } Pile &operator=(const Pile &unePile) { if (this != &unePile) { destruction(); clonage(unePile); } return *this; } private: int taille_; int nbElements; T *tab; T *tos; void clonage(const Pile &); void destruction(void); friend ostream &operator<< <> (ostream &o, const Pile &unePile); }; template <class T> void Pile<T>::clonage(const Pile<T> &unePile) { taille_=unePile.taille_; nbElements=unePile.nbElements; tab = new T[taille_]; memcpy(tab, unePile.tab, sizeof(T) * nbElements); tos = tab + nbElements - 1; } template <class T> void Pile<T>::destruction(void) { delete [] tab; tab=0; tos=0; } template <class T> ostream &operator<< (ostream &o, const Pile<T> &unePile) { T* courant=unePile.tab; while (courant <= unePile.tos) { o << *courant << endl; ++courant; } return o; } #endif