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é #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
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :