Rechercher
 

Définition de fonctions templates en C++

Une fonction générique (une fonction template) est en fait un modèle de code permettant de produire des fonctions pour divers types de données. Tant que ce modèle de code n'est pas utilisé dans votre programme, aucune fonction (relative à ce template) ne sera produite dans l'exécutable (ou la librairie) résultant de la compilation. Par contre à chaque nouvel appel, et en fonction des types passés en paramètres, le compilateur pourra produire la fonction requise.

Attention, comme votre fonction est générique, vous ne devez pas faire de présupposition sur les types de données qui seront utilisés par la suite : il est donc important de réfléchir dans le pire des cas. Par exemple, vous ne connaissez pas à l'avance la taille des types de données qui seront utilisés. Il en résulte qu'il est important de passer les paramètres par références : ainsi on limitera le nombre d'octets qui seront empiler sur la pile d'exécution (la stack) durant l'appel.

Le « Hello World » de la fonction template consiste à définir une fonction de calcul de minimum : effectivement, l'algorithme de ce calcul est indépendant du type de données associé au deux valeurs à comparer. Il peut donc s'avérer intéressant de définir un template afin de limiter le nombre de lignes de code à maintenir. Pour faciliter la réutilisation de ce template, nous pouvons le définir dans un fichier d'entête nommé "Utility.h".

#ifndef __UTILITY_H__
#define __UTILITY_H__

template <typename T>
inline const  T & Mini( const T & a, const T & b ) {
    return a < b ? a : b;
} 

#endif

Note : il est aujourd'hui conseillé d'utiliser le mot clé typename. Pour autant, dans des versions anciennes de C++ on utilisait en lieu et place de typename le mot clé class. Pour des histoires de compatibilité, cette possibilité reste supportée à ce jour. Si vous rencontrez de définition de templates utilisant le mot clé class, n'imaginez pas que T ne peut être remplacé que par une classe : ce n'est pas le cas et des types primitifs (int, double, ...) peuvent bien entendu être utilisés.

Note : un template ne définissant pas directement de code et le code étant produit lors de l'utilisation de ce template, il est nécessaire de le définir, avec son corps, dans un fichier d'entête (.h). Si vous placer le corps du template dans un fichier .cpp, il en résultera très certainement des erreurs de compilation.

Note : bien qu'un template se définisse uniquement dans un fichier .h, il ne sera pas pour autant « inline ». Si vous souhaitez en plus optimiser votre code en rendant les fonctions produites par le template inlines, vous pouvez alors cumuler les mots clés template et inline.

Pour que le template puisse s'appliquer et produire une fonction, il faudra que le(s) type(s) utilisé(s) fournisse(nt) les opérateurs ou les méthodes requises. Par exemple, dans notre cas, nous pourrons appliquer le template Mini qu'à des types supportant un opérateur d'infériorité (<).

#include <iostream>
#include <string>

#include "Utility.h"

using namespace std;


int main( int argc, char * argv[] ) {

    double d1 = 3.6;
    double d2 = 1.2;
    double dRes = Mini( d1, d2 );
    
    cout << "dRes == " << dRes << endl;

    int i1 = 3;
    int i2 = 1;
    int iRes = Mini( i1, i2 );
    
    cout << "iRes == " << iRes << endl;

    string s1 = "qwerty";
    string s2 = "azerty";
    string sRes = Mini( s1, s2 );
    
    cout << "sRes == " << sRes << endl;

    
    return 0;
}

Attention, si vous utilisez deux paramètres proches mais néanmoins de types différents (par exemple int et double), une erreur de compilation sera produite. Il devient alors nécessaire de fixer le type associé à T au niveau de l'appel de votre template. En voici un petit exemple.

#include <iostream>

#include "Utility.h"

using namespace std;


int main( int argc, char * argv[] ) {

    // double d = Mini( 3, 4.1 );    // ne compile pas
    double d = Mini<double>( 3, 4.1 );    // Compile parfaitement
    
    return 0;
}

Un dernier point : méfiez-vous des fonctions génériques. En définissant un tel élément vous dites au compilateur de fonctionner avec n'importe quel type qui répondra aux exigences requises (ici l'opérateur <). Un pointeur pouvant être comparé, il est donc possible d'appeler notre modèle de fonction avec des pointeurs : dans ce cas, c'est le pointeur ayant la plus basse adresse en mémoire qui sera retourné. Que pensez-vous de l'exemple suivant ? Amusez-vous à permuter les deux lignes qui déclarent les variables s1 et s2. Notez-bien ce piège.

#include <iostream>

#include "Utility.h"

using namespace std;


int main( int argc, char * argv[] ) {

    const char * s1 = "toto";      // Swap these two lines
    const char * s2 = "titi";      // and view the results
    
    cout << Mini( s1, s2 ) << endl;
    
    return 0;
}

Dans ce cas, vous pouvez définir une fonction spécifique pour le type const char * : en cas de présence d'un template et d'une fonction pouvant tous les deux être utilisés, ce sera toujours la fonction (plus spécifique) qui s'appliquera. Modifiez comme ci-dessous votre fichier "Utility.h" et relancez votre main. Constatez la différence.

#ifndef __UTILITY_H__
#define __UTILITY_H__

#include <cstring>

template <typename T>
inline const  T & Mini( const T & a, const T & b ) {
    return a < b ? a : b;
} 

inline const char * Mini( const char * s1, const char * s2 ) {
    return strcmp( s1, s2 ) < 0 ? s1 : s2;
} 

#endif