Programmation C-C++/Les templates/Instanciation des template

Cours de C/C++
^
Les templates
Généralités
Déclaration des paramètres template
Fonctions et classes template
Instanciation des template
Spécialisation des template
Mot-clé typename
Fonctions exportées

Livre original de C. Casteyde

La définition des fonctions et des classes template ne génère aucun code tant que tous les paramètres template n'ont pas pris chacun une valeur spécifique. Il faut donc, lors de l'utilisation d'une fonction ou d'une classe template, fournir les valeurs pour tous les paramètres qui n'ont pas de valeur par défaut. Lorsque suffisamment de valeurs sont données, le code est généré pour ce jeu de valeurs. On appelle cette opération l'instanciation des template.

Plusieurs possibilités sont offertes pour parvenir à ce résultat : l'instanciation implicite et l'instanciation explicite.

Instanciation implicite modifier

L'instanciation implicite est utilisée par le compilateur lorsqu'il rencontre une expression qui utilise pour la première fois une fonction ou une classe template, et qu'il doit l'instancier pour continuer son travail. Le compilateur se base alors sur le contexte courant pour déterminer les types des paramètres template à utiliser. Si aucune ambiguïté n'a lieu, il génère le code pour ce jeu de paramètres.

La détermination des types des paramètres template peut se faire simplement, ou être déduite de l'expression à compiler. Par exemple, les fonctions membres template sont instanciées en fonction du type de leurs paramètres. Si l'on reprend l'exemple de la fonction template Min définie dans l'Exemple 12-4, c'est son utilisation directe qui provoque une instanciation implicite.

Exemple 12-10. Instanciation implicite de fonction template modifier

int i=Min(2,3);

Dans cet exemple, la fonction Min est appelée avec les paramètres 2 et 3. Comme ces entiers sont tous les deux de type int, la fonction template Min est instanciée pour le type int. Partout dans la définition de Min, le type générique T est donc remplacé par le type int.

Si l'on appelle une fonction template avec un jeu de paramètres qui provoque une ambiguïté, le compilateur signale une erreur. Cette erreur peut être levée en surchargeant la fonction template par une fonction qui accepte les mêmes paramètres. Par exemple, la fonction template Min ne peut pas être instanciée dans le code suivant :

int i=Min(2,3.0);

parce que le compilateur ne peut pas déterminer si le type générique T doit prendre la valeur int ou double. Il y a donc une erreur, sauf si une fonction Min(int, double) est définie quelque part. Pour résoudre ce type de problème, on devra spécifier manuellement les paramètres template de la fonction, lors de l'appel. Ainsi, la ligne précédente compile si on la réécrit comme suit :

int i=Min<int>(2,3.0);

dans cet exemple, le paramètre template est forcé à int, et 3.0 est converti en entier.

On prendra garde au fait que le compilateur utilise une politique minimaliste pour l'instanciation implicite des template. Cela signifie qu'il ne créera que le code nécessaire pour compiler l'expression qui exige une instanciation implicite. Par exemple, la définition d'un objet d'une classe template dont tous les types définis provoque l'instanciation de cette classe, mais la définition d'un pointeur sur cette classe ne le fait pas. L'instanciation aura lieu lorsqu'un déréférencement sera fait par l'intermédiaire de ce pointeur. De même, seules les fonctionnalités utilisées de la classe template seront effectivement définies dans le programme final.

Par exemple, dans le programme suivant :

#include <iostream>

using namespace std;

template <class T>
class A
{
public:
     void f(void);
     void g(void);
};

// Définition de la méthode A<T>::f() :
template <class T>
void A<T>::f(void)
{
     cout << "A<T>::f() appelée" << endl;
}

// On ne définit pas la méthode A<T>::g()...

int main(void)
{
     A<char> a;  // Instanciation de A<char>.
     a.f();       // Instanciation de A<char>::f().
     return 0;
}

seule la méthode f de la classe template A est instanciée, car c'est la seule méthode utilisée à cet endroit. Ce programme pourra donc parfaitement être compilé, même si la méthode g n'a pas été définie.

Instanciation explicite modifier

L'instanciation explicite des template est une technique permettant au programmeur de forcer l'instanciation des template dans son programme. Pour réaliser une instanciation explicite, il faut spécifier explicitement tous les paramètres template à utiliser. Cela se fait simplement en donnant la déclaration du template, précédée par le mot clé template :

template nom<valeur[, valeur[...]]>;

Par exemple, pour forcer l'instanciation d'une pile telle que celle définie dans l'Exemple 12-5, il faudra préciser le type des éléments entre crochets après le nom de la classe :

template Stack<int>;  // Instancie la classe Stack<int>.

Cette syntaxe peut être simplifiée pour les fonctions template, à condition que tous les paramètres template puissent être déduits par le compilateur des types des paramètres utilisés dans la déclaration de la fonction. Ainsi, il est possible de forcer l'instanciation de la fonction template Min de la manière suivante :

template int Min(int, int);

Dans cet exemple, la fonction template Min est instanciée pour le type int, puisque ses paramètres sont de ce type.

Lorsqu'une fonction ou une classe template a des valeurs par défaut pour ses paramètres template, il n'est pas nécessaire de donner une valeur pour ces paramètres. Si toutes les valeurs par défaut sont utilisées, la liste des valeurs peut être vide (mais les signes d'infériorité et de supériorité doivent malgré tout être présents).

Exemple 12-11. Instanciation explicite de classe template modifier

template<class T = char>
class Chaine;

template class Chaine<>;  // Instanciation explicite de Chaine<char>.

Problèmes soulevés par l'instanciation des template modifier

Les templates doivent impérativement être définis lors de leur instanciation pour que le compilateur puisse générer le code de l'instance. Cela signifie que les fichiers d'en-tête doivent contenir non seulement la déclaration, mais également la définition complète des template. Cela a plusieurs inconvénients. Le premier est bien entendu que l'on ne peut pas considérer les template comme les fonctions et les classes normales du langage, pour lesquels il est possible de séparer la déclaration de la définition dans des fichiers séparés. Le deuxième inconvénient est que les instances des template sont compilées plusieurs fois, ce qui diminue d'autant plus les performances des compilateurs. Enfin, ce qui est le plus grave, c'est que les instances des template sont en multiples exemplaires dans les fichiers objets générés par le compilateur, et accroissent donc la taille des fichiers exécutables à l'issue de l'édition de liens. Cela n'est pas gênant pour les petits programmes, mais peut devenir rédhibitoire pour les programmes assez gros.

Le premier problème n'est pas trop gênant, car il réduit le nombre de fichiers sources, ce qui n'est en général pas une mauvaise chose. Notez également que les template ne peuvent pas être considérés comme des fichiers sources classiques, puisque sans instanciation, ils ne génèrent aucun code machine (ce sont des classes de classes, ou « métaclasses »). Mais ce problème peut devenir ennuyant dans le cas de bibliothèques template écrites et vendues par des sociétés désireuses de conserver leur savoir-faire. Pour résoudre ce problème, le langage donne la possibilité d'exporter les définitions des template dans des fichiers complémentaires. Nous verrons la manière de procéder dans la Section 12.7.

À faire... 

localiser

Le deuxième problème peut être résolu avec l'exportation des template, ou par tout autre technique d'optimisation des compilateurs. Actuellement, la plupart des compilateurs sont capables de générer des fichiers d'en-tête précompilés, qui contiennent le résultat de l'analyse des fichiers d'en-tête déjà lus. Cette technique permet de diminuer considérablement les temps de compilation, mais nécessite souvent d'utiliser toujours le même fichier d'en-tête au début des fichiers sources.

Le troisième problème est en général résolu par des techniques variées, qui nécessitent des traitements complexes dans l'éditeur de liens ou le compilateur. La technique la plus simple, utilisée par la plupart des compilateurs actuels, passe par une modification de l'éditeur de liens pour qu'il regroupe les différentes instances des mêmes template. D'autres compilateurs, plus rares, gèrent une base de données dans laquelle les instances de template générées lors de la compilation sont stockées. Lors de l'édition de liens, les instances de cette base sont ajoutées à la ligne de commande de l'éditeur de liens afin de résoudre les symboles non définis. Enfin, certains compilateurs permettent de désactiver les instanciations implicites des template. Cela permet de laisser au programmeur la responsabilité de les instancier manuellement, à l'aide d'instanciations explicites. Ainsi, les template peuvent n'être définies que dans un seul fichier source, réservé à cet effet. Cette dernière solution est de loin la plus sûre, et il est donc recommandé d'écrire un tel fichier pour chaque programme.

Ce paragraphe vous a présenté trois des principaux problèmes soulevés par l'utilisation des template, ainsi que les solutions les plus courantes qui y ont été apportées. Il est vivement recommandé de consulter la documentation fournie avec l'environnement de développement utilisé, afin à la fois de réduire les temps de compilation et d'optimiser les exécutables générés.