Pointeurs, références & allocation dynamique Rappels sur les fonctions Les spécificités du C++ Structures et classes Encapsulation des données Notions de constructeur et de destructeur Fonctions et classes amies Surcharge d'opérateur Héritage Notions de patrons de fonctions et de classes Introduction à la librairie standard STL
Compilation et directives de préprocesseur Convention d'écriture et organisation des programmes Écriture/lecture sur l'entrée/sortie standard Les membres données statiques Utilisation de enum et de typedef
Retour menu principal

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 et int, 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 valables

    unsigned 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 sera point<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 :

1

dans le cadre des patrons de fonctions et de classes, on parle communément de paramètres et non d’argument.

2

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”.