• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 25/03/2019

Créez des templates

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Le but de la programmation, en tout cas à l'origine, est de simplifier les tâches répétitives en les faisant s'exécuter sur votre ordinateur plutôt que devoir faire tous les calculs à la main. On veut donc s'éviter du travail à la chaîne.

Nous allons voir comment faire s'exécuter un même code pour différents types de variables ou classes. Cela nous permettra d'éviter la tâche répétitive de réécriture de portions de code semblables pour différents types. Pensez à la classevector: quel que soit le type d'objets que l'on y stocke, le tableau aura le même comportement et permettra d'ajouter et supprimer des éléments, de renvoyer sa taille, etc. Finalement, peu importe que ce soit un tableau d'entiers ou de nombres réels.

La force des templates est d'autoriser une fonction ou une classe à utiliser des types différents. Leur marque de fabrique est la présence des chevrons<et>et, vous l'aurez remarqué, la STL utilise énormément ce concept.

Les fonctions templates

Ce que l'on aimerait faire

Il arrive souvent qu'on ait besoin d'opérations mathématiques dans un programme. Une opération toute simple est celle qui consiste à trouver le plus grand de deux nombres. Dans le cas des nombres entiers, on pourrait écrire une fonction comme suit :

int maximum(int a,int b)
{
    if(a>b)
        return a;
    else
        return b;
}

Cette fonction est très bien et elle n'a pas de problème. Cependant, si un utilisateur de votre fonction aimerait utiliser desdoubleà la place desint, il risque d'avoir un problème. Il faudrait donc fournir également une version de cette fonction utilisant des nombres réels. Cela ne devrait pas vous poser de problème à ce stade du cours.

Pour être rigoureux, il faudrait également fournir une fonction de ce type pour leschar, lesunsigned int, les nombres rationnels, etc. On se rend vite compte que la tâche est très répétitive.
Cependant, il y a un point commun à toutes ces fonctions : le corps de la fonction est strictement identique. Quel que soit le type, le traitement que l'on effectue est le même. On se rend compte que l'algorithme utilisé dans la fonction est générique.

Il serait donc intéressant de pouvoir écrire une seule fois la fonction en disant au compilateur : « Cette fonction est la même pour tous les types, fais le sale boulot de recopie du code toi-même. » Eh bien, cela tombe bien parce que c'est ce que permettent les templates en C++ et c'est ce que nous allons apprendre à utiliser dans la suite.

Une première fonction template

Pour indiquer au compilateur que l'on veut faire une fonction générique, on déclare un « type variable » qui peut représenter n'importe quel autre type. On parle de type générique. Cela se fait de la manière suivante :

template<typename T>

Vous pouvez remarquer quatre choses importantes :

  1. Tout d'abord, le mot-clétemplateprévient le compilateur que la prochaine chose dont on va lui parler sera générique ;

  2. Ensuite, les symboles «<» et «>», que vous avez certainement déjà aperçus dans le chapitre sur lesvectoret sur la SL, constituent la marque de fabrique des templates ;

  3. Puis le mot-clétypenameindique au compilateur queTsera le nom que l'on va utiliser pour notre « type spécial » qui remplace n'importe quoi ;

  4. Enfin, il n'y a PAS de point-virgule à la fin de la ligne.

La ligne de code précédente indique au compilateur que dans la suite,Tsera un type générique pouvant représenter n'importe quel autre type. On pourra donc utiliser ceTdans notre fonction comme type pour les arguments et pour le type de retour.

template <typename T>
T maximum(const T& a, const T& b)
{
   if(a>b)
      return a;
   else
      return b;
}

Quand il voit cela, le compilateur génère automatiquement une série de fonctionsmaximum()pour tous les types dont vous avez besoin. Cela veut dire que si vous avez besoin de cette fonction pour des entiers, le compilateur crée la fonction :

int maximum(const int& a,const int& b)
{
   if(a>b)
      return a;
   else
      return b;
}

… et de même pour lesdouble,char, etc. C'est le compilateur qui se farcit le travail de recopie ! Parfait, on peut aller faire la sieste pendant ce temps. :-)

On peut écrire un petit programme de test :

#include <iostream>
using namespace std;

template <typename T>
T maximum(const T& a,const T& b)
{
   if(a>b)
      return a;
   else
      return b;
}

int main()
{
     double pi(3.14);
     double e(2.71);

     cout << maximum<double>(pi,e) << endl; //Utilise la "version double"de la fonction

     int cave(-1);
     int dernierEtage(12);

     cout << maximum<int>(cave,dernierEtage) << endl; //Utilise la "version int" de la fonction

     unsigned int a(43);
     unsigned int b(87);

     cout << maximum<unsigned int>(a,b) << endl; //Utilise la "version unsigned int" de la fonction.

     return 0;
}

Et tout cela se passe sans que l'on ait besoin d'écrire plus de code. Il faut juste indiquer entre des chevrons quelle « version » de la fonction on souhaite utiliser, comme pour lesvectoren somme : on devait indiquer quelle « version » du tableau on souhaitait utiliser.

Il n'est pas toujours utile d'indiquer entre chevrons quel type on souhaite utiliser pour les fonctions templates. Le compilateur est assez intelligent pour deviner ce que vous souhaitez faire. Mais dans des cas compliqués ou s'il y a plusieurs arguments de types différents, alors il devient nécessaire de spécifier la version.

int main()
{
    double pi(3.14);
    double e(2.71);

    cout << maximum(pi,e) << endl; //Utilise la "version double" de la fonction
    return 0;
}

Le compilateur voit dans ce cas que l'on souhaite utiliser la versiondoublede la fonction.
À vous de voir si votre compilateur comprend vos intentions.

Si vous êtes attentifs, vous avez peut-être remarqué que, pour les arguments, j'ai remplacé le passage par valeur par des références constantes. En effet, on ne sait pas quel type l'utilisateur va utiliser avec notre fonctionmaximum(). La taille en mémoire de ce type sera peut-être très grande : on passe donc une référence constante pour éviter une copie coûteuse et inutile.

Où mettre la fonction ?

Habituellement, un programme est subdivisé en plusieurs fichiers que l'on classe en deux catégories : les fichiers de code (les.cpp) et les fichiers d'en-tête (les.h). Généralement, on met le prototype de la fonction dans un.het la définition dans le.cpp, comme on l'a vu tout au début ce ce cours.
Pour les fonctions templates, c'est différent. TOUT doit obligatoirement se trouver dans le fichier.h, sinon votre programme ne pourra pas compiler.

Tous les types sont-ils utilisables ?

J'ai dit plus haut que le compilateur allait générer toutes les fonctions nécessaires. Cependant, il y a quand même une contrainte ici: le type que l'on passe à la fonction doit posséder unoperator>. Par exemple, on ne peut pas utiliser cette fonction avec unPersonnageou unMagiciendes chapitres précédents : ils ne possèdent pas de surcharge de>. Tant mieux, puisque prendre le maximum de deux personnages n'a pas de sens !
Les contraintes dépendent des fonctions que vous écrivez. Si vous utilisez l'opérateur + dans la fonction, alors il faut que l'objet passé en argument surcharge cet opérateur. Si vous effectuez une copie dans la fonction, alors l'objet doit posséder un constructeur de copie etc.

Des fonctions plus compliquées

Vous aviez appris à écrire une fonction qui calcule la moyenne d'un tableau. À nouveau, les opérations à effectuer sont les mêmes quel que soit le type contenu. Écrivons donc cette fonction sous forme de template.

Voici ma version :

template<typename T>
T moyenne(T tableau[], int taille)
{ 
   T somme(0);                   //La somme des éléments du tableau
   for(int i(0); i<taille; ++i)
      somme += tableau[i];

   return somme/taille;
}

Le souci que nous avions était que pour le typeint, nous nous retrouvions avec une division entière qui posait problème (les moyennes étaient arrondies vers le bas). Ce problème serait résolu si l'on pouvait utiliser un type différent deintpour la somme et donc la moyenne.

Pas de problème. Ajoutons donc un deuxième paramètre template pour le type de retour et utilisons-le.

template<typename T, typename S>
S moyenne(T tableau[], int taille)
{ 
   S somme(0);                //La somme des éléments du tableau
   for(int i(0); i<taille; ++i)
      somme += tableau[i];

   return somme/taille;
}

Avec cela, il est enfin possible de calculer correctement la moyenne.
Par contre, il faut explicitement indiquer les types à utiliser lors de l'appel de la fonction. Le compilateur ne peut pas deviner quel type vous aimeriez pourS:

#include<iostream>
using namespace std;

template<typename T, typename S>
S moyenne(T tableau[], int taille)
{
  S somme(0);                  //La somme des éléments du tableau
  for(int i(0); i<taille; ++i)
    somme += tableau[i];

  return somme/taille;
}

int main()
{
  int tab[5];
  //Remplissage du tableau

  cout << "Moyenne : " << moyenne<int,double>(tab,5) << endl;

  return 0;
}

De cette manière, on peut spécifier le type utilisé pour le calcul de la moyenne tout en préservant la liberté totale sur le type contenu dans le tableau. Pour bien assimiler le tout, je ne peux que vous inviter à faire quelques exercices, par exemple :

  • écrire une fonction renvoyant le plus petit de deux éléments ;

  • réécrire la fonctionmoyenne()pour qu'elle reçoive en argument unstd::vector<T>au lieu d'un tableau statique ;

  • écrire une fonction template renvoyant un nombre aléatoire d'un type donné.

La spécialisation

Pour l'instant, nous n'avons essayé la fonctionmaximum()qu'avec des types de base. Essayons-la donc avec une chaîne de caractères :

int main()
{
  cout << "Le plus grand est: " << maximum<std::string>("elephant","souris") << endl;

  return 0;
}

Le résultat de ce petit programme est :

Le plus grand est: souris

On l'a déjà vu, l'opérateur<pour les chaînes de caractères compare suivant l'ordre lexicographique. Mais imaginons (comme précédemment) que le critère de comparaison qui nous intéresse est la longueur de la chaîne. Cela se fait en spécialisant la fonction template.

La spécialisation

La spécialisation emploie la syntaxe suivante :

template <>
string maximum<string>(const string& a, const string& b)
{
  if(a.size()>b.size())
    return a;
  else
    return b;
}

Vous remarquerez deux choses:

  • la première ligne ne comporte aucun type entre<et>;

  • le prototype de la fonction utilise cette fois le type que l'on veut et plus le type génériqueT.

Avec cette spécialisation, on obtient le comportement voulu :

int main()
{
  cout << "Le plus grand est: " << maximum<std::string>("elephant","souris") << endl;

  return 0;
}

qui donne :

Le plus grand est: elephant

La seule difficulté de la spécialisation est la syntaxe qui commence par la lignetemplate<>. Si vous vous souvenez de cela, vous savez tout.

L'ordre des fonctions

Pour pouvoir compiler et avoir le comportement voulu, votre programme devra être organisé d'une manière spéciale. Il faut respecter un ordre particulier :

  1. la fonction générique ;

  2. les fonctions spécialisées.

L'ordre est essentiel.
Lors de la compilation, le compilateur cherche une fonction spécialisée. S'il n'en trouve pas, alors il utilise la fonction générique déclarée au-dessus.

Les classes templates

Voyons maintenant comment réaliser des classes template, c'est-à-dire des classes dont le type des arguments peut varier. Cela peut vous sembler effrayant, mais vous en avez déjà utilisé beaucoup. Pensez àvectoroudequepar exemple.
Il est temps de savoir réaliser des modèles de classes utilisables avec différents types.

Je vous propose de travailler sur un exemple que l'on pourrait trouver dans une bibliothèque comme Qt. Lorsque l'on veut dessiner des choses à l'écran, on utilise quelques formes de base qui servent à décomposer les objets plus complexes. L'une de ces formes est le rectangle qui, comme vous l'aurez certainement remarqué, est la forme des fenêtres ou des boutons, entre autres.

Quelles sont les propriétés d'un rectangle ?

Un rectangle a quatre côtés, une surface et un périmètre. Les deux derniers éléments peuvent être calculés si l'on connaît sa longueur et sa largeur. Voilà pour les attributs.

Quelles sont les actions qu'on peut associer à un rectangle ?

Ici, il y a beaucoup de choix. Nous opterons donc pour les actions suivantes : vérifier si un point est contenu dans le rectangle et déplacer le rectangle.

Nous pourrions donc modéliser notre classe comme illustré à la figure suivante :

Modélisation de la classe Rectangle
Modélisation de la classe Rectangle

Le type des attributs

Maintenant que nous avons modélisé la classe, il est temps de réfléchir aux types des attributs, en l'occurrence la position des côtés.

Si l'on veut avoir une bonne précision, alors il faut utiliser desdoubleou desfloat. Si par contre on considère que, de toute façon, l'écran est composé de pixels, on peut se dire que l'utilisation d'intest largement suffisante.

Les deux options sont possibles et on peut très bien avoir besoin des deux approches dans un seul et même programme. Et c'est là que vous devriez tous me dire : « Mais alors, utilisons donc des templates ! ». Vous avez bien raison. Nous allons écrire une seule classe qui pourra être instanciée par le compilateur avec différents types.

Création de la classe

Je suis sûr que vous connaissez la syntaxe même si je ne vous l'ai pas encore donnée. Comme d'habitude, on déclare un type génériqueT. Puis on déclare notre classe.

template <typename T>
class Rectangle{
    //…
};

Notre type générique est reconnu par le compilateur à l'intérieur de la classe. Utilisons-le donc pour déclarer nos quatre attributs.

template <typename T>
class Rectangle{
 
   //…

private:
   
   //Les côtés du Rectangle
   T m_gauche;
   T m_droite;
   T m_haut;
   T m_bas;

};

Voilà. Jusque là, ce n'était pas bien difficile. Il ne nous reste plus qu'à écrire les méthodes.

Les méthodes

Les fonctions les plus simples à écrire sont certainement les accesseurs qui permettent de connaître la valeur des attributs. La hauteur d'un rectangle est évidemment la différence entre la position du haut et la position du bas. Comme vous vous en doutez, cette fonction est template puisque le type de retour de la fonction sera unT.

Une première méthode

Nous pouvons donc écrire la méthode suivante :

template <typename T>
class Rectangle{
public:
    //…

   T hauteur() const
   {
      return m_haut-m_bas;
   }

private:
   
   //Les cotes du Rectangle
   T m_gauche;
   T m_droite;
   T m_haut;
   T m_bas;
};

Vous remarquerez qu'il n'y a pas besoin de redéclarer le type templateTjuste avant la fonction membre puisque celui que nous avons déclaré avant la classe reste valable pour tout ce qui se trouve à l'intérieur.

Et si je veux mettre le corps de ma fonction à l'extérieur de ma classe ?

Bonne question. On prend souvent l'habitude de séparer le prototype de la définition. Et cela peut se faire aussi ici. Pour cela, on mettra le prototype dans la classe et la définition à l'extérieur mais il faut indiquer à nouveau qu'on utilise un type variableT:

template <typename T>
class Rectangle{
public:
    //…

   T hauteur() const; 
   
    //…
};

template<typename T>
T Rectangle<T>::hauteur() const
{
   return m_haut-m_bas;
}

Vous remarquerez aussi l'utilisation du type template dans le nom de la classe puisque cette fonction sera instanciée de manière différente pour chaqueT.

Une fonction un peu plus complexe

Une des fonctions que nous voulions écrire est celle permettant de vérifier si un point est contenu dans le rectangle ou pas. Pour cela, on doit passer un point $\($$$(x; y)$$$\)$ en argument à la fonction. Le type de ces arguments doit évidemment êtreT, de sorte que l'on puisse comparer les coordonnées sans avoir de conversions.

template <typename T>
class Rectangle{
public:
 
   //…

   bool estContenu(T x, T y) const
   {
      return (x >= m_gauche) && (x <= m_droite) && (y >= m_bas) && (y <= m_haut);
   }

private:
    //…
};

Vous remarquerez à nouveau l'absence de redéfinition du typeT. Quoi, je me répète ? C'est sûrement que cela devient clair pour vous. ;-)

Constructeur

Il ne nous reste plus qu'à traiter le cas du constructeur. À nouveau, rien de bien compliqué, on utilise simplement le typeTdéfini avant la classe.

template <typename T>
class Rectangle{
public:

   Rectangle(T gauche, T droite, T haut, T bas)
        :m_gauche(gauche),
         m_droite(droite),
         m_haut(haut),
         m_bas(bas)
   {}

   //…
};

Et comme pour toutes les autres méthodes, on peut définir le constructeur à l'extérieur de la classe. Vous êtes bientôt des pros, je vous laisse donc essayer seuls.

Finalement, voyons comment utiliser cette classe.

Instanciation d'une classe template

Il fallait bien y arriver un jour ! Comment crée-t-on un objet d'une classe template et en particulier de notre classe Rectangle ?

En fait, je suis sûr que vous le savez déjà. Cela fait longtemps que vous créez des objets à partir de la classe templatevectoroumap. Si l'on veut unRectanglecomposé dedouble, on devra écrire :

int main()
{
   Rectangle<double> monRectangle(1.0, 4.5, 3.1, 5.2);
  
   return 0;
}

L'utilisation des fonctions se fait ensuite comme d'habitude :

int main()
{
   Rectangle<double> monRectangle(1.0, 4.5, 3.1, 5.2);

   cout << monRectangle.hauteur() << endl;
  
   return 0;
}

Pour terminer ce chapitre, je vous propose d'ajouter quelques méthodes à cette classe. Je vous parlais d'une méthodedeplacer()qui change la position du rectangle. Essayez aussi d'écrire les méthodessurface()etperimetre().

Enfin, pour bien tester tous ces concepts, vous pouvez refaire la classeZFractionde sorte que l'on puisse spécifier le type à utiliser pour stocker le numérateur et le dénominateur. Bonne chance !

En résumé

  • Les templates sont utilisés pour créer différentes versions d'une fonction ou d'une classe pour des types différents.

  • Pour créer une fonction ou une classe template, il faut déclarer un type générique en utilisant la syntaxetemplate<typename T>.

  • Pour utiliser une fonction ou une classe template, on indique le type désiré entre les chevrons<et>.

  • Il est possible de spécialiser les templates pour leur imposer un comportement particulier pour certains types.

Exemple de certificat de réussite
Exemple de certificat de réussite