Les modèles

Nous sommes maintenant capables d'exprimer plusieurs types de relations entre objets:

Nous allons voir dans ce chapitre comment exprimer la relation: Est une liste de, ou Est un tableau de, etc.; nous avons défini au début du cours une classe appelée complexe, qui comprend essentiellement deux membres privés, r et i. Or, ces deux nombres étaient définis comme des float, ce qui peut paraître un peu restrictif... Ne faudrait-il pas plutôt définir les parties réelle et imaginaire des complexes comme des double, pour avoir une meilleure précision lors des calculs numériques ? Nous ne voulons pas avoir à choisir, en tous cas pas au moment d'écrire l'objet complexe: les algorithmes utilisés seront les mêmes, quelque soit le type des champs utilisés pour la partie réelle et pour la partie imaginaire. D'où la notion de modèles.

Les modèles sont très utilisés pour créer des objets de type conteneurs: ce sont des objets qui vont en "contenir" d'autres. Nous en reparlerons dans le chapitre sur la bibliothèque standard apres

Les modèles de fonctions

La fonction min ci-dessous renvoie simplement le plus petit de deux nombres... ou de deux objets:

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

La fonction peut être utilisée avec n'importe quel objet (ou presque):

int i=4,j=6;
int m = min(i,j);

float x=3.14, y=5.65;
float z= min(x,y);

string s1="houlala", s2="aieaieaie";
string s = min(s1, s2);

complexe c1=0, c2=1;
complexe c = min(c1,c2);   // NON !!! Ne passe pas à la compilation !

Dans le code ci-dessus, quatre versions de la même fonction ont été compilées. On dit qu'elles ont été "instantiées". Le compilateur a choisi la fonction à instantier à partir du type des arguments. Cependant, la quatrième a abouti à une erreur de compilation, car l'opérateur < n'est pas défini pour la classe complexe.

Le code suivant aboutit lui aussi à une erreur de compilation:

int i=4;
float y=3.14;
cout << min(i,y) << endl;

En effet, le compilateur n'a aucun moyen de savoir quelle fonction template doit être utilisée, il se retrouve devant une ambiguïté. Le programmeur doit dans ce cas soit forcer une conversion, soit imposer le type d'instantiation. Cela s'écrit de la manière suivante:

int i=4;
float y=3.14;
cout << min<float>(i,y) << endl;

Classes paramétrées

Définition d'une classe paramétrée

Voici la nouvelle définition de la classe complexe, en utilisant des modèles:

template<typename NUM=float> class complexe {
public:
complexe(NUM x=0, NUM y=0);
complexe(const complexe<NUM> & );
~complexe();
operator NUM();
complexe<NUM> & operator=(const complexe<NUM> &);
complexe<NUM> & operator+= (const complexe<NUM> &);
NUM get_r() const { return r;};
NUM get_i() const { return i;};
void set_r(NUM x) { r=x; m_flg=false;};
void set_i(NUM x) { i=x; m_flg=false;};
NUM get_m() const;
static void set_debug() { debflg=true;};
static void clr_debug() { debflg=false;};

private:
NUM r;
NUM i;
mutable bool m_flg;
mutable NUM m;
static bool debflg;
void _calc_module() const {m=sqrt(r*r+i*i);};
};

Le nom de classe est précédé par le mot-clé template<typename NUM> (que l'on traduit par modèle,ou patron), ce qui signifie qu'un certain type sera utilisé lors de l'instantiation de cette classe. D'autre part, dès que le nom complexe est utilisé pour désigner la classe qui est en train d'être définie, on doit citer le paramètre, d'où la notation un peu lourde complexe<NUM> partout dans la définition des paramètres.

ATTENTION L'expression typename NUM, peut aussi s'écrire class NUM. Dans ce contexte, class signifie en fait "n'importe quel type"; La notation typename, plus récente, est donc bien meilleure et doit être utilisée dans les nouveaux codes.

ATTENTIONLe type est complexe<T>, on fera référence à ce type complet lorsqu'on annoncera par exemple un type de retour de fonction. Par contre, le constructeur a pour nom complexe, le destructeur ~complexe.

ATTENTIONDéfinir une fonction en-dehors de la portée de la déclaration de classe revient à définir un modèle de fonction:

template <typename NUM> complexe<NUM>::~complexe()

Ce qui signifie:

Paramètres par défaut

Dans le code ci-dessus, on remarque la structure typename NUM=float, qui revient à donner à NUM une valeur par défaut (float en l'occurrence).

Instantiation

Le programme principal ressemblera à ce qui suit:

typedef complexe<> complexe_float;
typedef complexe<double> complexe_double;

main() {
 complexe_float F;
 complexe_double D;
}

Le typedef ne fait rien d'autre que de déclarer un synonyme. Autrement dit, il ne crée pas un nouveau type. Il n'est pas indispensable, mais simplement très souhaitable pour la lisibilité du code: en effet, la syntaxe complexe<float> se révèle difficilement lisible.

Dans le cas des flottants, il suffit de déclarer complexe<>, le symbole <> étant là pour rappeler qu'il s'agit bien d'un modèle.

Paramètres utilisables

On peut mettre plusieurs paramètres dans les modèles. Ces paramètres peuvent être de deux sortes:

Exemple: La classe tableau

La classe tableau avant définie ci-dessus peut être réécrite en utilisant un modèle:

template<size_t TAILLE> class tableau {
    private:
       int buffer[TAILLE];
       void copie(const tableau<TAILLE> &);
    
    public:
        tableau(){};
        tableau(const tableau &);
        tableau<TAILLE> & operator=(const tableau<TAILLE> &);
        ~tableau() {};
    };
    
    template<size_t TAILLE> tableau<TAILLE>::tableau(const tableau & b ) {
       copie(b);
       cout << "copie effectuee par constructeur de copie\n";
    };
    
    template<size_t TAILLE> void tableau<TAILLE>::copie(const tableau<TAILLE> & b) {
       memcpy(buffer,b.buffer,TAILLE);
    };

Cette implémentation est intéressante, parce qu'on n'a plus besoin d'utiliser l'opérateur new dans le constructeur. Du coup, on n'a plus besoin non plus du "trio infernal" avant Par contre, on ne peut plus choisir la taille du tableau lors de l'exécution du code, celle-ci doit être fixée à la compilation. Il résulte de tout ça que l'implémentation est finalement bien plus simple:

template<size_t TAILLE> class tableau {
    private:
       int buffer[TAILLE];
    };

Si A et B sont des objets de type tableau, on pourra écrire:A=B, le compilateur saura générer automatiquement l'opérateur =. Bien sûr, dans une vraie implémentation on définira également les opérateurs[].

Spécialisation

Il n'est pas toujours possible de s'en tenir à l'écriture du code général, tel que le modèle l'implémente: ne serait-ce que pour des raisons de performance, il est parfois nécessaire de réécrire le code pour certains types particuliers. Par exemple, une pile d'objets peut être implémenté sous forme de modèle mais on conçoit qu'une pile de booléens peut être écrite différemment, en utilisant le fait que des booléens ne tiennent pas sur un ou plusieurs octets, mais sur un bit. On écrira cette spécialisation de la manière suivante:

template<> class pile<bool> {
}
Le nom de type correspondant à la spécialisation se trouve après le nom du template.

Spécialisation partielle

On peut aussi spécialiser partiellement les modèles. Par exemple, la classe générale:

template<typename T1, typename T2> class Machin {
...
}

peut être spécialisée de plusieurs manières:

// Les deux paramètres ont le même type
template<typename T> class Machin<T,T> {
...
}

// Le second type est double
template<typename T1> class Machin<T1,double> {
...
}

// On travaille avec des pointeurs
template<typename T1, typename T2> class Machin<T1 *, T2 *> {
...
}

Quelques conseils...

Les modèles permettent de définir des objets de manière extrêmement générale, en ce sens ils constituent un outil très puissant. Mais, chaque médaille ayant son revers, ils sont d'une utilisation assez délicate. Il semble d'ailleurs que, encore actuellement, tous les compilateurs n'implémentent pas les modèles de manière complètement identique, ce qui n'est pas pour faciliter les choses...

Il est important de connaître la syntaxe des modèles, car elle est employée en permanence dans la bibliothèque standard: cela est tout-à-fait compréhensible, dans la mesure où une bonne partie de la bibliothèque standard est constituée de "conteneurs" et d'algorithmes associées, c'est-à-dire d'objets encapsulant des structures de données: seuls les modèles permettent de les décrire de manière générale

Quelques conseils, pour ne pas se noyer dans les modèles:


xhtml Licence Creative Commons Emmanuel Courcelle <emmanuel.courcelle@inp-toulouse.fr>