Citation de la bibliothèque standard C ++: un tutoriel et un manuel :
La seule façon portable d'utiliser des modèles pour le moment est de les implémenter dans des fichiers d'en-tête en utilisant des fonctions en ligne.
Pourquoi est-ce?
(Précision: les fichiers d'en-tête ne sont pas la seule solution portable. Mais ils sont la solution portable la plus pratique.)
Réponses:
Attention: il n'est pas nécessaire de 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:
En lisant cette ligne, le compilateur créera une nouvelle classe (appelons-la
FooInt
), qui est équivalente à la suivante:Par conséquent, le compilateur doit avoir accès à l'implémentation des méthodes, pour les instancier avec l'argument template (dans ce cas
int
). Si ces implémentations n'étaient pas dans l'en-tête, elles ne seraient pas accessibles, et donc le compilateur ne pourrait 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
Foo.tpp
De cette façon, l'implémentation est toujours séparée de la déclaration, mais est accessible au compilateur.
Solution alternative
Une autre solution consiste à garder l'implémentation séparée et à instancier explicitement toutes les instances de modèle dont vous aurez besoin:
Foo.h
Foo.cpp
Si mon explication n'est pas suffisamment claire, vous pouvez consulter la Super-FAQ C ++ à ce sujet .
la source
Beaucoup de bonnes réponses ici, mais je voulais ajouter ceci (pour être complet):
Si vous, 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 pourra les trouver comme d'habitude.
Modifier: Ajout d'un exemple d'instanciation de modèle explicite. Utilisé une fois le modèle défini et toutes les fonctions membres définies.
Cela instanciera (et rendra donc disponible pour l'éditeur de liens) la classe et toutes ses fonctions membres (uniquement). Une syntaxe similaire fonctionne pour les fonctions de modèle, donc 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 assez 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é?) L'utilise
extern template class vector<int>
pour l'empêcher de l'instancier dans tous les autres fichiers (1000?) Qui utilisent le vecteur.la source
type
sans les répertorier manuellement.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 des 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é..cpp
fichier de la classe et les deux instanciations sont référencées à partir d'autres.cpp
fichiers, et j'obtiens toujours l'erreur de liaison selon laquelle les membres ne sont pas trouvés.C'est à cause de l'exigence d'une compilation séparée et parce que les modèles sont un polymorphisme de style instanciation.
Permet de se rapprocher un peu du béton pour une explication. Disons que j'ai les fichiers suivants:
class MyClass<T>
class MyClass<T>
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 dur 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'analyser l'ensemble du programme. Ce n'est que l'éditeur de liens qui doit gérer l'ensemble du programme à la fois, et le travail de l'éditeur de liens est beaucoup plus facile.
bar.cpp n'a même pas besoin d'exister lorsque je compile foo.cpp , mais je devrais quand même être capable de lier le foo.o que j'avais déjà avec le bar.o Je viens juste de le produire, sans avoir besoin de recompiler foo .cpp . foo.cpp pourrait même être compilé dans une bibliothèque dynamique, distribué ailleurs sans foo.cpp et lié avec du code qu'ils écrivent des années après avoir écrit foo.cpp .
«Polymorphisme de style instanciation» signifie que le modèle
MyClass<T>
n'est pas vraiment une classe générique qui peut être compilée en code pouvant fonctionner pour n'importe quelle valeur deT
. Cela ajouterait les frais généraux tels que la boxe, besoin de passer des pointeurs de fonction pour allocataires et les constructeurs, etc. L'intention des modèles de C est d'éviter d' avoir à écrire presque identiquesclass MyClass_int
,class MyClass_float
etc., mais être encore en mesure de finir avec le code compilé est comme si nous avions écrit chaque version séparément. Un modèle est donc 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 chacun queT
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 pour savoir que cela
MyClass<int>
est nécessaire. Il peut voir le modèleMyClass<T>
, mais il ne peut pas émettre de code pour cela (c'est un modèle, pas une classe). Et lorsque bar.cpp est compilé, le compilateur peut voir qu'il doit créer unMyClass<int>
, mais il ne peut pas voir le modèleMyClass<T>
(uniquement son interface dans foo.h ), il ne peut donc pas le créer.Si foo.cpp lui-même utilise
MyClass<int>
, le code correspondant sera généré lors de la compilation de foo.cpp , donc lorsque 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èle d'être implémenté dans un fichier .cpp en écrivant un seul modèle. Mais il n'y a aucun moyen pour bar.cpp d'utiliser le modèle comme modèle et de l'instancier sur tous les types qu'il aime; il ne peut utiliser que des versions préexistantes de la classe basée sur des modèles 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", celles qui ne sont jamais utilisées étant filtrées lors de la liaison. Mis à part les énormes frais généraux et les difficultés extrêmes auxquelles une telle approche serait confrontée car les fonctionnalités de "modificateur de type" comme les pointeurs et les tableaux permettent même aux types intégrés de donner lieu à un nombre infini de types, ce qui se passe lorsque j'étend maintenant mon programme en ajoutant:
class BazPrivate
et utiliseMyClass<BazPrivate>
Il n'y a aucun moyen que cela puisse fonctionner à moins que nous non plus
MyClass<T>
MyClass<T>
, afin que le compilateur puisse générerMyClass<BazPrivate>
pendant la compilation de baz.cpp .Personne n'aime (1), car les systèmes de compilation d'analyse de programme entier prennent une éternité à compiler, et parce qu'il est impossible de distribuer les bibliothèques compilées sans le code source. Nous avons donc (2) à la place.
la source
Les modèles doivent être instanciés par le compilateur avant de réellement les compiler en code objet. Cette instanciation ne peut être obtenue que si les arguments du modèle sont connus. Imaginez maintenant un scénario dans lequel une fonction de modèle est déclarée dans
a.h
, définie dansa.cpp
et utilisée dansb.cpp
. Quanda.cpp
est compilé, on ne sait pas nécessairement que la compilation à venirb.cpp
nécessitera une instance du modèle, et encore moins quelle instance spécifique serait-ce. Pour plus d'en-têtes et de fichiers source, la situation peut rapidement devenir plus compliquée.On peut affirmer que les compilateurs peuvent être rendus plus intelligents pour "anticiper" 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 autrement compliqués. AFAIK, les compilateurs ne font pas de telles anticipations. Comme Anton l'a souligné, certains compilateurs prennent en charge les déclarations d'exportation explicites des instanciations de modèle, mais tous les compilateurs ne le prennent pas (encore?).
la source
En fait, avant C ++ 11, la norme définissait le
export
mot-clé qui 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 le Edison Design Group, qui est utilisée par le compilateur Comeau C ++. Tous les autres vous ont demandé d'écrire des modèles dans des fichiers d'en-tête, car le compilateur a besoin de la définition de modèle pour une instanciation appropriée (comme d'autres l'ont déjà souligné).
En conséquence, le comité de la norme ISO C ++ a décidé de supprimer la
export
fonctionnalité des modèles avec C ++ 11.la source
export
aurait réellement donné et ce qui ne l'a pas été ... et maintenant je suis entièrement d'accord avec les gens d'EDG: cela ne nous aurait pas apporté ce que la plupart des gens (moi-même en '11 inclus) pense que ce serait le cas, et le standard C ++ est mieux sans lui.Bien que le C ++ standard n'ait pas une telle 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, les corps des fonctions de modèle doivent être disponibles dans un fichier d'en-tête. Pour répéter: cela signifie que ces compilateurs ne permettront pas de les définir dans des fichiers non en-tête tels que les fichiers .cpp
Il existe un mot-clé d' exportation qui est censé atténuer ce problème, mais il est loin d'être portable.
la source
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 du modèle. N'oubliez pas qu'un modèle ne représente pas directement le code, mais un modèle pour plusieurs versions de ce code. Lorsque vous compilez une fonction non modèle dans un
.cpp
fichier, 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, du code concret doit être émis lors du remplacement des paramètres du modèle par des types concrets.Il y avait une fonctionnalité avec le
export
mot-clé qui devait être utilisée pour une compilation séparée. Laexport
fonctionnalité est déconseillée dansC++11
et, AFAIK, un seul compilateur l'a implémentée. Vous ne devriez pas utiliserexport
. La compilation séparée n'est pas possible dansC++
ouC++11
mais peut-être dansC++17
, si les concepts le font, nous pourrions avoir un moyen de compilation séparée.Pour obtenir une compilation séparée, une vérification du corps du modèle distinct doit être possible. Il semble qu'une solution soit possible avec des concepts. Jetez un œil à ce document 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 le code pour le code 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 survient avec la migration vers les modules, qui est actuellement en cours de traitement.
la source
Cela signifie que la façon la plus portable de définir les implémentations de méthode des classes de modèle est de les définir dans la définition de classe de modèle.
la source
Même s'il existe de nombreuses bonnes explications ci-dessus, il me manque un moyen pratique de séparer les modèles en en-tête et corps.
Ma principale préoccupation est d'éviter la recompilation de tous les utilisateurs de modèles lorsque je modifie sa définition.
Avoir toutes les instanciations de modèle 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 et l'utilisateur du modèle peuvent ne pas avoir le droit de le modifier.
J'ai adopté l'approche suivante, qui fonctionne également pour les anciens compilateurs (gcc 4.3.4, aCC A.03.13).
Pour chaque utilisation de modèle, il y a 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 à 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:
MyTemplate.cpp:
MyInstantiatedTemplate.h:
MyInstantiatedTemplate.cpp:
main.cpp:
De cette façon, seules les instanciations du modèle devront être recompilées, pas tous les utilisateurs du modèle (et les dépendances).
la source
MyInstantiatedTemplate.h
fichier et duMyInstantiatedTemplate
type ajouté . C'est un peu plus propre si vous ne l'utilisez pas, à mon humble avis. Découvrez ma réponse sur une autre question montrant ceci: stackoverflow.com/a/41292751/4612476Juste pour ajouter quelque chose de remarquable ici. On peut définir très bien les méthodes d'une classe de modèle dans le fichier d'implémentation quand ce ne sont pas des modèles de fonction.
myQueue.hpp:
myQueue.cpp:
la source
isEmpty
depuis une autre unité de traduction en plusmyQueue.cpp
...Si le problème est le temps de compilation supplémentaire et le gonflement de la taille binaire produits en compilant le .h dans tous les modules .cpp l'utilisant, dans de nombreux cas, ce que vous pouvez faire est de faire descendre la classe de modèle d'une classe de base non modèle pour les parties non dépendantes du type de l'interface, et cette classe de base peut avoir son implémentation dans le fichier .cpp.
la source
class XBase
partout où j'ai besoin d'implémenter untemplate class X
, en mettant les pièces dépendantes du typeX
et tout le resteXBase
.C'est exactement correct car le compilateur doit savoir de quel type il s'agit pour l'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 elles doivent être rendues publiques 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 ne connaît pas le type, il ne peut pas le compiler. Dans .Net, c'est possible car tous les objets dérivent de la classe Object. Ce n'est pas .Net.
la source
Le compilateur génère du code pour chaque instanciation de modèle lorsque vous utilisez un modèle lors de l'étape de compilation. Dans le processus de compilation et de liaison, les fichiers .cpp sont convertis en objet pur ou en code machine qui contient en eux des références ou des symboles non définis car les fichiers .h qui sont inclus dans votre main.cpp n'ont ENCORE aucune implémentation. Ceux-ci sont prêts à être liés à un autre fichier objet qui définit une implémentation de votre modèle et vous disposez donc d'un exécutable a.out complet.
Cependant, étant donné que les modèles doivent être traités lors de l'étape de compilation afin de générer du code pour chaque instanciation de modèle que vous définissez, la simple compilation d'un modèle distinct de son fichier d'en-tête ne fonctionnera pas car ils vont toujours de pair, pour la raison même que chaque instanciation de modèle est littéralement une toute nouvelle classe. Dans une classe régulière, vous pouvez séparer .h et .cpp car .h est un plan directeur de cette classe et le .cpp est l'implémentation brute, de sorte que tous les fichiers d'implémentation peuvent être compilés et liés régulièrement, cependant l'utilisation de modèles .h est un plan directeur de la façon la classe ne doit pas ressembler à l'objet, ce qui signifie qu'un fichier modèle .cpp n'est pas une implémentation régulière brute d'une classe, c'est simplement un plan pour une classe, donc toute implémentation d'un fichier modèle .h peut '
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 il suffit de modifier le
typename T
l'utilisation d'un type concret dans le fichier .h ne fera pas le travail car ce que .cpp est là pour lier, je ne le trouverai pas plus tard car les modèles de rappel sont abstraits et ne peuvent pas être compilés, donc je suis forcé pour donner l'implémentation maintenant, donc je sais quoi compiler et lier, et maintenant que j'ai l'implémentation, elle est liée dans le fichier source inclus. Fondamentalement, au moment où j'instancie un modèle, j'ai besoin de créer une toute nouvelle classe, et je ne peux pas le faire si je ne sais pas à quoi devrait ressembler cette classe lorsque j'utilise le type que je fournis, sauf si je le fais remarquer au compilateur de l'implémentation du modèle, maintenant le compilateur peut remplacerT
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 plans directeurs pour l'apparence des classes, les classes sont des plans directeurs pour l'apparence d'un objet. Je ne peux pas compiler de modèles séparés 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 ++, sont une pure abstraction de langage. Nous devons pour ainsi dire résilier les modèles, et nous le faisons en leur donnant un type concret à traiter afin que notre abstraction de modèle puisse se transformer en un fichier de classe ordinaire et à son tour, il peut être compilé normalement. La séparation du fichier .h de modèle et du fichier .cpp de modèle n'a aucun sens. C'est absurde parce que la séparation de .cpp et .h n'est que là où 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,
Le sens
typename T
est remplacé pendant l'étape de compilation et non pas l'étape de liaison, donc si j'essaie de compiler un modèle sansT
être remplacé comme un type de valeur concret qui n'a absolument aucun sens pour le compilateur et comme résultat, le code objet ne peut pas être créé car il ne le fait pas savoir ce quiT
est.Il est techniquement possible de créer une sorte de fonctionnalité qui sauvera le fichier template.cpp et changera les types quand il les trouvera dans d'autres sources, je pense que la norme a un mot
export
- clé qui vous permettra de mettre des modèles dans un autre fichier cpp mais pas que de nombreux compilateurs implémentent réellement cela.Juste une note latérale, lorsque vous faites des spécialisations pour une classe de modèle, vous pouvez séparer l'en-tête de l'implémentation car une spécialisation par définition signifie que je me spécialise pour un type concret qui peut être compilé et lié individuellement.
la source
Une manière d'avoir une implémentation séparée est la suivante.
inner_foo a les déclarations avancées. foo.tpp a l'implémentation et inclut inner_foo.h; et foo.h n'aura qu'une seule ligne, pour inclure foo.tpp.
Au moment 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 se compile. De cette façon, il n'y a aucune limitation 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 avancées de classe dans * .tpp. Cela est gênant lors de l'écriture de code dans un IDE ou lors de l'utilisation de YouCompleteMe ou d'autres.
la source
Je suggère de regarder cette page gcc qui discute des compromis entre le modèle "cfront" et "borland" pour les instanciations de modèle.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
Le modèle "borland" correspond à ce que propose l'auteur, en fournissant la définition complète du modèle et en compilant les choses plusieurs fois.
Il contient des recommandations explicites concernant l'utilisation de l'instanciation manuelle et automatique des modèles. Par exemple, l'option "-repo" peut être utilisée pour collecter des modèles qui doivent être instanciés. Ou une autre option consiste à désactiver les instanciations automatiques des modèles à l'aide de "-fno-implicit-templates" pour forcer l'instanciation manuelle des modèles.
D'après mon expérience, je me fie à la bibliothèque standard C ++ et aux modèles Boost qui sont instanciés pour chaque unité de compilation (à l'aide d'une bibliothèque de modèles). Pour mes grandes classes de modèles, je fais une instanciation manuelle des modèles, une fois, pour les types dont j'ai besoin.
C'est mon approche car je fournis un programme de travail, pas une bibliothèque de modèles à utiliser dans d'autres programmes. L'auteur du livre, Josuttis, travaille beaucoup sur les bibliothèques de modèles.
Si j'étais vraiment préoccupé par la vitesse, je suppose que j'explorerais à l'aide des en-têtes précompilés https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
qui gagne du soutien dans de nombreux compilateurs. Cependant, je pense que les en-têtes précompilés seraient difficiles avec les fichiers d'en-tête de modèle.
la source
Une autre raison pour laquelle il est judicieux d'écrire des déclarations et des définitions dans des fichiers d'en-tête est pour la lisibilité. Supposons qu'il existe une telle fonction de modèle dans Utility.h:
Et dans Utility.cpp:
Cela nécessite que chaque classe T ici implémente l'opérateur inférieur à (<). Il générera une erreur de compilation lorsque 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 lire uniquement le fichier d'en-tête pour voir les tenants et aboutissants de ce modèle afin d'utiliser cette API sur vos propres classes, bien que le compilateur vous le dise dans ce cas sur lequel l'opérateur doit être remplacé.
la source
Vous pouvez réellement définir votre classe de modèle dans un fichier .template plutôt que dans un fichier .cpp. Celui qui dit que vous ne pouvez le définir qu'à l'intérieur d'un fichier d'en-tête a tort. C'est quelque chose qui remonte au c ++ 98.
N'oubliez pas que votre compilateur traite votre fichier .template comme un fichier c ++ pour conserver l'intelligence.
Voici un exemple de cela pour une classe de tableau dynamique.
Maintenant, à l'intérieur de votre fichier .template, vous définissez vos fonctions comme vous le feriez normalement.
la source