Template ✱

  • Your native language: with Chrome, right-click on the page and select « Translate into … »

  • English version:

Modèle de fonction

Introduction

Prenons une fonction max() qui traite des entiers :

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

Supposons que l’on appelle cette fonction max() avec pour arguments deux valeurs flottantes. Que se passe-il ? Pour initialiser les paramètres, on convertit ces valeurs en entier. Mais surtout, après son traitement, la fonction va retourner un entier ce qui peut poser problème. En effet, lorsqu’on appelle max(3.2,4.5), on ne s’attend pas à récupérer comme résultat 4. Pour corriger ce problème, une solution consiste à utiliser le polymorphisme ad hoc en écravant une fonction pour chaque type concerné :

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

Cependant, ce choix d’implémentation oblige à répéter plusieurs fois maladroitement la fonction de départ, ceci pour chaque type à traiter. C’est ainsi qu’est apparu un nouveau concept : la généricité, c’est-à-dire la capacité d’une fonction à paramétrer ses types. Un tel code s’appelle un modèle ou un patron (template). L’idée est simple : de la même manière qu’on peut transmettre des valeurs comme arguments d’une fonction, pourquoi ne pas transmettre aussi des types en arguments ? Voici un exemple de définition d’un template de la fonction max() ainsi que son appel : :

#include <iostream>

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

int main()
{
        std::cout << max(1,2) << std::endl;
        std::cout << max(1.3,4.5) << std::endl;
}

Entier ou objet ?

En choisissant d’écrire une fonction en utilisant un template, vous allez finir à un moment par vous mélanger. En effet, pour un argument en lecture seule, vous avez l’habitude de passer les types fondamentaux (int, double…) par valeur et les objets par const référence, ce qui est la bonne attitude. Cependant, dans le cas d’un template, on ne sait pas ce que représente le type T : un entier ou un objet bigInteger. Par conséquent, on prend la convention suivante qui a le mérite de fonctionner dans les deux configurations :

CONVENTION : Dans une fonction template, les paramètres en lecture seule sont passés en utilisant des const reference.

En ce qui concerne les arguments devant être modifiés, la question ne se pose pas : que ce soit pour les types fondamentaux ou les objets, le passage se fait dans tous les cas par référence, donc aucune ambigüité cette fois.

En résumé, voici la syntaxe optimale de la fonction template max() pouvaint ainsi aussi bien traiter des int que des bigInteger :

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

Règles de déduction

Soit vous transmettez lors de l’appel de la fonction template tous les paramètres, soit vous demandez au compilateur de les déduire de manière implicite. Dans le cas des template, il n’y pas de place à l’ambigüité,

CONVENTION : Dans une fonction template, la déduction du type d’un paramètre T doit être univoque et identique pour tous les arguments.

Exemple 1 : appel implicite

Si nous utilisons le code suivant, nous obtenons une erreur :

#include <iostream>

template<typename T>  T mean(const T & a, const T & b, const T & c)  { return (a+b+c)/3; }

int main()
{
        std::cout << mean(1,2,3.14) << std::endl;
}
>> no matching function for call to ‘mean(int, int, double)’

Le compilateur n’a pas trouvé de fonction/fonction template acceptant un (int,int,double). Si la fonction mean() aurait pu été retenue si on avait passé comme arguments (int,int,int) ou (double,double,double). Il n’y a pas à ce niveau de mécanisme de conversion implicite.

Cependant, si l’on transmet explicitement les types au template, alors les mécanismes de conversion implicite sont actifs :

Ainsi, il faut indiquer explicitement le type utilisé par la fonction template :

template<typename T>  T mean(const T & a, const T & b, const T & c)  { return (a+b+c)/3; }

int main()
{
        std::cout << mean<int>(1,2,3.14) << std::endl;
}

>> 2

Exemple 2 : le tout générique

Techniquement, il est possible d’utiliser plusieurs typename dans un template. Ainsi, on peut pousser le concept encore plus loin en paramétrant tous les types utilisés, il est même possible de paramétrer le type de retour séparement !

#include <iostream>

template<typename ReturnType,typename T1, typename T2, typename T3>
ReturnType mean(const T1 & a, const T2 & b, const T3 & c)  { return (ReturnType)((a+b+c)/3); }

int main()
{
        std::cout << mean<float,double,float,int>(9.7,4.8,5) << std::endl;
}
>> 6.5

Si plusieurs paramètres templates existent, la norme du langage permet de n’en instancier qu’une partie. Ainsi, dans le contexte de l’exemple précédent, on peut écrire : mean<float,double>(…), les deux paramètres suivant étant alors géré comme des paramètres implicites.

Quizzz

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

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

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

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

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

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

et avec la fonction :

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.3);

  • mx<int,5>(7);

  • mx<5>(7,9);

  • mx<5>(3.14);

Template class

Syntaxe

Les template class 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 modèle de fonction. Voici un exemple :

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

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

Notes diverses

Assistance de Visual Studio

En faisant un détour par Visual Studio, si vous placez le curseur de la souris sur l’appel d’une fonction générique, l’interface vous indique la version choisie par le compilateur :

../_images/visual.png

Opérateur de conversion de type

Maintenant que nous connaissons la syntaxe des fonctions templates, nous pouvons introduire l’opérateur de convertion de type : static_cast<type>. En donnant le type attentdu, on peut convertir tout élément vers le type désiré si l’opération est licite :

int main()
{
        double pi = 3.1415;
        std::cout << static_cast<int>(pi) << std::endl;
}
>> 3

Fonction template avec littéral

Les paramètres des fonctions templates ne se limitent pas aux types, on peut aussi fournir des littéraux ! Cela peut apporter un gain de performance notable car on injecte des constantes dans le code durant la phase de compilation. Voici un exemple :

template <typename T, int N>
T multiply(T value)
{
        return value * N;
}

int main()
{
        std::cout << multiply<int, 5>(10) << std::endl;       // Multiplies 10 by 5
        std::cout << multiply<double, 3>(2.5) << std::endl;   // Multiplies 2.5 by 3
        return 0;
}

Classe template avec littéral

Les templates de classe peuvent aussi utiliser des literraux. Dans l’exemple ci dessous, plûtot que créer un vecteur de taille quelconque, on force dès la compilation la taille de ce container, ce qui garantit une exécution optimale :

#include <iostream>

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

void main()
{
        vfixed<int, 20> v20;
        v20.Reset(10);

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

>> 10 29

Nommer des templates spécialisés

Si vous trouvez la syntaxe à base de <> trop longue, rien ne vous empêche d’instancier un template et de lui associer un nom :

#include <iostream>

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

using v20i = vfixed<int, 20>;

void main()
{
        v20i v20;
        v20.Reset(10);

        std::cout << v20.v[0] << " " << v20.v[19] << std::endl;
}
>> 10 29

Quizzz

  • Les arguments des templates peuvent être des variables.

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

  • La place d’un template est dans un fichier header.

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

  • On peut donner un nom à une classe template spécialisée (template avec template arguments)

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

Travail à rendre sur Github Classroom

Exercice 1 : template function

  • Créez un fichier nommé 5_template_fnt.cpp

  • Dans le code exemple ci-dessous, transformez les 4 fonctions données vers leurs versions templates.

  • Après cela, écrivez une version template de la fonction test() pour qu’elle teste les 4 fonctions dans le type donné en argument.

  • Changez le main() pour que les tests soient effectués en entier, en double et en flottant. Les valeurs numériques restent les mêmes dans la fonction test().

  • Uploadez le fichier sur votre github

#include <iostream>
using namespace std;

int  max(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()
{
        cout << "max(3, 7)   = " << max(3, 7) << '\n';
        cout << "max(-5, -2) = " << max(-5, -2) << '\n';

        int x = 10, y = 20;
        swap_int(x, y);
        cout << "swap: x=" << x << ", y=" << y << '\n';

        int a = 1, b = 2, c = 3;
        leftRotate(a, b, c);
        cout << "leftRotate: a=" << a << ", b=" << b << ", c=" << c << "\n";

        int t = 41;
        inc(t);
        cout << "inc(t): t=" << t << '\n';

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

int main()
{
        test();
        return 0;
}

Exercice 2 : template class

  • Créez un fichier nommé 5_template_class.cpp

  • Créez une structure template TupleN stockant deux éléments numériques a, b

  • Ajoutez les fonctions membres suivantes :

    • constructeur par défaut qui met les deux valeurs à zéro

    • constructeurs paramétriques

    • reset() qui remet à zéro les deux éléments

    • sort() qui les tris dans l’ordre croissant

    • positive() qui retourne true si les deux éléments sont positifs

    • double getter

    • setter

  • Attention, MAXIMISEZ la présence du mot-clef const

  • Uploadez le fichier sur votre github