Choisissez votre style : colorisé, impression

Exercice 0 : reprise de l'exemple du cours (templates, niveau 0)

Exemple traité à la page 168 de l'ouvrage C++ par la pratique.

Le but de cet exercice est de reprendre les exemples du cours sur la programmation générique (« templates »).

[Essayez de le faire par vous même chaque étape avant de regarder la solution qui suit]

Modèle de fonction

Dans un fichier templates.cc, commençons par définir un modèle de fonctions permettant d'échanger la valeur de ses deux arguments (de même type).

Testez votre modèle dans le main en déclarant deux entiers et en échangeant leurs valeurs. Faites de même avec deux double, puis deux autres variables d'un type de votre choix.

Solution :

Le modèle que nous voulons définir nécessite un seul type abstrait. On commence donc par écrire
template<typename type>
puis on peut déclarer et définir la fonction juste derrière :
template<typename type>
void echange(type& var1, type& var2) {
  type tmp(var1);
  var1 = var2;
  var2 = tmp;
}

Pour le main il suffit de faire quelque chose comme :

int main()
{
  int a(2), b(4);
  echange(a,b);
  cout << a << " " << b << endl;

  double da(2.3), db(4.5);
  echange(da,db);
  cout << da << " " << db << endl;

  string sa("ca marche"), sb("coucou");
  echange(sa, sb);
  cout << sa << " " << sb << endl;
}

C'est ce que l'on appelle des instances implicites du modèle de classes puisque c'est le compilateur qui décide quelle instance du modèle choisir à chaque fois.

Instanciation explicite (locale)

Voyons maintenant comment expliciter les instances quand cela est nécessaire.

Définissez un modèle de fonctions utilisant deux variable du même type abstrait et retournant le plus grand des deux éléments (en supposant que cela est un sens, c'est-à-dire que l'opérateur < est définit)

[Note: attention la fonction max existe déjà dans le système. Utilisez un autre nom, par exemple monmax]

Dans la fonction main définissez un entier et un double et utilisez le modèle précédemment défini pour retourner le maximum des deux.

Peut-on se contenter de faire une instanciation implicite ?

Solution :

Le modèle demandé se définit aussi simplement que le premier modèle :

template<typename type>
type monmax(const type& x, const type& y) {
  if (x < y) return y;
  else       return x;
}

L'instantiation implicite dans le main se ferait par exemple comme cela :

cout << monmax(3.14, 7) << endl;

mais cela ne fonctionne pas car le compilateur ne peut pas décider entre
int monmax<int>(const int&, const int&);
et
double monmax<double>(const double&, const double&);
et il n'existe pas de
monmax(const double&, const int&)

On est donc obligé de faire une instanciation explicite locale de la fonction choisie :

cout << monmax<double>(3.14, 7) << endl;

Spécialisation

Je voudrais maintenant illustrer la notion de spécialisation de modèle de fonctions, très utilise pour différencier un cas particulier du modèle du cas général.

Définissez un modèle de classes prenant un argument de type pointeur sur un type abstrait et affichant à l'écran la valeur pointée avec un petit message simple, en supposant que l'opérateur << existe pour le type abstrait.

Spécialisez ensuite l'instance particulière du modèle pour le cas où le type abstrait est int en changeant par exemple le message associé.

Testez dans le main (2 instanciations implicites, une pour double par exemple et une pour int).

Solution :

Le modèle général se définit par exemple comme suit :

template<typename Type>
void affiche(Type* t) {
   cout << "J'affiche " << *t << endl;
}

Pour spécialiser (totalement) le modèle, il suffit de remplacer le type abstrait par un type concret, ici int, mais sans oublier d'indiquer au début que l'on travaille bien avec un modèle de fonctions par le mot clé template. Il faut également donner le nom complet de la fonction, c'est-à-dire avec son type concret entre < et >, ici <int> :

template<>
void affiche<int>(int* t) {
  cout << "J'affiche le contenu d'un entier : " << *t << endl;
}

On peut tester la spécialisation avec le bout de code suivant dans le main :

double da(3.3);

affiche(&da);

int a(4);

affiche(&a);

Modèle de classe

Nous allons finir en appliquant tout ce que nous venons de voir à un modèle de classes.

Définissez un modèle de classes réalisant une paire de 2 objets (définis par deux types abstraits différents).

La classe devra comprendre un constructeur et un destructeur virtuel, une méthode get et set pour chacun des 2 éléments de la paire.

Je vous demande de plus de définir le constructeur à l'extérieur de la classe.

Solution :

La définition de la classe se fait exactement comme une classe usuelle, sauf que les types ont ici des noms abstraits (T1 et T2 dans la solution ci-dessous) qui sont introduit par le mot clé template avant la définition de la classe :

template<typename T1, typename T2>
class Paire {
public:
  Paire(const T1&, const T2&);
  virtual ~Paire(){}
  T1 get1() const { return premier; }
  T2 get2() const { return second;  }
  void set1(const T1& val) { premier = val; }
  void set2(const T2& val) { second  = val; }
protected:
  T1 premier;
  T2 second;
};

Pour la définition externe du constructeur, il est important, non seulement de remettre la définition du modèle (template <typename T1, typename T2>), mais aussi d'indiquer dans le nom de la classe que c'est bien la classe qui est sujette au modèle et non la méthode (constructeur ici).
Cela se fait en indiquant Paire<T1,T2> comme nom de classe :

template<typename T1, typename T2>
Paire<T1,T2>::Paire(const T1& un, const T2& deux)
  : premier(un), second(deux)
{}

Terminons en spécialisant la classe pour le cas de paires string-int en ajoutant dans ce cas particulier une méthode permettant d'ajouter une valeur entière au second élément de la paire.

Solution :

La spécialisation se fait exactement comme dans le cas des modèles de fonctions présenté plus haut : il faut indiquer tous les types explicitement (spécialisation totale) et faire précéder le tout de template<> :

template<> class Paire<string,int> {
public:
  Paire(const string& un, int deux) : premier(un), second(deux) {}
  virtual ~Paire(){}
  string get1() const { return premier; }
  int get2()    const { return second;  }
  void set1(const string& val) { premier = val; }
  void set2(int val) { second  = val; }
  void add(int); // une methode de plus
protected:
  string premier;
  int    second;
};

void Paire<string,int>::add(int i) {
  second += i;
}

Vous pouvez trouver ici le code complet de l'exemple.


Dernière mise à jour : $Date: 2022/04/29 16:54:14 $