====== Les fonctions génériques en C++ ======
===== Pourquoi rendre des fonctions paramétrables par des types ? =====
Nous considérons une fonction ''index_of'' qui effectue la recherche d'un élément dans un tableau. Cette fonction très naïve peut-être écrite comme suit :
int index_of(int* anArray, int theArrayLength, int theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(anArray[index] == theValue)
return index;
return -1;
}
Cependant, cette fonction ne fonctionne que pour des tableaux d'entiers ayant comme type ''int''. Si nous envisageons de travailler sur des tableaux de nombres à virgule flottant ayant par exemple le type ''float'', il faudrait réécrire une nouvelle fonction :
int index_of(float* anArray, int theArrayLength, int theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(anArray[index] == theValue)
return index;
return -1;
}
dont la seule différence serait que cette fonction prend comme argument un tableau de type ''float*'' et non pas un tableau de type ''int*''.
Et si nous devons supporter l'ensemble des types correspondant à des nombres entiers et des nombres à virgule flottante, il faudrait réécrire les fonctions pour les types ''char'', ''unsigned char'', ''short'', ''unsigned short'', ''int'', ''unsigned int'', ''long'', ''unsigned long'', ''float'', ''double'' et enfin ''long double''. Cela fait beaucoup de code à recopier.
===== Les fonctions paramétrées par des types =====
C++ propose de définir des fonctions paramétrables par le type des arguments. Ainsi la fonction précédente peut-être paramétrer par le type des éléments stockés dans le tableau comme suit :
template
int index_of(valueT* anArray, int theArrayLength, valueT theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(anArray[index] == theValue)
return index;
return -1;
}
Avec cette nouvelle syntaxe, nous définissons un **patron** ou un **modèle** (en anglais __template__) qui définit une famille de fonctions :
* qui s'appellent ''index_of'',
* qui prennent trois paramètres :
- un pointeur sur un tableau d'éléments, les éléments ayant chacun pour type ''valueT'' qui est un type paramètre,
- un entier qui donne le nombre d'éléments du tableau,
- un élément de type ''valueT'' qui correspond à l'élément à rechercher dans le tableau.
En fait, quand nous définissons une fonction __template__, nous ne générons pas de code, nous donnons simplement un modèle au compilateur pour générer une fonction si jamais il est nécessaire de générer une telle fonction.
Supposons désormais que nous effectuons une recherche dans un tableau :
template
int index_of(valueT* anArray, int theArrayLength, valueT theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(anArray[index] == theValue)
return index;
return -1;
}
int arrayOfIntegers[9] = { 1, 3, 4, 2, 9, 5, 7, 8, 6 };
int main()
{
std::cout << "'2' est en position " << index_of(arrayOfIntegers, 9, 2);
}
Quand nous écrivons :
std::cout << "'2' est en position " << index_of(arrayOfIntegers, 9, 2);
le compilateur cherche une fonction ayant comme nom ''index_of'' et qui prend comme argument ''int*'', ''int'', ''int''.
* Il n'existe pas de fonction ''index_of(int*, int, int)''.
* Il existe une fonction __template__, donc un modèle de fonction qui prend trois arguments : ''valueT*'', ''int'', ''valueT''.\\ Si on remplace ''valueT'' par ''int'', on génèrera à partir du modèle ''index_of(valueT*, int, valueT)'' une fonction ''index_of(int*, int, int)''.\\ Le compilateur génère la fonction ''index_of(int*, int, int)'' en remplaçant ''valueT'' par ''int'' et en créant ainsi la fonction ''index_of(valueT*, int, valueT)''.
===== Fonctions génériques & fonctions spécialisées =====
Si nous considérons la fonction précédente et que nous souhaitons l'utiliser pour chercher des tableaux contenant des chaînes de caractères :
const char* tokens[6] = { "if", "else", "try", "catch", "switch", "case" };
int main()
{
std::string token = "try";
std::cout << index_of(tokens, 6, token.c_str());
return 0;
}
L'exécution de ce code va retourner non pas l'indice ''2'' comme nous pourrions attendre mais au contraire ''-1''. Le résultat est normal, puisque nous testons non pas l'égalité de deux chaînes de caractères et de deux pointeurs qui pointent sur le premier caractère d'une chaîne de caractères. Et bien entendu, ces deux pointeurs sont différents si les deux chaînes de caractères sont distinctes même si le contenu des deux chaînes de caractères sont identifiques.
Nous pouvons définir une version spécialisée du code :
#include
#include
#include
template
int index_of(valueT* anArray, int theArrayLength, valueT theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(anArray[index] == theValue)
return index;
return -1;
}
template<>
int index_of(const char** anArray, int theArrayLength, const char* theValue)
{
for(int index = 0; index < theArrayLength; index ++)
if(std::strcmp(anArray[index], theValue) == 0)
return index;
return -1;
}
const char* tokens[6] = { "if", "else", "try", "catch", "switch", "case" };
int main()
{
std::string token = "try";
std::cout << index_of(tokens, 6, token.c_str());
return 0;
}
Dans ce cas, comme il existe une version spécialisée qui correspond au type des arguments, la fonction qui sera appellée sera
template<>
int index_of(const char** anArray, int theArrayLength, const char* theValue)
de préférence à la fonction :
template
int index_of(valueT* anArray, int theArrayLength, valueT theValue)
En effet, la fonction spécialisée est considérée comme prioritaire sur la fonction non spécialisée. Et c'est ainsi que nous allons exécuter le code qui ne compare plus les pointeurs mais bien les chaînes de caractères en appelant la fonction [[https://en.cppreference.com/w/c/string/byte/strcmp|''strcmp'']] qui effectue cette comparaison.
==== Fonctions partiellement spécialisées ====
Dans l'exemple précédent, nous avions une fonction complétement spécialisée, l'ensemble des paramètres de type était défini. Nous pouvons définir des spécialisations partielles de fonctions.
Considérons le cas d'une fonction ''is_pointer'' qui doit retourner ''true'' si l'argument passé en paramètre est un pointeur et ''false'' dans tous les autres cas.
Le comportement par défaut de la fonction est de retourner ''false''.
template
bool is_pointer(T anItem) { return false; }
Si l'argument est de type pointeur, alors la fonction doit retournée ''true''. Nous pouvons introduire les deux spécialisations suivantes :
template
bool is_pointer(const T* anItem) { return true; }
template
bool is_pointer(T* anItem) { return true; }
Donc maintenant, que va-t-il se passer. Quand on va appeller la fonction avec un objet ayant un certain type, le compilateur va regarder si le type de l'argument est de type ''T*'' ou ''const T*'' et dans ce cas, il va appeller l'une des deux fonctions :
template
bool is_pointer(const T* anItem) { return true; }
template
bool is_pointer(T* anItem) { return true; }
et retournera le résultat ''true''. Sinon, il appellera la fonction générique non spécialisée et il retournera le résultat ''false''.
Au passage, ce calcul s'effectue uniquement à la compilation. C'est d'ailleurs pour cela que C++ a introduit avec la version 2020 l'annotation [[https://en.cppreference.com/w/cpp/language/consteval|''consteval'']] pour indiquer que le résultat de la fonction est calculable à la compilation et qu'il n'est pas besoin de générer de code pour l'exécution. On retrouve ici l'objectif de performance de C++. En C++ 20, nous écrirons :
template
consteval bool is_pointer(T anItem) { return false; }
template
consteval bool is_pointer(const T* anItem) { return true; }
template
consteval bool is_pointer(T* anItem) { return true; }
===== Pour aller plus loin =====
[[https://en.cppreference.com/w/cpp/language/function_template|Template functions]]
[[https://en.cppreference.com/w/cpp/language/template_parameters|Template parameters and template arguments]]
[[https://en.cppreference.com/w/cpp/language/template_argument_deduction|Template argument deduction]]
[[https://en.cppreference.com/w/cpp/language/constraints|Contraints & Concepts]]