2164 votes

Pourquoi les modèles ne peuvent-ils être mis en œuvre que dans le fichier d'en-tête ?

Citation de La bibliothèque standard C++ : un tutoriel et un manuel :

La seule façon portable d'utiliser les modèles pour le moment est de les implémenter dans les fichiers d'en-tête en utilisant des fonctions en ligne.

Comment cela se fait-il ?

(Précision : les fichiers d'en-tête ne sont pas des fichiers seulement solution portable. Mais c'est la solution portable la plus pratique).

27 votes

S'il est vrai que placer toutes les définitions des fonctions de template dans le fichier d'en-tête est probablement la façon la plus pratique de les utiliser, on ne voit toujours pas clairement ce que fait "inline" dans cette citation. Il n'est pas nécessaire d'utiliser des fonctions en ligne pour cela. "Inline" n'a absolument rien à voir avec cela.

15 votes

Le livre est périmé.

16 votes

Un modèle n'est pas comme une fonction qui peut être compilée en code d'octets. Il s'agit simplement d'un modèle permettant de générer une telle fonction. Si vous placez un modèle seul dans un fichier *.cpp, il n'y a rien à compiler. De plus, l'instanciation explicite n'est en fait pas un modèle, mais le point de départ de la création d'une fonction à partir du modèle, qui se retrouve dans le fichier *.obj.

1899voto

Luc Touraille Points 29252

Mise en garde : il est no nécessaire de placer l'implémentation dans le fichier d'en-tête, voir la solution alternative à la fin de cette réponse.

Quoi qu'il en soit, la raison pour laquelle votre code échoue est que, lors de l'instanciation d'un modèle, le compilateur crée une nouvelle classe avec l'argument de modèle donné. Par exemple :

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

En lisant cette ligne, le compilateur créera une nouvelle classe (appelons-la FooInt ), ce qui est équivalent à ce qui suit :

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

Par conséquent, le compilateur doit avoir accès à l'implémentation des méthodes, pour les instancier avec l'argument du modèle (dans le cas présent int ). Si ces implémentations ne se trouvaient pas dans l'en-tête, elles ne seraient pas accessibles et le compilateur ne pourrait donc pas instancier le modèle.

Une solution courante consiste à écrire la déclaration du modèle dans un fichier d'en-tête, puis à implémenter la classe dans un fichier d'implémentation (par exemple .tpp), et à inclure ce fichier d'implémentation à la fin de l'en-tête.

Foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

De cette manière, la mise en œuvre est toujours séparée de la déclaration, mais elle est accessible au compilateur.

Solution alternative

Une autre solution consiste à séparer l'implémentation et à instancier explicitement toutes les instances de modèles dont vous aurez besoin :

Foo.h

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Si mon explication n'est pas assez claire, vous pouvez jeter un coup d'œil sur le site web de la Commission européenne. Super-FAQ C++ sur ce sujet .

118 votes

En fait, l'instanciation explicite doit se faire dans un fichier .cpp qui a accès aux définitions de toutes les fonctions membres de Foo, plutôt que dans l'en-tête.

17 votes

"le compilateur doit avoir accès à l'implémentation des méthodes, pour les instancier avec l'argument du modèle (dans ce cas en

43 votes

I

336voto

Ben Points 22160

Cela s'explique par la nécessité d'une compilation séparée et par le fait que les modèles sont un polymorphisme de type instanciation.

Rapprochons-nous un peu du concret pour trouver une explication. Disons que j'ai les fichiers suivants :

  • foo.h
    • déclare l'interface de class MyClass<T>
  • foo.cpp
    • définit la mise en œuvre de class MyClass<T>
  • bar.cpp
    • utilise MyClass<int>

La compilation séparée signifie que je devrais pouvoir compiler foo.cpp indépendamment de bar.cpp . Le compilateur effectue tout le travail d'analyse, d'optimisation et de génération de code sur chaque unité de compilation de manière totalement indépendante ; nous n'avons pas besoin d'effectuer une analyse de l'ensemble du programme. Seul l'éditeur de liens doit traiter l'ensemble du programme en une seule fois, et son travail est nettement plus facile.

bar.cpp n'a même pas besoin d'exister lorsque je compile foo.cpp mais je devrais toujours être en mesure d'établir un lien entre le foo.o Je l'avais déjà fait avec le bar.o Je viens juste de produire, sans avoir besoin de recompiler foo.cpp . foo.cpp pourrait même être compilée dans une bibliothèque dynamique, distribuée ailleurs sans que l'on ait besoin de l'aide d'un tiers. foo.cpp et lié au code qu'ils écrivent des années après que j'ai écrit foo.cpp .

Le "polymorphisme de type instanciation" signifie que le modèle MyClass<T> n'est pas vraiment une classe générique qui peut être compilée en un code fonctionnant pour n'importe quelle valeur de T . Cela ajouterait des frais généraux tels que la mise en boîte, la nécessité de passer des pointeurs de fonction aux allocateurs et aux constructeurs, etc. L'objectif des modèles C++ est d'éviter d'avoir à rédiger des fichiers class MyClass_int , class MyClass_float etc., mais pour pouvoir obtenir un code compilé qui soit pratiquement identique à celui que nous aurions obtenu si nous l'avions compilé. avait écrit chaque version séparément. Un modèle est donc littéralement un modèle ; un modèle de classe est no une classe, c'est une recette pour créer une nouvelle classe pour chaque T que nous rencontrons. Un modèle ne peut pas être compilé en code, seul le résultat de l'instanciation du modèle peut être compilé.

Ainsi, lorsque foo.cpp est compilé, le compilateur ne peut pas voir bar.cpp de savoir que MyClass<int> est nécessaire. Il peut voir le modèle MyClass<T> mais il ne peut pas émettre de code pour cela (c'est un modèle, pas une classe). Et quand bar.cpp est compilé, le compilateur peut voir qu'il a besoin de créer un fichier MyClass<int> mais il ne peut pas voir le modèle MyClass<T> (seule son interface en foo.h ), il ne peut donc pas le créer.

Si foo.cpp utilise lui-même MyClass<int> le code correspondant sera généré lors de la compilation. foo.cpp Ainsi, lorsque bar.o est lié à foo.o ils peuvent être branchés et fonctionneront. Nous pouvons utiliser ce fait pour permettre à un ensemble fini d'instanciations de modèles d'être implémentées dans un fichier .cpp en écrivant un seul modèle. Mais il n'y a aucun moyen pour bar.cpp pour utiliser le modèle comme modèle et l'instancier sur les types qu'il souhaite ; il ne peut utiliser que des versions préexistantes de la classe modèle que l'auteur de foo.cpp de l'idée de fournir.

On pourrait penser que lors de la compilation d'un modèle, le compilateur devrait "générer toutes les versions", celles qui ne sont jamais utilisées étant filtrées lors de l'édition des liens. Outre l'énorme surcharge et les difficultés extrêmes auxquelles une telle approche serait confrontée, étant donné que les "modificateurs de type" tels que les pointeurs et les tableaux permettent même aux types intégrés de donner naissance à un nombre infini de types, que se passe-t-il lorsque j'étend mon programme en ajoutant :

  • baz.cpp
    • déclare et met en œuvre class BazPrivate et utilise MyClass<BazPrivate>

Il n'y a aucune chance que cela fonctionne, à moins de

  1. Il faut recompiler foo.cpp chaque fois que nous changeons tout autre fichier du programme au cas où il ajouterait une nouvelle instanciation de MyClass<T>
  2. Exiger que baz.cpp contient (éventuellement par le biais de l'en-tête) le modèle complet de MyClass<T> afin que le compilateur puisse générer MyClass<BazPrivate> lors de la compilation des baz.cpp .

Personne n'aime (1), parce que les systèmes de compilation par analyse de programmes entiers nécessitent un temps d'adaptation important. pour toujours et parce qu'il est impossible de distribuer des bibliothèques compilées sans le code source. Nous avons donc (2) à la place.

76 votes

Citation accentuée un modèle est littéralement un modèle ; un modèle de classe n'est pas une classe, c'est une recette pour créer une nouvelle classe pour chaque T que nous rencontrons

0 votes

J'aimerais savoir s'il est possible d'effectuer les instanciations explicites ailleurs que dans l'en-tête ou le fichier source de la classe ? Par exemple, les faire dans main.cpp ?

1 votes

@Birger Vous devriez pouvoir le faire à partir de n'importe quel fichier qui a accès à l'implémentation complète du modèle (soit parce qu'il est dans le même fichier, soit par le biais d'inclusions d'en-tête).

271voto

MaHuJa Points 890

Il y a beaucoup de bonnes réponses ici, mais je voulais ajouter ceci (pour être complet) :

Si, au bas du fichier d'implémentation cpp, vous instanciez explicitement tous les types avec lesquels le modèle sera utilisé, l'éditeur de liens pourra les trouver comme d'habitude.

Edit : Ajout d'un exemple d'instanciation explicite d'un modèle. Utilisé après que le modèle a été défini et que toutes les fonctions membres ont été définies.

template class vector<int>;

Cela instanciera (et donc mettra à la disposition de l'éditeur de liens) la classe et toutes ses fonctions membres (uniquement). Une syntaxe similaire fonctionne pour les modèles de fonctions, donc si vous avez des surcharges d'opérateurs non membres, vous pouvez avoir besoin de faire la même chose pour eux.

L'exemple ci-dessus est assez inutile puisque vector est entièrement défini dans les en-têtes, sauf lorsqu'un fichier d'inclusion commun (en-tête précompilé ?) utilise extern template class vector<int> afin d'éviter qu'il ne l'instancie dans toutes les o (1000 ?) qui utilisent le vecteur.

75 votes

Ugh. Bonne réponse, mais pas de solution vraiment propre. L'énumération de tous les types possibles pour un modèle ne semble pas aller dans le sens de ce qu'un modèle est censé être.

10 votes

Cela peut être une bonne chose dans de nombreux cas, mais cela va généralement à l'encontre de l'objectif du modèle, qui est de vous permettre d'utiliser la classe avec n'importe quelle type sans les lister manuellement.

11 votes

vector n'est pas un bon exemple car un conteneur cible intrinsèquement "tous" les types. Mais il arrive très fréquemment que vous créiez des modèles qui ne sont destinés qu'à un ensemble spécifique de types, par exemple les types numériques : int8_t, int16_t, int32_t, uint8_t, uint16_t, etc. Dans ce cas, il est toujours judicieux d'utiliser un modèle, mais les instancier explicitement pour l'ensemble des types est également possible et, à mon avis, recommandé.

94voto

David Hanak Points 5960

Les modèles doivent être instancié par le compilateur avant de les compiler en code objet. Cette instanciation n'est possible que si les arguments du modèle sont connus. Imaginons maintenant un scénario dans lequel une fonction template est déclarée en a.h définie dans a.cpp et utilisé dans b.cpp . Quand a.cpp est compilé, on ne sait pas nécessairement que la prochaine compilation b.cpp nécessitera une instance du modèle, sans parler de l'instance spécifique. Pour les fichiers d'en-tête et les fichiers source, la situation peut rapidement devenir plus compliquée.

On peut arguer que les compilateurs peuvent être rendus plus intelligents pour "anticiper" toutes les utilisations du modèle, mais je suis sûr qu'il ne serait pas difficile de créer des scénarios récursifs ou autrement compliqués. AFAIK, les compilateurs ne font pas de telles recherches. Comme Anton l'a souligné, certains compilateurs supportent les déclarations explicites d'exportation des instanciations de modèles, mais pas tous (encore ?).

1 votes

"L'exportation est une norme, mais elle est difficile à mettre en œuvre et la plupart des équipes de compilateurs ne l'ont pas encore fait.

6 votes

L'exportation n'élimine pas la nécessité de divulguer les sources, ni ne réduit les dépendances de compilation, tout en exigeant un effort massif de la part des concepteurs de compilateurs. C'est pourquoi Herb Sutter lui-même a demandé aux concepteurs de compilateurs d'oublier l'exportation. L'investissement en temps nécessaire serait mieux utilisé ailleurs...

2 votes

Je ne pense donc pas que l'exportation ne soit pas encore mise en œuvre. Cela ne sera probablement jamais fait par quelqu'un d'autre qu'EDG après que les autres aient vu combien de temps cela a pris et combien peu a été gagné.

74voto

DevSolar Points 18897

En fait, avant C++11, la norme définissait l'élément export mot-clé qui serait permettent de déclarer des modèles dans un fichier d'en-tête et de les implémenter ailleurs. En quelque sorte. Pas vraiment, car les seuls qui ont jamais mis en œuvre Cette caractéristique a souligné :

Avantage fantôme n° 1 : cacher le code source. De nombreux utilisateurs ont déclaré qu'ils s'attendaient à ce que l'utilisation de l'exportation leur permette de ne plus avoir à expédier le code source. n'auront plus à fournir de définitions pour les modèles de fonctions membres/non-membres et les fonctions membres des modèles de classes. modèles. Ce n'est pas le cas. Avec l'exportation, les auteurs de bibliothèques doivent toujours envoyer le code source complet du modèle ou son équivalent direct (par exemple, un modèle de classe). (par exemple, un arbre d'analyse spécifique au système) parce que l'information complète est nécessaire pour l'instanciation. [...]

Avantage Phantom #2 : Constructions rapides, dépendances réduites. De nombreux utilisateurs s'attendent à ce que l'exportation permette de véritables des modèles en code objet, ce qui, selon eux, devrait permettre des compilations plus rapides. Ce n'est pas le cas, car la compilation des modèles exportés est en effet séparée, mais pas en code objet. Au lieu de cela, l'exportation rend presque toujours plus lentes, parce qu'au moins la même quantité de travail de compilation doit encore être effectuée au moment de la préliaison. L'exportation ne réduit même pas les dépendances entre les définitions de modèles, car ces dépendances sont intrinsèques, indépendantes de l'organisation des fichiers.

Aucun des compilateurs les plus répandus n'a implémenté ce mot-clé. La seule implémentation de cette fonctionnalité se trouve dans le frontend écrit par le Edison Design Group, qui est utilisé par le compilateur Comeau C++. Tous les autres compilateurs vous obligent à écrire des modèles dans les fichiers d'en-tête, car le compilateur a besoin de la définition du modèle pour une instanciation correcte (comme d'autres l'ont déjà souligné).

En conséquence, le comité de normalisation ISO C++ a décidé de supprimer l'élément export des modèles avec C++11.

9 votes

...et quelques années plus tard, je enfin a compris ce que export aurait en fait donné et je suis maintenant tout à fait d'accord avec les gens du GDE : Il ne nous aurait pas apporté ce que la plupart des gens (y compris moi-même en 2001) ont obtenu. penser il le ferait, et la norme C++ s'en porte mieux.

6 votes

@DevSolar : ce document est politique, répétitif et mal écrit. ce n'est pas de la prose d'un niveau standard habituel. Il est incroyablement long et ennuyeux, disant en gros trois fois la même chose sur des dizaines de pages. Mais je suis maintenant informé que l'exportation n'est pas l'exportation. C'est une bonne information !

1 votes

@v.oddou : Un bon développeur et un bon rédacteur technique sont deux compétences distinctes. Certains peuvent faire les deux, beaucoup ne le peuvent pas.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X