Template


To translate this course in your native langage:

  • In Chrome: right-click anywhere on the page and select “Translate to…”

  • Or use this language selector:

Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.


Modèle de fonction

On utilise using namespace std; pour limiter la présence des std::.

Introduction

La fonction maxi() ci-dessous ne travaille qu’avec des entiers :

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

Si on utilise des nombres à virgule comme arguments, comme par exemple en écrivant maxi(3.2, 4.5), ils seront alors transformés en nombres entiers. La fonction renverra donc 4 au lieu de 4.5. Pour éviter ce problème, on peut créer plusieurs versions de la fonction maxi(), une pour chaque type (int, float, double, etc.).

int    maxi(int    a, int    b) { if (b>a) return b; else return a; }
float  maxi(float  a, float  b) { if (b>a) return b; else return a; }
double maxi(double a, double b) { if (b>a) return b; else return a; }

Cela peut être très long, et il faut le faire pour chaque fonction. Pour éviter cela, on utilise la généricité (genericity), un mécanisme de programmation qui permet d’écrire un patron de fonction capable de fonctionner avec plusieurs types. Cette technique s’appuie sur le mot-clé template<typename T> pour désigner T comme un type générique. Ainsi, dans la fonction qui suit ce mot-clef, on peut utiliser le type T comme tout autre type. Voici un exemple :

#include <iostream>
using namespace std;

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

int main()
{
        cout << maxi(1,2) << endl;
        cout << maxi(1.3,4.5) << endl;
}

Référence ou copie ?

Lorsque vous écrivez une fonction, vous pouvez choisir un passage par copie ou par référence. Normalement, la stratégie est la suivante :

  • Pour des types fondamentaux, on préfère un passage par copie car plus efficace

  • Pour les objets, on préfère un passage par reference / const reference pour éviter une copie

Cependant, pour les patrons de fonction, nous ne savons pas le type utilisé à l’avance, et les autres programmeurs peuvent aussi bien utiliser votre template en passant des types fondamentaux ou des objets. Comme le surcoût d’une copie d’un gros objet est généralement plus important que le surcoût d’un passage par référence sur un type fondamental, on préfère adopter la convention suivante :

Si votre fonction template peut manipuler des objets, préférez un passage par reference ou const reference. Si elle ne manipule que des types fondamentaux, un passage par valeur convient généralement très bien.

Pour le type de retour, les règles habituelles s’appliquent : objet pour un temporaire/local, référence pour un élément pérenne.

Exemple

Prenons une fonction qui :

  • lit deux objets en entrée → 2 paramètres const reference

  • retourne un nouvel objet → retour par valeur

Voici le code correspondant :

template<typename T>  T exemple(const T & a, const T & b)
{
        T result;
        ...
        return result;
}

Templates à plusieurs paramètres

Techniquement, il est possible d’utiliser plusieurs typename dans un template. Voici un exemple :

template<typename Key, typename Value>
struct Element
{
    Key   key;
    Value value;
};

int main()
{
    Element<int,string>    player;
    Element<string,double> price;
}

Fonction template avec valeur constante

Les paramètres des fonctions templates ne se limitent pas aux types, on peut aussi leur fournir des valeurs constantes ! Voici un exemple avec la fonction puissance, le paramètre N défini dans le template est utilisé directement dans le code comme une valeur :

template <int N>
double power(double x)
{
    double result = 1;

    for (int i = 0; i < N; ++i)   result *= x;

    return result;
}

int main()
{
    cout << power<7>(2.0) << endl; // 128
}

Ainsi, la valeur 7 est connue à la compilation et peut être exploitée par le compilateur pour optimiser le code.

Appel d’un template

Spécification explicite

L’appel explicite consiste à spécifier le type T lors de l’appel de la fonction template :

int result = maxi<int>(4.3,2);    // result : 4

Cette syntaxe permet d’instancier la fonction template (instantiate the function template) maxi(int a, int b) à partir du patron de fonction T maxi(T a,T b). Les arguments sont alors convertis vers le type imposé, c’est à dire le type int. Ainsi, cet appel retourne 4.

Déduction implicite

Il est également possible de ne pas préciser le type T. Celui-ci est alors déduit automatiquement à partir du type des arguments.

Règle : Lors d’une déduction implicite, toutes les occurrences du paramètre T doivent conduire à la même déduction de type.

Ainsi :

maxi(2, 3);      // T = int
maxi(2.0, 3.0);  // T = double
maxi(2, 3.0);    // error : T = int ? double ?

Quizzz

Indiquez si les syntaxes proposées sont valides (V) ou fausses (F) :

  • template<typename T> T maxi(T a, T b) {…}

  • template<typename U> T maxi(U a, U b) {…}

  • template<typename A,typename B> A maxi(A a, B b) {…}

  • template<typename A,typename B> A maxi(T a, T b) {…}

  • template<typename A,typename B,typename C> A maxi(B a, C b) {…}

Indiquez si les appels suivants sont corrects (V) ou produisent une erreur (F) :

template<int B,typename A>  int mx(A a)   { if (a>B) return a; else return 0;  }
  • mx<int,int>(7);

  • mx<4,int>(9);

  • mx<2,int>(4.3);

  • mx<int,5>(7);

Classe template

Syntaxe

Les classes template fournissent un mécanisme élégant pour créer des classes génériques opérant sur divers types de données. Leur syntaxe est similaire au template de fonction. Voici un exemple :

template<typename T>
struct vec2D
{
        T a;
        T b;
};

int main()
{
        vec2D<int> t;
        t.a = 5;
        t.b = 1;
}

Fonctions membres d’une classe template

Lorsque vous écrivez une fonction membre dans une classe template, il est possible de réutiliser le paramètre template T de la classe dans cette fonction :

template<typename T>
struct vec2D
{
        T a;
        T b;
        void set(T v)
        {
           a = b = v;
        }

        T getSum()
        {
          T r = a+b;
          return r;
        }
};

Classe template avec constante

Les templates de classe peuvent aussi utiliser des constantes. Voici un exemple avec un vecteur de taille fixe, mais paramétrable à la compilation :

#include <iostream>

template<typename T, int Size>
struct FixedVector
{
        T v[Size];
        void reset(T r) { for (int i = 0; i < size; i++)  v[i] = r + i; }
};

int main()
{
        FixedVector<int, 20> v20;
        v20.reset(10);

        cout << v20.v[0] << " " << v20.v[19] << endl;
}

>> 10 29

Nommer des classes templates spécialisées

Si vous trouvez l’écriture FixedVector<int, 20> trop longue, vous pouvez instancier ce template et lui associer un nom. Ainsi, on peut utiliser l’exemple pour écrire :

using vi20 = FixedVector<int,20>;

int main()
{
        vi20  v;
        v.reset(10);
}

Quizzz

  • Les arguments des templates peuvent être des valeurs constantes.

  • Je peux utiliser le paramètre template d’une classe comme argument d’une fonction template.

  • Comme argument d’un template, on ne peut trouver que des types.

  • On peut donner un nom à une classe template spécialisée

  • Les arguments des templates doivent être connus à la compilation.

Travail à rendre sur l’espace partagé

Exercice 1 : template function

  • Créez un fichier nommé template_fnt.cpp

  • Dans le code exemple ci-dessous, transformez les 4 fonctions en fonctions templates.

  • Modifiez ensuite la fonction test() afin qu’elle teste ces quatre fonctions templates.

  • Transformez également test() en fonction template, afin de pouvoir choisir le type utilisé pour les tests.

  • Enfin, dans la fonction main(), appelez la fonction test() pour des types int et double. Les valeurs passées devront correspondre au type considéré.

#include <iostream>
using namespace std;

int  maxx(int a, int b)                    { if (a>b) return a; else return b; }
void swap_int(int& a, int& b)              { int tmp = a; a = b; b = tmp; }
void leftRotate(int& a, int& b, int& c)    { int tmp = a; a = b; b = c;  c = tmp; }
int& inc(int& a)                           {  ++a;  return a; }


void test(int a, int b, int c)
{
    cout << "a=" << a << ", b=" << b << ", c=" << c << endl;

    cout << "maxx(" << a << "," << b << ")  = " << maxx(a, b) << endl;
    cout << "maxx(" << b << "," << c << ")  = " << maxx(b, c) << endl;


    swap_int(a, b);
    cout << "swap: a=" << a << ", b=" << b << endl;

    leftRotate(a, b, c);
    cout << "leftRotate: a=" << a << ", b=" << b << ", c=" << c << endl;

    inc(c);
    cout << "inc(c): c=" << c << endl;

    inc(inc(c));
    cout << "inc(inc(c)): c=" << c << '\n';
}

int main()
{
    test(-1,5,10);
}
  • Déposez votre fichier dans l’espace partagé

Exercice 2

Dans le chapitre précédent, nous avons codé la classe V10 correspondant à un vecteur de 10 entiers. Nous allons maintenant mettre en place sa version template.

  • Créez un fichier nommé template_V10.cpp.

  • Transformez la classe V10 créée dans le chapitre précédent afin de la rendre générique grâce aux templates C++.

    • Votre nouvelle classe devra permettre de choisir à la fois le type des données et la taille du tableau.

    • Par exemple :

      V10<int, 10>   a;
      V10<double,5>  b;
      V10<char, 20>  c;
      
    • La classe devra reprendre les fonctionnalités de V10 : constructeurs, size(), get(), set(), print(), equals(), operator[], find(), copyFrom() et operator=

    • Veillez à utiliser le mot-clé const partout où cela est pertinent, aussi bien pour les méthodes que pour les paramètres.

  • Pour vérifier votre code, vous devez utiliser les tests suivants : tests.txt

  • Déposez votre fichier contenant la classe et les tests dans l’espace partagé

Avertissement

IA or not IA, that is the question. Ces exercices sont au programme de l’examen.