Généricité ✱

Modèle de fonction

Introduction

Si nous voulons créer une fonction max() traitant plusieurs types comme les doubles, des int ou des floats, nous pouvons créer une fonction de cette forme :

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

Le problème est que si l’on passe deux valeurs flottantes, elles seront converties en entier et ensuite la valeur de retour sera forcément entière, ce qui ne correspond pas au résultat attendu. Une solution consiste à utiliser le polymorphisme ad hoc et à écrire une fonction pour chaque type devant être traité :

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 à dupliquer le code autant de fois que le nombre de types que vous voulez traiter. De plus, un principe important dans le développement informatique est la non duplication du code, car cela augmente le nombre de lignes inutilement et cela favorise la présence d’erreurs. En effet, statistiquement parlant, on peut considérer qu’un code deux fois plus long contient en moyenne deux fois plus de bugs. Il est plus judicieux d’écrire un code une seule fois et de faire en sorte qu’il soit le plus réutilisable possible pour éviter les duplications et autres copiés-collés.

Ainsi est apparu un nouveau concept appelé généricité : la capacité d’un morceau de code à s’adapter à des types différents. En C++, on parle de modèles ou de patrons (template) utilisant des paramètres acceptant des types comme arguments. Dans ce contexte, voici une implémentation possible de notre fonction max() mais pas forcément la meilleure :

#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;
}

Pensez aux const références

Les paramètres de la fonction template précédente posent problème :

  • Imaginons que l’on traite des BigInteger servant à gérer des entiers de plusieurs milliers de chiffres, ainsi l’écriture : max(T a, T b) implique que ces objets vont être passés par recopie, entraînant une perte de performance. Ainsi, l'utilisation de références est recommandée.

  • Le fait de passer un objet par référence sous-entend que cet objet peut-être modifié par la fonction template. Lorsque ce n’est pas le cas, il est recommandé d’utiliser des const références pour clarifier le fait que les arguments sont accessibles uniquement en mode read-only.

  • Indirectement, mais cela est hors programme, en utilisant des const references, le compilateur accepte que vous passiez en argument des littéraux (des valeurs comme 1000) ce qui ne serait pas possible avec des références standards qui n’acceptent que des lvalues.

  • Vous le savez déjà : on ne peut retourner une référence vers un objet local. Le type de retour ne peut donc être une référence.

En résumé, voici la syntaxe optimale pour la fonction template max :

SYNTAXE

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

Appel explicite

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 modèle avec les types adéquats. Nous remarquons qu’il n’utilise pas le même mécanisme que pour la surcharge de fonctions qui aurait sélectionné par défaut la fonction mean(int,int,int) car elle demandait le moins de conversion implicite.

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

Opérateur de conversion de type

Maintenant que nous connaissons l’existence des modèles, nous pouvons introduire l’opérateur de convertion de type static_cast<type>. En donnant explicitement 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

Le tout générique

Techniquement, il est possible d’utiliser plusieurs typename dans un modèle. Pour le fun, on peut ainsi pousser le concept encore plus loin en paramétrant tous les types utilisés :

#include <iostream>

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

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

Paramètres implicites

Il est possible de ne transmettre qu’une partie des paramètres attendus, les autres étant donnés implicitement par les arguments :

#include <iostream>

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

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

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

../_images/visual.png

On peut aller jusqu’à ne spécifier aucun argument du template :

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

int main()
{
        std::cout << mxx(1,7) << std::endl;
}

Passer des valeurs

Les paramètres des patrons ne se limitent pas aux types, on peut aussi fournir des valeurs ! 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 multiplier(T valeur)
{
        return valeur * N;
}

int main()
{
        cout << multiplier<int, 5>(10) << endl;     // Multiplie 10 par 5
        cout << multiplier<double, 3>(2.5) << endl; // Multiplie 2.5 par 3
        return 0;
}

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) {…}

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

Appel de la fonction : template<typename T,> T max(T a, T b) { if (b>a) return b; else return a; }

Modèle de classe

Syntaxe

Les modèles de classe 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 = t.b = 1;
}

Remarques

Intégration dans les .h

Les patrons doivent entièrement être localisés dans les fichiers d’entête (.h). Même s’il est possible de séparer leur interface de leur implémentation, le compilateur requiert que tout le code associé à un modèle de classe soit placé dans le même fichier d’entête. Voici un exemple de modèle de classe que l’on peut trouver dans un fichier header (.h) :

template<typename T> struct tuple
{
        T a,b;
        void Reset(T r) { a = b = r; }
};

ou de manière équivalente mais en séparant le code de l’interface :

// interface
template<typename T> struct tuple
{
        T a,b;
        void Reset(T r);
};

// code
template<typename T> void tuple<T>::Reset(T r)
{
         a = b = r;
}

Nommer les modèles spécialisés

Si vous trouvez la syntaxe à base de <> trop longue, rien ne vous empêche d’utiliser des noms spécifiques :

#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

Passage de valeurs

Les modèles permettent d’obtenir des types paramétrables, mais on peut aussi se servir de ce mécanisme pour transmettre des valeurs constantes. Par exemple, pour des questions de recherche de performance, on peut créer une classe v20 correspondant à un vecteur de 20 valeurs. Cette constante 20 est injectée dans le modèle au moment de la compilation 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

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.

Hardcore C++

On peut tout autant utiliser une fonction comme argument d’une fonction template ! Voici un exemple :

void add1(int& v)    {   v += 1;  }
void add2(int& v)    {   v += 2;  }

template <typename F> void doOperation(F f)
{
        int temp = 0;
        f(temp);
        std::cout << "Résultat : " << temp << std::endl;
}

int main()
{
        doOperation(add1);
        doOperation(add2);
}

>> 1
>> 2

On peut aussi passer un opérateur en prenant soin de l’embarquer dans un objet (functor) !

struct Op { void operator() (int& v) { v += 3; } };  // on embarque un opérateur dans une structure

template <typename F> void doOperation(F f)
{
        int temp = 0;
        f(temp);
        std::cout << "Résultat : " << temp << std::endl;
}


int main()
{
        Op functor;
        doOperation(functor);
}

>> 3

Accrochez vos ceintures ! On peut aussi passer une fonction template comme argument template d’une fonction template !

template<int T> void addx(int& v) { v += T; }

template <typename F> void doOperation(F f)
{
        int temp = 0;
        f(temp);
        std::cout << "Résultat : " << temp << std::endl;
}

int main()
{
        doOperation(addx<4>);
}

>> 4