Outils d'utilisateurs

Outils du Site


fonctions_generiques

Ceci est une ancienne révision du document !


Fonctions génériques

Intérêt de la programmation générique

La programmation générique consiste à écrire un code qui n'est pas spécifique d'un type particulier, mais peut s'adapter à plusieurs types.

Pour comprendre ce concept, prenez un exemple simple : vous souhaitez écrire une fonction qui réalise une addition. Vous pouvez par exemple écrire :

#include <iostream>
 
int add(int lhs, int rhs) {
    return lhs + rhs;
}
 
int main() {
    std::cout << add(3, 4) << std::endl;
}

affiche :

7

Rien de compliqué, c'est une fonction classique qui prend deux parametres entiers et retourne un entier.

Si vous ajoutez le second calcul suivant :

#include <iostream>
 
int add(int lhs, int rhs) {
    return lhs + rhs;
}
 
int main() {
    std::cout << add(3, 4) << std::endl;
    std::cout << add(1.2, 3.4) << std::endl;
}

affiche :

main.cpp:9:22: warning: implicit conversion from 'double' to 'int' 
changes value from 1.2 to 1 [-Wliteral-conversion]
    std::cout << add(1.2, 3.4) << std::endl;
                 ~~~ ^~~
main.cpp:9:27: warning: implicit conversion from 'double' to 'int' 
changes value from 3.4 to 3 [-Wliteral-conversion]
    std::cout << add(1.2, 3.4) << std::endl;
                 ~~~      ^~~
2 warnings generated.
7
4

Premièrement, le code émet deux avertissements, du fait de la conversion implicite de double en int. Et deuxièmement, le résultat obtenu n'est pas correct, la valeur attendue (4.6) est arrondie. La raison est que vous avez écrit une fonction add qui utilise des entiers comme parametres. Le compilateur a le choix entre réaliser une conversion des types si c'est possible, ou de produire une erreur si ce n'est pas le cas.

L'appel de la fonction add avec des valeurs de type double est équivalent au code suivant :

const int lhs = 1.2;  // arrondi en 1
const int rhs = 3.4;  // arrondi en 3
add(lhs, rhs);        // calcul 1 + 3

Une première solution pour corriger ce problème est d'utiliser une surcharge de fonctions et d'écrire une fonction add qui prend un entier en paramètre et une fonction add qui prend un réel :

int add(int lhs, int rhs) {
    return lhs + rhs;
}
 
double add(double lhs, double rhs) {
    return lhs + rhs;
}

Dans ce cas, le compilateur n'a pas besoin de faire de conversion, il utilise la fonction correspondante aux types des arguments.

Cependant, cette approche est limitée. Si vous appelez cette fonction avec des arguments de type short int ou float (par exemple), le résultat sera automatiquement convertie respectivement en int et en double (par promotion).

Pour éviter cela, il faudra proposer une surcharge de la fonction add pour chaque type d'arguments que vous voulez utiliser. Le code n'est pas facilement évolutif, vous devez modifier un code existant si vous ajouter des nouveaux types.

La programmation générique va permettre de résoudre ce problème, en écrivant des fonctions qui prendront plusieurs types de parametres. Un exemple de telles fonctions que vous avez déjà rencontrée est les algorithmes de la bibliothèque standard, qui peuvent être appellee sur plusieurs types de conteneurs.

std::string s { "azerty" };
std::sort(std::begin(s), std::end(s));  // ok
 
vector<int> v { 1, 3, 5, 2, 4 };
std::sort(std::begin(v), std::end(v));  // ok

Definir une fonction template

Dans une fonction template, un ou plusieurs types utilisés dans la fonction (généralement les types des parametres de fonction ou du retour de la fonction) sont remplacés par un parametre template, pouvant representer plusieurs types.

La syntaxe d'une fonction template est la suivante :

template<LISTE_PARAMETRES_TEMPLATE>
FONCTION...

La première ligne permet de definir un ou plusieurs paramètres template, qui seront utilisés dans la fonction comme si c'était des types.

Un paramètre template s'écrit de la façon suivante :

typename IDENTIFIANT

Le mot-clé typename indique que l'identifiant represente un nom de type. L'identifiant respecte les règles habituelles pour écrire un identifiant (contient des lettres minuscules ou majuscules, le caractère _ ou des chiffre sauf en premiere position).

Une liste de parametres template sera constitué de plusieurs parametres template (mot-clé typename et identifiant), séparés par des virgules.

typename IDENTIFIANT, typename IDENTIFIANT, typename IDENTIFIANT (...)

Il est classique d'utiliser des majuscules uniquement pour ecrire les parametres template. En particulier, vous verrez souvent des parametres template nommes T, U, etc. Bien sur, il est préférable de donner des noms les plus expressifs possible.

Par exemple, avec la fonction add precedente :

template<typename T>
T add(T lhs, T rhs) {
    return lhs + rhs;
}

Ce code definie une fonction template qui possède un paramètre template nomme T. Ce paramètre template est utilisée trois fois dans la fonction : dans les deux parametres de fonction en entrée et comme type de retour de la fonction.

typename et class

Il est également possible d'utiliser le mot-clé class a la place de typename. Pour éviter les ambiguïtés avec les classes de la programmation objet, seul le mot-clé typenamesera utilisée dans ce cours. Mais retenez que vous pouvez rencontrer aussi class dans un code.

Ce code implique que les types des parametres en entrée et en sortie sont le même type : T peut être remplacé par n'importe quel type, mais chaque occurrence de T correspondra toujours au même type.

Il est possible d'utiliser des types differents pour les differents parametres. Par exemple :

template<typename T, typename U, typename V>
T add(U lhs, V rhs) {
    return lhs + rhs;
}

Ce code utilise trois paramètre template T, U et V, chaque paramètre pouvant être remplacé par un type différent.

Vous etes libre de definir autant de parametre template que vous souhaitez et de les mélanger avec des types concrets.

template<typename T, typename U>
T f(int a, U b, double c, T d) {}

Appeler une fonction template

Lors de l'appel d'une fonction template, le compilateur va remplacer les parametres template par des types concret. Cette étape s'appelle l'instanciation des template.

A partir d'une fonction template, le compilateur va générer les fonctions concrète correspondant a chaque type concret qui sont utilisés dans les appels de fonction.

Par exemple, pour la fonction add precedente, si cette fonction est appellee avec les types int et double, le compilateur va générer deux fonction surchargées, correspondant a ces types concrets.

template<typename T>
T add(T lhs, T rhs);
 
// devient :
 
int add(int lhs, int rhs);
 
double add(double lhs, double rhs);

Parametres et arguments

Notez la similitude des termes utilisés entre parametres et arguments de fonction et parametres et argument template : les parametres apparaissent dans la declaration des fonctions et les arguments dans les appels de fonction.

Pour les parametres et arguments de fonction :

void f(int a, int b, int x) {}  // a, b et c = parametres de fonction
 
int main() {
    f(x, y, z);                 // x, y, z = arguments de fonction
}

Pour les parametres et arguments template :

template<typename T, typename U>  // T et U = parametres template
void f(T a, U b) {}
 
int main() {
    f<int, double>(x, y);         // int et double = arguments template
}

deduction automatique des arguments template

type par defaut

valeur et type, compile time et runtime.

similitude argument/parametre

fonction template intermediaire, forward

auto n'est pas pris en charge par tous les compilateurs. Possibilité d'expliciter le type générique en utilisant des fonctions template.

Une fonction classique permet de passer des données en paramètre. Les fonctions template vont plus loin, elles permettent de passer des types comme paramètre. C'est-à-dire que les types manipulés par un template ne sont pas fixés (int, double, etc), mais sont des paramètres.

Vous avez déjà vu des template dans ce cours, le meilleur exemple est std::vector et std::array. Ces classes template représentent des collections pouvant contenir n'importe quel type de données. Le type manipulé dans la collection est indiqué dans les chevrons :

std::vector<int> ints {};    // tableau de int
std::vector<double> doubles {}; // tableau de double

Pour définir une fonction template, la syntaxe :

template<paramètres template>
paramètre_retour nom_fonction(paramètres de fonction) {
}

On voit ici qu'une fonction template prend deux types de paramètre :

  • les paramètres template, qui sont des types et sont évalués à la compilation. Mot-clé “typename” ou “class” suivi d'un nom de paramètre ;
  • les paramètres de fonction, qui sont des valeurs et sont évaluées à l'exécution ( sauf constexpr)

Les paramètres template déclarés entre les chevrons peuvent ensuite être utilisés dans la fonction (même dans les paramètres de fonction et le type de retour de fonction).

Par exemple, pour la fonction add, on peut écrire :

template<typename T>
T add(T lhs, T rhs) {
    return lhs + rhs; 
}

On déclare ici un paramètre template qui se nomme “T”, que l'on utilise comme retour de fonction et comme type pour les paramètres de fonction (n'oubliez pas que T représente un type, pas une variable).

Pour appeler une fonction template, en spécifiant les arguments :

nom_fonction<arguments template>(arguments de fonction);

Les arguments template sont les types qui seront utilisés pour appeler la fonction. Par exemple, écrire add<int> permet au compilateur de remplacer T par int dans le code précédent, qui devient :

int add(int lhs, int rhs) {
    return lhs + rhs; 
}

De même si on écrit add<double> ou n'importe quoi d'autre.

On peut donc appeler cette fonction add avec différents types d'arguments :

int i = add<int>(1, 2);
double d = add<double>(1.2, 3.4);

Le type T est défini uniquement dans la fonction template, donc il n'est pas possible d'écrire :

T i = add<int>(1, 2);
T d = add<double>(1.2, 3.4);

(cela n'aurait pas de sens, le compilateur ne sait pas si T = int ou double). Il est possible d'utiliser l'inférence de type :

auto i = add<int>(1, 2);
auto d = add<double>(1.2, 3.4);

Dans ce cas, le compilateur sait déterminer le type.

déduction automatique des arguments template

différence avec auto → un seul type pour lhs et rhs, add(1, 1.2) pose problème. Possible écrire :

template<typename T1, typename T2, typename T3>
T1 add(T2 lhs, T3 rhs);

Nécessite de mettre T1 au moins (ne peut pas être déduit)

Possible aussi de ne pas mettre common_type :

template<typename T1, typename T2>
std::common_type<T1, T2> add(T1 lhs, T2 rhs);

plus besoin de spécifier le type de retour

note : template != generique. Plus puissant, langage complet (langage dans un langage, turing complet, meta programmation).

Les algorithmes de la bibliothèque standard

Idem, avec Itérateur en paramètre template. Prototype :

template<typename Iterator>
void sort(Iterator begin, Iterator end);

Lorsque l'on appelle cette fonction, le compilateur détermine quel est le type de Iterator en fonction des arguments passés :

std::vector<int> v {};
std::sort(std::begin(v), std::end(v); // on sait que Iterator correspond à un itérateur sur un vector<int>
fonctions_generiques.1476653201.txt.gz · Dernière modification: 2016/10/16 23:26 par gbdivers