Notions de patrons de fonctions et de classes
Nous avons vu que la surdéfinition de fonctions permettait de donner un nom unique à plusieurs fonctions réalisant un travail différent. La notion de “patron” de fonction (template en anglais) est à la fois plus puissante et plus restrictive; plus puissante car il suffit d’écrire une seule fois la définition d’une fonction pour que le compilateur puisse automatiquement l’adapter à n’importe quel type; plus restrictive car cela suppose alors que l’ensemble des fonctions ainsi définies corresponde à la même définition, donc au même algorithme. Dans ce cadre, les notions de surdéfinition de fonction et de patron de fonctions répondent chacune à un besoin différent.
Les patrons de fonction
Pour tenter d’illustrer l’intérêt des patrons de fonctions et leurs différences
vis-à-vis de la surdéfinition de fonction, considérons le cas scolaire de la
recherche du minimum de deux valeurs. Du point de vue de la surdéfinition de
fonction, il s’agit d’écrire une fonction min
pour chaque type de variable
rencontrée, soit
int min(const int a, const int b) { return a < b ? a : b; } float min(const float a, const float b) { return a < b ? a : b; } char min(const char a, const char b) { return a < b ? a : b; }
Selon le contexte c’est-à-dire suivant le type d’argument fourni lors de l’appel
de la fonction min
, le compilateur se chargera d’utiliser la fonction
adéquat. Toutefois, l’ensemble de ces définitions réalise la même opération et
d’autre part, il devient nécessaire à l’auteur de la fonction, de considérer
l’ensemble des types possibles et imaginables. Pour simplifer considérablement
les choses, le C++ permet l’utilisation de patrons de fonctions afin de
généraliser la définition de fonctions. Ainsi, l’ensemble des cas susceptibles
d’être rencontrés par la surdéfinition de fonctions peut se résumer à la
déclaration suivante
template<typename T> T min(const T & a, const T & b) { return a < b ? a : b; }
Seul l’entête a changé, le corps de la fonction étant nécessairement indépendant
du type de paramètres fournis1. Ainsi la précédente déclaration précise que
l’on est en présence d’un patron (template
) dans lequel apparaît un
“paramètre” de type désigné par la lettre T
. En d’autres termes, dans la
définition de la fonction min
, T
représente un type quelconque à la fois
type de retour de la fonction mais également argument de cette dernière.
Pour utiliser le patron min
précédemment crée, il suffit d’utiliser la
fonction min
dans des conditions appropriées (dans notre cas, en fournissant
deux arguments de même type). Le code suivant illustre quelques appels de cette
fonction
int main() { const int i1 = 2, i2 = 7; const float f1 = 3.4, f2 = 5.6; const char c1 = 'd', c2 = 'z'; cout << "min(" << i1 << "," << i2 << ") = " << min(i1,i2) << endl; cout << "min(" << f1 << "," << f2 << ") = " << min(f1,f2) << endl; cout << "min(" << c1 << "," << c2 << ") = " << min(c1,c2) << endl; cout << "min(&c1,&c2) = " << min(&c1,&c2) << endl; return 0; }
Dans le premier cas où les arguments sont de type int
, le compilateur
“enregistrera” i.e. fabriquera automatiquement la fonction min
dite fonction
patron correspondant à des arguments de type entier. De même, pour des arguments
de type float
ou un caractère.
S’il n’est plus nécessaire de définir une implémentation par type de données, le
compilateur doit cependant disposer de la définition du patron afin de
déterminer quelle fonction enregistrer. Aussi, la définition doit nécessairement
intervenir avant une quelconque utilisation de la fonction. D’autre part, cette
généralisation dans l’écriture de fonctions s’applique également aux classes
moyennant la surcharge des opérateurs mis en jeu (ici, l’opérateur <
). Il est
parfaitement envisageable d’appeler la fonction min
avec comme argument la
désormais célèbre classe point
, sous réserve que l’opérateur d’infériorité <
soit surchargé. Ainsi, le programme suivant demeure valable
class point { public: point(const unsigned int abs = 0, const unsigned int ord = 0) : m_x(abs), m_y(ord) {} unsigned int get_norme() const { return m_X*m_X+m_Y*m_Y; } private: unsigned int m_x; unsigned int m_y; }; bool operator<(const point & p1, const point & p2) { return p1.get_norme() < p2.get_norme(); } int main() { point myPoint1(3,4), myPoint2(5,2); point myPoint3 = std::min(myPoint1,myPoint2); return 0; }
Le couplage entre patron de fonctions et surcharge d’opérateur constitue donc un outil puissant qui permet d’obtenir une forme “d’abstraction” en s’affranchissant des problèmes de type.
Remarques
- Le mécanisme même des patrons fait que ces instructions sont utilisées par le
compilateur pour enregistrer chaque fois qu’il est nécessaire, les
instructions correspondant à la fonction requise. La définition de patron ne
peut donc intervenir dans un module objet (fichiers
.cc,.cpp
,…) indépendant de l’utilisation qui en sera faite. Dans la pratique, les définitions de patrons se situent donc dans un fichier d’entête de telle sorte à être “en ligne”2 Un patron de fonctions peut comporter un ou plusieurs paramètres de type, chacun devant être précédé du mot clé
class
:template<typename T, typename U...> void fct(T t, U u, ...)
Dans tous les cas, il est nécessaire que chaque paramètre de type apparaisse au moins une fois dans l’entête du patron.
Dans l’hypothèse où la fonction
min
est appelée avec des arguments de type différents (char
etint
, par exemple), il y aura une erreur de compilation du fait que le C++ impose une correspondance absolue des types. Il est cependant possible d’intervenir sur ce mécanisme d’identification de type, en imposant le type des arguments lors de l’appel de la fonction. Ainsi, les instructions suivantes sont toutes valablesunsigned int i1 = 2; int i2 = 3; char c1 = 'c'; // Force l'utilisation de min<int> en imposant la conversion // de c en int, le résultat étant une valeur entière cout << "min(" << c1 << "," << i2 << ") = " << min<int>(c1, i2) << endl; // Force l'utilisation de min<int> en imposant la conversion // de i1 en int, le résultat étant une valeur entière cout << "min(" << i1 << "," << i2 << ") = " << min<int>(i1, i2) << endl; // Force l'utilisation de min<char> en imposant la conversion // de i1 et i2 en char, le résultat étant un caractère cout << "min(" << i1 << "," << i2 << ") = " << min<char>(i1,i2) << endl;
Les patrons de classe
Le précédent paragraphe a montré comment C++ permettait, grâce à la notion de patron de fonction, de définir une famille de fonctions paramétrées par un ou plusieurs types. D’une manière comparable, C++ permet de définir des “patrons de classe” afin de définir une seule et unique fois la classe pour que le compilateur puisse automatiquement l’adapter à différents types. Ce mécanisme évite ainsi de définir plusieurs classes similaires pour décrire un même concept appliqué à plusieurs types de données différents. Cette notion est largement utilisée pour définir tous les types de “containers” (comme les listes, les tables, les piles, etc.), mais également d’algorithmes génériques tels que ceux de la bibliothèque standard.
La syntaxe permettant de définir un patron de classe est similaire à celle qui
permet de définir des patrons de fonctions. Un exemple de classe template,
portant sur la structure point
pour laquelle la précision de représentation
(entiers, entiers non signés, réels,…) est le paramètre type de la classe, est
template<typename T> class point { public: point(T abs = 0, T ord = 0) : m_x(abs), m_y(ord) {} void affiche(); private: T m_x; T m_y; };
Pour complèter la définition de notre patron de classe, il convient de définir
les méthodes. Selon que l’on souhaite définir la méthode en ligne (i.e. à
l’intérieur de la définition du patron de classe) ou non, la démarche est
sensiblement différente. Dans le cas de la définition en ligne telle le
constructeur point(T abs = 0, T ord = 0)
, l’utilisation demeure naturelle, la
seule contrainte tenant à l’emploi du paramètre de type T
. En revanche,
lorsque la méthode est définie en dehors, il est impératif de rappeler au
compilateur :
- que, dans la définition de cette fonction, vont apparaître des paramètres de
type. On fournit donc la liste de paramètre sous la forme
template<class T>
, - le nom du patron concerné. Par exemple, si nous définissons la méthode
affiche
, son nom serapoint<T>::affiche()
.
Ainsi, la méthode affiche
serait définie de la façon suivante
template<typename T> void point<T>::affiche() { cout << "Coordonnées : " << m_x << " " << m_y << endl; }
L’utilisation de patron de classe est similaire à celle des patrons de fonctions
à ceci près qu’il est nécessaire d’imposer le paramètre de type lors de
l’instanciation de la classe. Aussi après avoir crée le patron de classe
point
, la déclaration d’instances de point
est
point<int> myPointWithInteger; point<double> myPointWithDouble(3.2,4.5);
Les contraintes d’utilisation de patron de classe sont du même ordre que celles inhérentes à l’utilisation des patrons de fonctions : les recommendations issues du premier paragraphe de cette fiche sont donc également applicables aux patrons de classe. La principale d’entre elles tient à la définition de la classe et de ses méthodes qui est indispensable au compilateur pour enregistrer chaque fois que nécessaire les instructions requises. En pratique, on placera donc les définitions de patron dans un fichier d’entête approprié.
Il est également envisageable de fournir un nombre quelconque de paramètre de type dans la définition du patron de classe de même que des paramètres expressions. L’exemple ci-dessous illustre ces cas
template<typename T, typename U, unsigned int n> class tableau { T m_tab[n]; U m_mean; public: tableau() {} T& operator[](const unsigned int i) const { return m_tab[i]; } U get_mean() const { return m_mean; } }; int main() { tableau<int, float, 5> myTableau1; tableau<float, float, 10> myTableau2; tableau<point<int>, point<float>, 6> myTableau3; }
Par ailleurs, il n’est pas possible de surdéfinir un patron de classe c’est-à-dire de créer plusieurs patrons de même nom mais comportant une liste de paramètres (de type ou d’expression) différent et ce contrairement aux patrons de fonctions. En conséquence, les ambiguïtés évoquées lors de l’instanciation d’une classe fonction ne peuvent plus se poser dans le cas de l’instanciation d’une classe patron.
Notes :
dans le cadre des patrons de fonctions et de classes, on parle communément de paramètres et non d’argument.
les compilateurs récents permettent l’usage du mot clé export
lors de
la définition d’un patron export template<class T> T min(T a, T b) {...}
. On
peut alors utiliser ce patron depuis un autre fichier source, en se contentant
de mentionner sa “déclaration”.