page perso
Programmation
Précompilation de templates
Pré-compiler un template serait un avantage pour accélérer les temps de compilation des projets, mais cela semble actuellement difficile à réaliser. Il existe pourtant une méthode complètement standard qui permet d'approcher cet idéal.
Je commence par rappeler rapidement le principe d'utilisation habituel des templates, puis j'explique comment organiser le code pour pouvoir faire de la précompilation.

Un template doit normalement être défini dans le fichier d'en-tête où il est déclaré.


Rappel:
  • Déclaration: on informe le compilateur de l'existence d'une variable, d'une constante ou d'une fonction, en lui donnant son nom et son type. On le fait traditionnellement dans les fichiers d'en-tête.
  • Définition: pour une fonction, on écrit le code; pour une variable, on réserve sa place.
Exemple :
toto.htoto.cpp
//PI est définie ailleurs;
//c'est à l'édition de liens
//qu'on va la chercher.
extern const float PI;

//pas besoin de spécifier
//ni le code, ni le nom
// des arguments
int max(int,int);
#include "toto.h" //ne pas oublier!
//PI sera écrite "en dur" dans un emplacement
//réservé du fichier compilé
const float PI = 3.1415927;

//voilà ce que fait la fonction
int max(int x, int y)
{
  return (x>y) ? x : y;
}

À part dans le cas d'un petit template utilitaire défini dans un fichier *.cpp pour un usage restreint, il est plus courant de déclarer les templates dans un fichier d'en-tête. Dans ce cas, il est obligatoire d'écrire le code dans ce même fichier d'en-tête, sinon le compilateur ne pourra pas réaliser l'instanciation avec l'argument template requis. Voyez l'exemple suivant:

//utilitaires.h

template <typename T> T max(T x, T y);
//utilitaires.cpp

#include "utilitaires.h"

template <typename T> T max(T x, T y)
{
  return (x>y) ? x : y;
}
//toto.cpp
#include "utilitaires.h"
void f(int x, int y)
{
  cout << max(x,y) << endl;
}
Compilation OKCompilation OK
Edition de liens : ERREUR dans toto.cpp :
où est le code de template <typename T> max(T,T) avec T=int ?

Que s'est-il passé ? Le compilateur ne peut pas réellement écrire un code générique pour le template. Au contraire, quand il a besoin d'une version spécifique de la fonction, (ici avec T=int), il la réecrit intégralement en remplaçant T par sa valeur (ici int).
A la limite, si un template n'est jamais instancié, il pourrait contenir des erreurs.

//toto.cpp

#include <iostream>
using namespace std;

template <typename T>
T max(T x, T y)
{
  gabu;//syntaxe correcte,
  zo; //mais cela ne veut
  meuh; //strictement rien dire.
}

int main(int, char**)
{
  return 0;
}
Compilation, édition de liens et exécution OK (sur certains compilateurs)


Normalement, un template doit être inline

Un template, que ce soit une fonction ou une classe, doit généralement être déclaré inline.
Prenons l'exemple suivant, qui semble ne pas poser de problèmes : la fonction template "max" est utilisée dans plusieurs fichiers source
//utilitaires.h
template <typename T> T max(T x, Ty)
{
  return (x>y) ? x : y;
}
//toto1.cpp
#include "utilitaires.h"

int f(int x, int y)
{
  cout << max(x,y) << endl;
}
//toto2.cpp
#include "utilitaires.h"

int g(float x, float y)
{
  cout << max(x,y) << endl;
}
Compilation OKCompilation OK
Edition de liens OK


En revanche, mettre des <int> à la place des <float> dans la fonction g pose problème. Cela peut paraître surprenant mais c'est pourtant tout-à-fait logique.

//utilitaires.h
template <typename T> T max(T x, Ty)
{
  return (x>y) ? x : y;
}
//toto1.cpp
#include "utilitaires.h"

int f(int x, int y)
{
  cout << max(x,y) << endl;
}
//toto2.cpp
#include "utilitaires.h"

int g(int x, int y)
{
  cout << max(x,y) << endl;
}
Compilation OKCompilation OK
Edition de liens : ERREUR : "multiple symbol int max(int,int)"


Que s'est-il passé ? Dans le premier cas, l'instanciation du template a été faite pour deux types différents, tantôt int, tantôt float. La phase de pé-processing chargée d'instancier les templates nécessaires a donc généré deux fonctions "max", une version int max(int,int) et une version float max(float,float). Ces deux versions n'ont pas la même signature, ce qui signifie que le compilateur sait les différencier.
En revanche, dans le cas n°2, la fonction int max(int,int) existe deux fois, et le compilateur ne peut pas deviner que le code est le même, puisqu'il a été; généré dans deux fichiers indépendants (toto1.cpp et toto2.cpp). La compilation séparée est donc correcte, mais l'édition de liens problématique. D'où l'erreur "symbole multiple".
Pour résoudre le problème, la solution est de déclarer le template inline. "max" n'est alors plus une fonction mais une sorte de macro, remplacée par le bloc de code qui la définit. Dans ce cas, il n'y a pas de lien à établir lors de l'édition de lien, et tout rentre dans l'ordre.

Petit rappel sur sur la signature des fonctions:

Un compilateur C++ refuse de compiler s'il détecte deux fonctions de même nom et de même signature. Il dit qu'il y a ambiguïté. La signature d'une fonction correspond en fait au prototype : nombre d'arguments, type d'arguments, attributs divers (static, const...) En C, le problème ne se pose pas, puisque la surcharge de fonction n'y est pas permise. (Deux fonctions ne peuvent avoir le même nom). En revanche, en C++, il faut tenir compte du contexte pour comprendre à quelle fonction portant ce nom on fait appel. Parfois, le contexte est insuffisant et le compilateur s'arrête sur une erreur de type "ambiguïté.

Comment pré-compiler un template:

Nous venons de voir que l'utilisation des templates impose habituellement non seulement d'écrire du code (parfois beaucoup) dans le fichier d'en-tête, ce qui alourdit considérablement les temps de compilation, mais en plus nécessite l'inlining de ce code, ce qui peut augmenter énormément la taille des exécutables générés.
Les compilateurs apportent plus ou moins de solutions à ces problèmes:
  • Dans le standard du C++, le mot-clé export, placé devant la déclaration d'un template, est censé permettre d'utiliser ce dernier comme une fonction normale, c'est-à-dire que l'on peut déporter le code dans un fichier *.cpp. Le compilateur doit se débrouiller pour que cela fonctionne. Malheureusement, à l'heure où j'écris ces lignes (juillet 2003), un seul compilateur au monde (à ma connaissance) sait implémenter ce système, le compilateur payant Comeau C++. On est donc loin du standard.
  • Les compilateurs sachant faire des headers pré-compilés, comme Borland C++ ou GCC depuis sa version 3.4, peuvent compenser la taille des headers par une pré-compilation "faite maison", sur laquelle on n'a donc pas de contrôle, et qui ne correspond à aucun standard non plus.
Il existe cependant une technique très simple, certes moins souple que le mot-clé (pour l'instant utopique) export, pour précompiler les templates sans s'éloigner un seul instant du standard. L'idée est la suivante : il suffit d'instancier uniquement les templates dont on a besoin dans un fichier *.cpp particulier. Considérez l'exemple suivant (le code est volontairement simplifié; j'ai omis les gardes de compilations à base de #ifndef, et le fichier utilitaires.cpp n'est pas très propre.)

//utilitaires.h

//déclaration uniquement
template <typename T>
T max(T,T);
//utilitaires.hxx

//définition uniquement
#include "utilitaires.h"
template <typename T>
T max(T x,T y)
{
  return (x>y) ? x : y;
}
//main.cpp

//on n'inclut que les déclarations
#include "utilitaires.h"

int main(int, char**)
{
  float x = 1;
  float y = 2;
  cout << max(x,y) << endl;
  return 0;
}
//utilitaires.cpp

//instanciations uniquement
#include "utilitaires.hxx"

//on force l'instanciation
// de float max(float,float)

float toto = max<float>(0,0);
Compilation OKCompilation OK
Edition de liens OK


Dans cet exemple, main.cpp n'a besoin de connaître que la déclaration du template. En effet, comme on savait que seule la version <float> était nécessaire, on a forcé son instanciation dans utilitaires.cpp. Ainsi, l'édition de liens sait résoudre l'appel à float max(float,float) dans le main.
Les avantages de cette technique sont immédiats:
  • La relecture du fichier d'en-tête est considérablement allégée;
  • le template n'a plus besoin d'être inline;
  • et surtout, le code du template n'est compilé qu'une seule fois, dans le fichier qui force son instanciation (ici utilitaires.cpp) !


Évidemment, ce tour de passe-passe se paye par une limitation inévitable: comme il faut forcer l'instanciation de template pour tous les types dont on aura besoin, il faut savoir à l'avance quels sont ces types, ce qui prive l'utilisateur de ce template pour tous les types qu'il aura définit lui-même, et qui aurait pu y être appliqués. On ne peut donc pas appliquer cette technique pour toutes les situations, notamment si l'on crée un conteneur template, cas où justement il aurait été appréciable de pouvoir précompiler le code.
Par contre, cette technique s'avère au contraire idéale si vous devez créer une bibliothèque template et que vous voulez empêcher l'utilisateur de faire n'importe quoi en appelant les fonctions pour des types non autorisés.
Enfin, pensez qu'il est possible de mixer les deux approches pour contenter des utilisateurs lambdas et des utilisateurs avancés. Pour cela, inversez les rôles des fichiers *.h et *.hxx. L'utilisateur de base pensera à inclure le fichier *.h et ne bénéficiera d'aucun changement par rapport à d'habitude; quant à l'utilisateur avancé, s'il est avisé de l'existence du couple de pré-compilation (*.hxx, *.cpp), il pourra les utiliser à sa guise.
 
 

PHP MySQL Valid CSS!