web-dev-qa-db-fra.com

Pourquoi les modèles ne peuvent-ils être implémentés que dans le fichier d'en-tête?

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

Pour le moment, le seul moyen d'utiliser des modèles consiste à les implémenter dans des fichiers d'en-tête à l'aide de fonctions inline.

Pourquoi est-ce?

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

1618
MainID

Il est non nécessaire pour mettre 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; 

Lors de la lecture de cette ligne, le compilateur créera une nouvelle classe (appelons-la FooInt), ce qui équivaut à 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 de modèle (dans ce cas, int). Si ces implémentations ne figuraient 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 de 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 façon, l'implémentation est toujours séparée de la déclaration, mais est accessible au compilateur.

Une autre solution consiste à garder l'implémentation séparée 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 consulter la Super-FAQ C++ à ce sujet .

1415
Luc Touraille

Beaucoup de réponses correctes ici, mais je voulais ajouter ceci (pour être complet):

Si, au bas du fichier cpp d'implémentation, effectuez une instanciation explicite de tous les types avec lesquels le modèle sera utilisé, l'éditeur de liens sera en mesure de les trouver comme d'habitude.

Edit: Ajout d'un exemple d'instanciation de modèle explicite. 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 mettra ainsi à la disposition de l'éditeur de liens) la classe et toutes ses fonctions membres (uniquement). Une syntaxe similaire fonctionne pour les fonctions de modèle. Par conséquent, si vous avez des surcharges d'opérateurs non membres, vous devrez peut-être faire de même pour celles-ci.

L'exemple ci-dessus est plutôt inutile car le vecteur est entièrement défini dans les en-têtes, sauf lorsqu'un fichier include commun (en-tête précompilé?) Utilise extern template class vector<int> pour l'empêcher de l'instancier dans tous les autre (1000?) Fichiers qui utilisent des vecteurs.

230
MaHuJa

C'est à cause de la nécessité d'une compilation séparée et parce que les modèles sont un polymorphisme de type instanciation.

Permet de nous rapprocher un peu du concret pour une explication. Dites que j'ai les fichiers suivants:

  • foo.h
    • déclare l'interface de class MyClass<T>
  • foo.cpp
    • définit l'implémentation de class MyClass<T>
  • bar.cpp
    • utilise MyClass<int>

Une 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 en toute indépendance; nous n'avons pas besoin d'analyser l'ensemble du programme. Seul l'éditeur de liens doit gérer l'ensemble du programme en même temps, et son travail est considérablement plus facile.

bar.cpp n'a même pas besoin d'exister lorsque je compile foo.cpp, mais je devrais quand même pouvoir lier le foo.o J'ai déjà eu avec le bar.o Je viens de produire, sans avoir besoin de recompiler foo.cpp. foo.cpp pourrait même être compilé dans une bibliothèque dynamique, distribuée ailleurs sans foo.cpp, et lié au code qu'ils écrivent des années après que j'ai écrit foo. cpp.

"Polymorphisme de type instanciation" signifie que le modèle MyClass<T> n'est pas vraiment une classe générique pouvant être compilée dans un code pouvant fonctionner pour toute valeur de T. Cela ajouterait une surcharge telle que la boxe, nécessitant de passer des pointeurs de fonction aux allocateurs et aux constructeurs, etc. L'intention des modèles C++ est d'éviter d'avoir à écrire class MyClass_int, class MyClass_float, etc. presque identique, mais encore être capable de se retrouver avec du code compilé qui est généralement comme si nous avions écrit chaque version séparément. Ainsi, 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. Un modèle ne peut pas être compilé en code, seul le résultat de son instanciation peut être compilé.

Donc, quand foo.cpp est compilé, le compilateur ne peut pas voir bar.cpp 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 doit créer un MyClass<int>, mais il ne peut pas voir le modèle MyClass<T> (seulement son interface dans - foo.h) afin qu'il ne puisse 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, alors quand bar.o = est lié à foo.o ils peuvent être connectés et fonctionneront. Nous pouvons utiliser ce fait pour permettre à un ensemble fini d'instanciations de modèles d'être implémenté dans un fichier .cpp en écrivant un seul modèle. Mais il n’ya aucun moyen pour bar.cpp d’utiliser le modèle comme modèle et de l’instancier sur le type de son choix; il ne peut utiliser que des versions préexistantes de la classe basée sur un modèle que l'auteur de foo.cpp pensait fournir.

Vous pourriez penser que lors de la compilation d'un modèle, le compilateur doit "générer toutes les versions", les filtres qui ne sont jamais utilisés étant filtrés lors de la création de liens. Mis à part l'énorme surcharge et les difficultés extrêmes d'une telle approche, les fonctionnalités de type modificateur, telles que les pointeurs et les tableaux, permettent même aux types intégrés de générer un nombre infini de types, ce qui arrive quand j'étends mon programme maintenant. en ajoutant:

  • baz.cpp
    • déclare et implémente class BazPrivate, et utilise MyClass<BazPrivate>

Il n’est pas possible que cela fonctionne à moins que nous ne

  1. Je dois 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 contienne (éventuellement via l’en-tête includes) le modèle complet de MyClass<T>, afin que le compilateur puisse générer MyClass<BazPrivate> lors de la compilation de baz.cpp.

Personne n'aime (1), car les systèmes de compilation d'analyse de programme complet prennent pour toujours à compiler, et parce qu'il est impossible de distribuer des bibliothèques compilées sans le source code. Nous avons donc (2) à la place.

218
Ben

Les modèles doivent être instanciés par le compilateur avant de les compiler dans un code objet. Cette instanciation ne peut être réalisée que si les arguments du modèle sont connus. Imaginons maintenant un scénario dans lequel une fonction modèle est déclarée dans a.h, définie dans a.cpp et utilisée dans b.cpp. Lorsque a.cpp est compilé, il n'est pas nécessairement connu que la compilation à venir b.cpp nécessitera une instance du modèle, et encore moins de quelle instance spécifique s'agirait-il. Pour plus de fichiers en-tête et sources, la situation peut rapidement devenir plus compliquée.

On peut soutenir que les compilateurs peuvent être plus intelligents pour "regarder en avant" pour 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 compliqués. D'après ce que j'en sais, les compilateurs ne font pas une telle anticipation. Comme Anton l'a souligné, certains compilateurs prennent en charge les déclarations d'exportation explicites d'instanciations de modèles, mais tous les compilateurs ne le prennent pas en charge (pas encore?).

74
David Hanak

En réalité, avant C++ 11, la norme définissait le mot clé export qui aurait permettrait de déclarer des modèles dans un fichier d'en-tête et de les implémenter ailleurs.

Aucun des compilateurs populaires n'a implémenté ce mot clé. Le seul que je connaisse est l'interface écrite par Edison Design Group, utilisée par le compilateur Comeau C++. Tous les autres utilisateurs vous demandaient d'écrire des modèles dans des fichiers d'en-tête, car le compilateur avait 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 la fonctionnalité export des modèles avec C++ 11.

58
DevSolar

Bien que le C++ standard n’ait pas cette exigence, certains compilateurs exigent que tous les modèles de fonction et de classe soient disponibles dans chaque unité de traduction utilisée. En effet, pour ces compilateurs, le corps des fonctions de modèle doit être mis à disposition dans un fichier d’en-tête. Répéter: cela signifie que ces compilateurs ne leur permettront pas d'être définis dans des fichiers autres que des en-têtes tels que les fichiers .cpp

Il existe un mot-clé export qui est censé atténuer ce problème, mais il est loin d'être portable.

34
Anton Gogolev

Les modèles doivent être utilisés dans les en-têtes car le compilateur doit instancier différentes versions du code, en fonction des paramètres donnés/déduits pour les paramètres de modèle. N'oubliez pas qu'un modèle ne représente pas le code directement, mais un modèle pour plusieurs versions de ce code. Lorsque vous compilez une fonction non modèle dans un fichier .cpp, vous compilez une fonction/classe concrète. Ce n'est pas le cas pour les modèles, qui peuvent être instanciés avec différents types, à savoir qu'un code concret doit être émis lors du remplacement des paramètres de modèle par des types concrets.

Le mot clé export comportait une fonction destinée à être utilisée pour une compilation séparée. La fonctionnalité export est obsolète dans C++11 et, autant que je sache, un seul compilateur l'a implémentée. Vous ne devriez pas utiliser export. Une compilation séparée n'est pas possible dans C++ ou C++11, mais peut-être dans C++17, si les concepts le permettent, nous pourrions avoir un moyen de compilation séparée.

Pour obtenir une compilation séparée, une vérification séparée du corps du modèle doit être possible. Il semble qu'une solution est possible avec des concepts. Jetez un oeil à ceci papier récemment présenté à la réunion du comité des normes. Je pense que ce n'est pas la seule exigence, car vous devez toujours instancier du code pour le code du modèle dans le code utilisateur.

Le problème de compilation séparé pour les modèles, je suppose que c'est aussi un problème qui se pose avec la migration vers les modules, qui est en cours de traitement.

27
Germán Diago

Cela signifie que le moyen le plus portable de définir les implémentations de méthodes de classes de modèle consiste à les définir dans la définition de classe de modèle.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
15
Benoît

Bien qu'il y ait beaucoup de bonnes explications ci-dessus, il me manque un moyen pratique de séparer les modèles en en-tête et en corps.
Ma principale préoccupation est d’éviter la recompilation de tous les utilisateurs du modèle lorsque je modifie sa définition.
Avoir toutes les instanciations de modèles dans le corps du modèle n’est pas une solution viable pour moi, car l’auteur du modèle peut ne pas tout savoir si son utilisation est utilisée et l’utilisateur du modèle peut ne pas avoir le droit de le modifier.
J'ai adopté l'approche suivante, qui convient également aux compilateurs plus anciens (gcc 4.3.4, aCC A.03.13).

Pour chaque utilisation de modèle, il existe un typedef dans son propre fichier d'en-tête (généré à partir du modèle UML). Son corps contient l'instanciation (qui aboutit dans une bibliothèque qui est liée à la fin).
Chaque utilisateur du modèle inclut ce fichier d’en-tête et utilise le typedef.

Un exemple schématique:

MyTemplate.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

main.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

De cette façon, seules les instanciations de modèles devront être recompilées, mais pas tous les utilisateurs de modèles (et leurs dépendances).

12
lafrecciablu

Si le problème concerne le temps de compilation supplémentaire et la taille binaire générés en compilant le fichier .h dans le cadre de tous les modules .cpp qui l'utilisent, dans de nombreux cas, vous pouvez faire en sorte que la classe modèle descende d'une classe de base non modélisée pour des parties non dépendantes du type de l'interface, et cette classe de base peut avoir son implémentation dans le fichier .cpp.

6
Eric Shaw

C’est tout à fait correct car le compilateur doit savoir de quel type il s’agit pour une allocation. Ainsi, les classes de modèles, les fonctions, les énumérations, etc. doivent également être implémentées dans le fichier d’en-tête si celui-ci doit être rendu public ou faire partie d’une bibliothèque (statique ou dynamique) car les fichiers d’en-tête ne sont PAS compilés contrairement aux fichiers c/cpp qui sont. Si le compilateur ignore le type, il ne peut pas le compiler. En .Net, c'est possible car tous les objets proviennent de la classe Object. Ce n'est pas. Net.

6
Robert

Juste pour ajouter quelque chose de remarquable ici. On peut définir les méthodes d'une classe basée sur un modèle parfaitement dans le fichier d'implémentation quand ce ne sont pas des modèles de fonction.


myQueue.hpp:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}
3
Nikos

Une façon d'avoir une implémentation séparée est la suivante.

//inner_foo.h

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


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

inner_foo a les déclarations en avant. foo.tpp a l'implémentation et inclut inner_foo.h; et foo.h aura une seule ligne, à inclure foo.tpp.

Lors de la compilation, le contenu de foo.h est copié dans foo.tpp, puis le fichier entier est copié dans foo.h, après quoi il est compilé. De cette façon, il n'y a pas de limitations et la dénomination est cohérente, en échange d'un fichier supplémentaire.

Je le fais parce que les analyseurs statiques pour le code se cassent quand il ne voit pas les déclarations en avant de la classe dans * .tpp. Ceci est gênant lorsque vous écrivez du code dans un IDE ou que vous utilisez YouCompleteMe ou d’autres.

2
Pranay

Le compilateur générera du code pour chaque instanciation de modèle lorsque vous utilisez un modèle pendant l'étape de compilation. Au cours du processus de compilation et de liaison, les fichiers .cpp sont convertis en code objet pur ou en code machine contenant des références ou des symboles indéfinis, car les fichiers .h inclus dans votre fichier main.cpp n'ont aucune implémentation ENCORE. Ceux-ci sont prêts à être liés à un autre fichier objet qui définit une implémentation pour votre modèle. Vous disposez ainsi d'un exécutable a.out complet.

Cependant, étant donné que les modèles doivent être traités à l'étape de compilation afin de générer du code pour chaque instanciation de modèle que vous définissez, la compilation d'un modèle distinct de son fichier d'en-tête ne fonctionnera pas car elle ira toujours de pair, pour la même raison que chaque instanciation de modèle est littéralement une toute nouvelle classe. Dans une classe standard, vous pouvez séparer les fichiers .h et .cpp, car .h est un modèle de cette classe et .cpp est l’implémentation brute. Ainsi, tous les fichiers d’implémentation peuvent être compilés et liés régulièrement. Toutefois, l’utilisation de modèles .h permet de comprendre la classe ne doit pas ressembler à l'objet, c'est-à-dire qu'un fichier .cpp de modèle n'est pas une implémentation ordinaire brute d'une classe, mais simplement un plan détaillé pour une classe. Toute implémentation d'un fichier de modèle .h ne peut donc pas être compilée car vous avez besoin de quelque chose de concret à compiler, les modèles sont abstraits dans ce sens.

Par conséquent, les modèles ne sont jamais compilés séparément et ne sont compilés que lorsque vous avez une instanciation concrète dans un autre fichier source. Cependant, l’instanciation concrète doit connaître l’implémentation du fichier modèle, car la modification du typename T à l’aide d’un type concret dans le fichier .h ne permet pas d’exécuter le travail, car ce que .cpp permet de lier, je ne peux pas le trouver plus tard parce que rappelez-vous que les modèles sont abstraits et ne peuvent pas être compilés, je suis donc obligé de donner l'implémentation maintenant pour que je sache quoi compiler et lier, et maintenant que j'ai l'implémentation dans laquelle elle est liée le fichier source inclus. Fondamentalement, au moment où j'instancie un modèle, j'ai besoin de créer une toute nouvelle classe. Je ne peux pas le faire si je ne sais pas à quoi cette classe devrait ressembler lorsque j'utilise le type que je fournis, à moins que je ne remarque le compilateur l’implémentation du modèle, le compilateur peut donc remplacer T par mon type et créer une classe concrète prête à être compilée et liée.

Pour résumer, les modèles sont des modèles pour l'apparence des classes, tandis que les classes sont des modèles pour l'apparence d'un objet. Je ne peux pas compiler des modèles séparément de leur instanciation concrète car le compilateur ne compile que des types concrets, en d'autres termes, les modèles au moins en C++, est une abstraction de langage pur. Pour ainsi dire, nous devons désabstraire les modèles, et nous le faisons en leur donnant un type concret à traiter afin que notre abstraction de modèle puisse être transformée en un fichier de classe ordinaire et qu’elle puisse ensuite être compilée normalement. La séparation du fichier modèle .h et du fichier modèle .cpp n'a pas de sens. Cela n'a pas de sens car la séparation de .cpp et de .h n’est possible que lorsque le .cpp peut être compilé individuellement et lié individuellement, avec des modèles car nous ne pouvons pas les compiler séparément, car les modèles sont une abstraction, nous sommes donc toujours obligés de mettre l'abstraction toujours avec l'instanciation concrète où l'instanciation concrète doit toujours connaître

Signification typename T get est remplacé lors de la compilation, pas de la liaison. Par conséquent, si j'essaie de compiler un modèle sans que T soit remplacé en tant que type de valeur concret, il ne fonctionnera pas, car il s'agit de la définition des modèles, un processus de compilation, et la méta-programmation btw consiste à utiliser cette définition.

2
Moshe Rabaev

Une autre raison pour laquelle il est judicieux d'écrire les déclarations et les définitions dans les fichiers d'en-tête est la lisibilité. Supposons qu'il existe une telle fonction de modèle dans Utility.h:

template <class T>
T min(T const& one, T const& theOther);

Et dans le fichier Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

Ceci nécessite que chaque classe T implémente ici l'opérateur inférieur à (<). Une erreur de compilation se produira si vous comparez deux instances de classe qui n'ont pas implémenté le "<".

Par conséquent, si vous séparez la déclaration et la définition du modèle, vous ne pourrez pas uniquement lire le fichier d’en-tête pour voir les tenants et les aboutissants de ce modèle afin d’utiliser cette API sur vos propres classes, bien que le compilateur vous le dise dans cet exemple. cas sur lequel l'opérateur doit être remplacé.

0
ClarHandsome