Table of Contents

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<class valueT>
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 :

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<class valueT>
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.

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<iostream>
#include<string>
#include<cstring>
 
template<class valueT>
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<class valueT>
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 ''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<class T>
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<class T>
bool is_pointer(const T* anItem) { return true; }
template<class T>
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<class T>
bool is_pointer(const T* anItem) { return true; }
template<class T>
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 ''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<class T>
consteval bool is_pointer(T anItem) { return false; }
template<class T>
consteval bool is_pointer(const T* anItem) { return true; }
template<class T>
consteval bool is_pointer(T* anItem) { return true; }

Pour aller plus loin

Template functions

Template parameters and template arguments

Template argument deduction

Contraints & Concepts