Les templates en C++

mamiemando Messages postés 33073 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 16 avril 2024 - 30 mai 2022 à 03:07

Introduction

Nous allons présenter la notion de template (patron en français). Les templates font parties des grands apports du C++ par rapport au langage C.

Ils permettent de paramétrer un type ou une fonction par un (ou plusieurs) type(s). Par exemple, en C, si on veut définir un vecteur de flottants et une liste d'entiers, on est contraint de définir deux structures dédiées. En C++, on peut abstraire le type encapsulé par le vecteur si la classe implémentant le vecteur est "template".

On appelle dans ce qui suit symbole indifféremment une fonction, une structure ou une classe.

Avantages

Du moment que le(s) type(s) passé(s) en paramètre sont adaptés, on peut passer n'importe quel type à la classe template. Ainsi on ne code qu'un symbole quel que soit le(s) type(s) passé(s) en paramètre, ce qui rend le code d'autant plus facile à maintenir

Inconvénients

  • Les templates requierent quelques précautions d'usage (typename...)
  • Le programme est plus long à compiler.
  • L'implémentation d'une classe / structure / fonction template doit être faite dans un header (fichier
    .hpp
    ) afin d'être réutilisable. Pour rappel, on n'inclue jamais un fichier source (fichier
    .cpp
    ) depuis un autre fichier afin d'éviter des erreurs en fin de compilation (linkage). Il est important de comprendre que c'est parfaitement normal pour un template : le compilateur ne sait pas à l'avance quelles déclinaisons du symbole template il va devoir compiler, il faut donc que cette définition soit connue par l'ensemble des fichiers sources qui en auront besoin. En effet, contrairement aux symboles non template, il n'y a pas une manière absolue de compiler le symbole template (dans un fichier
    .o
    ).

Quand utiliser des templates ?

  • Pour une classe stockant une collection d'objets arbitraire (une liste, un vecteur, un graphe...).
  • Dans certains cas exotiques, voir comment afficher les nombres de 1 à 1000 en C ou C++ sans boucle ni structure conditionnelle ?)
  • Pour implémenter un algorithme devant s'abstraire partiellement de la structure de données passée en paramètre. Par exemple, la librairie boost implémente l'algorithme de Dijkstra de sorte à s'abstraire de la structure du graphe (du moment qu'elle hérite d'une structure de graphe boost adaptée), des primitives de calcul (comparaison et concaténation pour le calcul des meilleurs chemins), et des structures stockant les résultats du calcul.

Quelques templates célèbres

STL

La STL (Standard Template Library) est livrée de base avec les compilateurs C++. Cette librairie fournit un jeu de containers génériques, notamment :
  • std::vector
    : les vecteurs (tableau d'éléments de type T adjacents en mémoire), accès en O(1).
  • std::set
    : les ensembles d'éléments de type T sans doublon et ordonnés selon l'opérateur <, accès en O(log(n)).
  • std::list
    : les listes chaînées (accès en O(n), insertion en début et fin de liste en O(1)).

BGL

La BGL (Boost Graph Library) fournit des classes de graphe génériques et les algorithmes qui vont avec (algorithmes de plus court chemin, algorithme de flot, parcours de graphe, ...).

La BGL s'installe facilement sous Linux. Par exemple, sous Debian et les distributions qui en dérivent :
sudo apt install libboost-graph-dev

Pré-requis

Pour rappel, le C++ introduit (comparé au C) aussi l'opérateur ::. Il permet d'accéder aux déclarations (types, méthodes, ...) faites dans une classe / structure ou dans un espace de nommage. On peut le voir un peu comme le '/' des répertoires. Il n'est pas donc spécifique aux templates, mais souvent utilisé dans ce contexte. Par exemple, le type
std::vector<int>::const_iterator
signifie que l'on récupère le type
const_iterator
, stocké dans la classe
vector<int>
, elle-même codée dans le
namespace std
.

Par ailleurs, le C++ introduit deux mots clés pour gérer les classes templates :
  • Pour la déclaration, le mot clé
    template
    : il indique que le symbole (structure, classe, fonction) qui va suivre prend des paramètres templates. On écrit directement après le mot clé template les paramètres templates (précédés du mot clé typename, struct, class, ou type de base selon le type de paramètre template attendu) entre chevrons, suivis du symbole écrit normalement. Attention à bien séparer les chevrons (fermants) afin qu'il ne soient pas confondus avec l'opérateur >> .
  • Pour l'utilisation, le mot clé
    typename
    : il indique que le type qui suit n'est pas encore résolu car il dépend encore d'un ou plusieurs paramètres template. Il n'est donc utile que lorsqu'on déclare un symbole template, en vue de déclarer quelque chose d'autre.
    • Exemple 1 : pas de
      typename
      :

void f(const std::vector<int> & v) {
    std::vector<int>::const_iterator it = v.begin(); // Ne dépend pas d'un type template
}
    • Exemple 2 :
      typename
      nécessaire :

template <typename T>
void f(const std::vector<T> & v) {
    typename std::vector<T>::const_iterator it = v.begin(); // Type template à gauche d'un :: => besoin de typename
}

Convention de notations

Les paramètres templates sont généralement notés en camel case (par exemple
MonType
) alors que les autres types sont notés en minuscules et suffixés par
_t
(par exemple
mon_type_t
). Si on préfère utiliser tout le temps le style camel case, il est recommandé de préfixer les paramètres templates par
T
(par exemple
TMonType
)

Suivre cs conventions n'est pas obligatoire, mais aide à savoir quand le mot clé
typename
et rend le code plus lisible.

Déclaration d'un template

Fonction template

template <typename T1, typename T2, ... >
type_retour_t ma_fonction(param1_t p1,param2_t p2, ...){
...
}

Classe / structure template

template <typename T1, typename T2, ... >
class ma_classe_t{
...
};

template <typename T1, typename T2, ... >
struct ma_struct_t{
...
};
</code>

Utilisation d'un template

  • Lorsqu'on déclare une instance dépendant d'une structure / classe template, les paramètres templates (non optionnels) doivent être explicités.
  • Lorsqu'on appelle une fonction ou une méthode template, si les paramètres de celles-ci permettent de déduire ses paramètres template, on peut les omettre. Sinon il faut les préciser.

Exemple détaillé

L'exemple ci-dessous montre comment implémenter une classe de vecteur plus simple que
std::vector
:

my_vector.hpp

#include <iostream>
#include <cstdlib>
#include <ostream>

// Une classe template prenant un paramètre
template <typename T>
class my_vector_t{
 protected:
  unsigned taille; // stocke la taille du vecteur
  T *data; // stocke les composantes du vecteur
 public:
  // Le constructeur
  my_vector_t(unsigned taille0 = 0,const T & x0 = T()):
   taille(taille0),
   data((T *)malloc(sizeof(T)*taille0))
  {
   for(unsigned i=0;i<taille;++i) data[i] = x0;
  }

  // Le destructeur
  ~my_vector_t(){
   free(data);
  }

  // Retourne la taille du vecteur
  inline unsigned size() const{
   return taille;
  }

  // Un accesseur en lecture seule sur la ième case du vecteur
  inline const T & operator[](unsigned i) const{
   if(i >= size()) throw;
   return data[i];
  }

  // Un accesseur en lecture écriture sur la ième case du vecteur
  inline T & operator[](unsigned i){
   if(i >= size()) throw;
   return data[i];
  }
};

// Un opérateur template
template <typename T>
std::ostream & operator<<(std::ostream & out, const my_vector_t<T> & v){
 unsigned n = v.size();
 out << "[ ";
 for(unsigned i=0;i<n;++i) out << v[i] << ' '; 
 out << ']'; 
 return out;
}


main .cpp

#include "my_vector.hpp"

// Une fonction template (comme on ne l'utilise que dans ce fichier, pas besoin de la mettre dans un fichier header)
template <typename T>
void ecrire(const my_vector_t<T> & v){
    unsigned n = v.size();
    std::cout << "[ ";
    for(unsigned i=0;i<n;++i) std::cout << v[i] << ' '; 
    std::cout << ']';
}

int main(){
    my_vector_t<int> v(5); // un vecteur de 5 entiers
    v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
    ecrire<int>(v); // appel de la fonction template
    std::cout << std::endl;
    ecrire(v); // appel implicite de ecrire<int>
    std::cout << std::endl;
    std::cout << v << std::endl; // appel de l'opérateur template
    return 0;
}


Résultat :
[ 6 2 3 4 8 ]
[ 6 2 3 4 8 ]
[ 6 2 3 4 8 ]

Utilisations avancées

Spécifications de templates

Rien n'empêche d'implémenter spécifiquement un symbole pour un jeu de paramètres templates. Rien n'oblige à spécifier des paramètres template qui peuvent être déduits automatiquement par le compilateur. En cas d'ambiguïté, le compilateur privilégie la déclaration la "moins template".

Ici, on modifie
ecrire
pour que celle-ci ne soit spécifiquement utilisable que pour des
vector<int>
:
#include "my_vector.hpp"

// Une fonction template (comme on ne l'utilise que dans ce fichier, pas besoin de la mettre dans un fichier header)
template <typename T>
void ecrire(const my_vector_t<T> & v){
    unsigned n = v.size();
    std::cout << "[ ";
    for(unsigned i=0;i<n;++i) std::cout << v[i] << ' '; 
    std::cout << ']';
}

// Spécification de template (on pourrait garder sans problème l'ancienne déclaration en plus)
void ecrire(const my_vector_t<int> & v){
    unsigned n = v.size();
    std::cout << "{ ";
    for(unsigned i=0;i<n;++i) std::cout << v[i] << ' ';
    std::cout << '}';
}

int main(){
    my_vector_t<int> v(5); // un vecteur de 5 entiers
    v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
    ecrire<int>(v); // appel de la fonction template
    std::cout << std::endl;
    ecrire(v); // appel à ecrire (prévaut sur l'appel implicite de ecrire<int>)
    std::cout << std::endl;
    std::cout << v << std::endl; // appel de l'opérateur template
    return 0;
}

Résultat :
{ 6 2 3 4 8 }
{ 6 2 3 4 8 }
[ 6 2 3 4 8 ]

Template par défaut

Il est également possible de préciser un paramètre template par défaut de la même façon que pour un paramètre de fonction.

Par exemple :
template<typename T = int>
class my_vector_t{
   //...
};

int main(){
  my_vector<> v; // un vecteur d'int
  return 0;
}

Quelques exemples célèbres de templates par défaut : dans la STL le foncteur de comparaison, utilisé dans les std::set, est initialisé par défaut par std::less (foncteur de comparaison basé sur <). Ainsi on peut écrire indifféremment :
std::set<int> s;
std::set<int,std::less<int> > s_;

Récupérer des paramètres templates, des types et des méthodes statiques d'une classe template

Il est recommandé d'exposer à l'aide de
typedef
publics les types templates paramétrant la classe qu'on implémente. Cela permet depuis les classes qui utilise notre classe de récupérer les paramètres templates qui ont été utilisés.

Exemple :
template <typename T>
struct my_class_t{
 typedef T data_t;
};

int main(){
 typedef my_vector_t<int>::data_t data_t; // Ceci est un entier
}

Templates récursifs

Il est possible de définir des templates récursifs.

Exemple :

#include <iostream>

template <int N>
int fact(){
    return N * fact<N - 1>();
}

template <>
int fact<0>(){
    return 1;
}

int main(){
    std::cout << fact<5>() << std::endl;
    return 0;
}


Concrètement, ce code va engendrer la compilation de
fact<5>
,
fact<4>
...
fact<0>
. Cet exemple jouet n'a pas d'intérêt pratique, mais montre que c'est possible. Une cas de template récursif plus intéressant (mais plus compliqué) est celui mis en œuvre dans la librairie boost pour implémenter les tuples génériques.

Tester des valeurs de type template (avec boost)

Il est possible avec boost de vérifier si un type template correspond à un type attendu et de bloquer la compilation le cas échéant. Étant donné que ça utilise la librairie boost, je donne juste un bref exemple :
#include <boost/type_traits/is_same.hpp>
#include <boost/static_assert.hpp>

template <typename T>
struct ma_struct_qui_ne_doit_compiler_que_si_T_est_int{
 BOOST_STATIC_ASSERT((boost::is_same<T,int>::value));
 //...
};

Liens utiles