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

1779

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.)

MainID
la source
13
S'il est vrai que placer toutes les définitions de fonction de modèle dans le fichier d'en-tête est probablement le moyen le plus pratique de les utiliser, il n'est toujours pas clair 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.
AnT
7
Le livre est obsolète.
gerardw
1
Un modèle n'est pas comme une fonction qui peut être compilée en code octet. C'est juste un modèle pour générer une telle fonction. Si vous mettez 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 pour créer une fonction à partir du modèle qui se retrouve dans le fichier * .obj.
dgrat
5
Suis-je le seul à penser que le concept de modèle est paralysé en C ++ à cause de cela? ...
DragonGamer

Réponses:

1559

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:

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), qui est équivalente à la suivante:

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 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

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.

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

// 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 suffisamment claire, vous pouvez consulter la Super-FAQ C ++ à ce sujet .

Luc Touraille
la source
96
En fait, l'instanciation explicite doit être 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.
Mankarse
11
"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" Mais pourquoi une implémentation dans le fichier .cpp n'est pas accessible au compilateur? Un compilateur peut également accéder aux informations .cpp, comment pourrait-il les transformer en fichiers .obj? EDIT: la réponse à cette question se trouve dans le lien fourni dans cette réponse ...
xcrypt
31
Je ne pense pas que cela explique la question que clairement, l'élément clé est évidemment lié à la compilation UNIT qui n'est pas mentionnée dans ce post
zinking
6
@Gabson: les structures et les classes sont équivalentes à l'exception que le modificateur d'accès par défaut pour les classes est "privé", alors qu'il est public pour les structures. Il y a d'autres petites différences que vous pouvez apprendre en regardant cette question .
Luc Touraille
3
J'ai ajouté une phrase au tout début de cette réponse pour préciser que la question est basée sur une fausse prémisse. Si quelqu'un demande "Pourquoi X est-il vrai?" alors qu'en fait X n'est pas vrai, nous devons rapidement rejeter cette hypothèse.
Aaron McDaid
250

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.

template class vector<int>;

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.

MaHuJa
la source
51
Pouah. Bonne réponse, mais pas de vraie solution propre. La liste de tous les types possibles pour un modèle ne semble pas aller avec ce qu'un modèle est censé être.
Jiminion
6
Cela peut être bon dans de nombreux cas, mais brise généralement l'objectif du modèle qui est destiné à vous permettre d'utiliser la classe avec n'importe quel typesans les répertorier manuellement.
Tomáš Zato - Rétablir Monica
7
vectorn'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é.
UncleZeiv
Utilisé après que le modèle a été défini, "et que toutes les fonctions membres ont été définies". Merci !
Vitt Volt
1
J'ai l'impression de manquer quelque chose… J'ai mis l'instanciation explicite pour deux types dans le .cppfichier de la classe et les deux instanciations sont référencées à partir d'autres .cppfichiers, et j'obtiens toujours l'erreur de liaison selon laquelle les membres ne sont pas trouvés.
Oarfish
250

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:

  • foo.h
    • déclare l'interface de class MyClass<T>
  • foo.cpp
    • définit la mise en œuvre de class MyClass<T>
  • bar.cpp
    • les usages 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 de T. 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 identiques class MyClass_int, class MyClass_floatetc., 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 que Tnous 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èle MyClass<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 un MyClass<int>, mais il ne peut pas voir le modèle MyClass<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:

  • baz.cpp
    • déclare et implémente class BazPrivateet utiliseMyClass<BazPrivate>

Il n'y a aucun moyen que cela puisse fonctionner à moins que nous non plus

  1. Nous devons recompiler foo.cpp chaque fois que nous modifions un autre fichier du programme , au cas où il ajouterait une nouvelle nouvelle instanciation deMyClass<T>
  2. Exiger que baz.cpp contienne (éventuellement via l'en-tête inclut) le modèle complet de MyClass<T>, afin que le compilateur puisse générer MyClass<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.

Ben
la source
50
a souligné la citation d' 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
v.oddou
J'aimerais savoir, est-il possible de faire les instanciations explicites depuis un endroit autre que l'en-tête ou le fichier source de la classe? Par exemple, faites-les dans main.cpp?
gromit190
1
@Birger Vous devriez pouvoir le faire à partir de n'importe quel fichier ayant accès à l'implémentation complète du modèle (soit parce qu'il est dans le même fichier ou via les en-têtes inclus).
Ben
11
@ajeh Ce n'est pas de la rhétorique. La question est "pourquoi devez-vous implémenter des modèles dans un en-tête?", J'ai donc expliqué les choix techniques que le langage C ++ fait qui conduisent à cette exigence. Avant d'écrire ma réponse, d'autres ont déjà fourni des solutions de contournement qui ne sont pas des solutions complètes, car il ne peut pas y avoir de solution complète. Je pensais que ces réponses seraient complétées par une discussion plus approfondie de l'angle «pourquoi» de la question.
Ben
1
imaginez-le de cette façon, les gens ... si vous n'utilisiez pas de modèles (pour coder efficacement ce dont vous avez besoin), vous ne proposeriez de toute façon que quelques versions de cette classe. vous avez donc 3 options. 1). n'utilisez pas de modèles. (comme toutes les autres classes / fonctions, personne ne se soucie que les autres ne puissent pas modifier les types) 2). utiliser des modèles et documenter les types qu'ils peuvent utiliser. 3). donnez-leur le bonus d'implémentation (source) 4). donnez-leur toute la source au cas où ils voudraient faire un modèle à partir d'une autre de vos classes;)
Puddle
81

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 dans a.cppet utilisée dans b.cpp. Quand a.cppest compilé, on ne sait pas nécessairement que la compilation à venir b.cppné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?).

David Hanak
la source
1
"exporter" est standard, mais il est difficile à implémenter, donc la plupart des équipes de compilation ne l'ont pas encore fait.
vava
5
l'exportation n'élimine pas le besoin de divulgation de la source, ni ne réduit les dépendances de compilation, alors qu'elle nécessite un effort massif des constructeurs de compilateurs. Herb Sutter lui-même a donc demandé aux constructeurs de compilateurs d'oublier l'exportation. Comme l'investissement en temps nécessaire serait mieux dépensé ailleurs ...
Pieter
2
Je ne pense donc pas que l'exportation ne soit pas encore implémentée. 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é
Pieter
3
Si cela vous intéresse, le papier s'intitule "Pourquoi nous ne pouvons pas nous permettre d'exporter", il est répertorié sur son blog ( gotw.ca/publications ) mais pas de pdf là-bas (un rapide google devrait le faire cependant)
Pieter
1
Ok, merci pour le bon exemple et l'explication. Voici ma question cependant: pourquoi le compilateur ne peut pas comprendre où le modèle est appelé et compiler ces fichiers avant de compiler le fichier de définition? Je peux imaginer que cela peut être fait dans un cas simple ... La réponse est-elle que les interdépendances vont gâcher l'ordre assez rapidement?
Vlad
63

En fait, avant C ++ 11, la norme définissait le exportmot-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 exportfonctionnalité des modèles avec C ++ 11.

DevSolar
la source
6
... et quelques années plus tard, j'ai finalement compris ce qui nous exportaurait 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.
DevSolar
4
@DevSolar: cet article est politique, répétitif et mal écrit. ce n'est pas la prose de niveau standard habituelle là-bas. D'une longueur inouïe et ennuyeuse, disant essentiellement 3 fois les mêmes choses sur des dizaines de pages. Mais je suis maintenant informé que l'exportation n'est pas une exportation. C'est une bonne information!
v.oddou
1
@ 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. ;-)
DevSolar
@ v.oddou Le document n'est pas seulement mal écrit, c'est de la désinformation. C'est aussi une rotation sur la réalité: ce qui sont en fait des arguments extrêmement forts pour les exportations sont mélangés de manière à donner l'impression qu'ils sont contre l'exportation: «découvrir de nombreux trous liés à l'ODR dans la norme en présence d'exportation. Avant l'exportation, les violations ODR n'avaient pas à être diagnostiquées par le compilateur. Maintenant, c'est nécessaire parce que vous devez combiner des structures de données internes provenant de différentes unités de traduction, et vous ne pouvez pas les combiner si elles représentent réellement des choses différentes, vous devez donc faire la vérification. »
curiousguy
" doit maintenant ajouter dans quelle unité de traduction il se trouvait quand il s'est produit " Duh. Lorsque vous êtes obligé d'utiliser des arguments boiteux, vous n'avez aucun argument. Bien sûr, vous allez mentionner des noms de fichiers dans vos erreurs, quel est le problème? Que quelqu'un tombe amoureux de ce BS est ahurissant. " Même des experts comme James Kanze ont du mal à accepter que l'exportation soit vraiment comme ça. " QUOI? !!!!
curiousguy
34

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.

Anton Gogolev
la source
Pourquoi ne puis-je pas les implémenter dans un fichier .cpp avec le mot clé "inline"?
MainID
2
Vous pouvez, et vous n'avez même pas besoin de mettre "en ligne". Mais vous seriez en mesure de les utiliser uniquement dans ce fichier cpp et nulle part ailleurs.
vava
10
C'est presque la réponse la plus précise , sauf que «cela signifie que ces compilateurs ne permettront pas de les définir dans des fichiers non en-tête tels que des fichiers .cpp» est manifestement faux.
Courses de légèreté en orbite
28

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 .cppfichier, 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 exportmot-clé qui devait être utilisée pour une compilation séparée. La exportfonctionnalité est déconseillée dans C++11et, AFAIK, un seul compilateur l'a implémentée. Vous ne devriez pas utiliser export. La compilation séparée n'est pas possible dans C++ou C++11mais peut-être dans C++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.

Germán Diago
la source
15

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.

template < typename ... >
class MyClass
{

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

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:

#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 du modèle devront être recompilées, pas tous les utilisateurs du modèle (et les dépendances).

lafrecciablu
la source
1
J'aime cette approche à l'exception du MyInstantiatedTemplate.hfichier et du MyInstantiatedTemplatetype 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/4612476
Cameron Tacklind
Cela prend le meilleur de deux mondes. Je souhaite que cette réponse soit mieux notée! Voir également le lien ci-dessus pour une implémentation légèrement plus propre de la même idée.
Wormer
8

Juste 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:

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;

    ...
}
Nikos
la source
2
Si c'est vrai, votre réponse doit être vérifiée comme étant correcte. Pourquoi quelqu'un a-t-il besoin de toutes ces choses de vaudou hacky si vous pouvez simplement définir des méthodes non membres de modèle dans .cpp?
Michael IV
Eh bien, cela ne fonctionne pas. Au moins sur MSVC 2019, obtenir un symbole externe non résolu pour une fonction membre de la classe de modèle.
Michael IV
Je n'ai pas MSVC 2019 à tester. Ceci est autorisé par la norme C ++. Maintenant, MSVC est connu pour ne pas toujours respecter les règles. Si vous ne l'avez pas déjà fait, essayez Paramètres du projet -> C / C ++ -> Langue -> Mode de conformité -> Oui (permissif-).
Nikos
1
Cet exemple exact fonctionne mais vous ne pouvez pas appeler isEmptydepuis une autre unité de traduction en plus myQueue.cpp...
MM
7

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.

Eric Shaw
la source
2
Cette réponse devrait être modifiée davantage. J'ai " indépendamment " découvert votre même approche et recherchais spécifiquement quelqu'un d'autre pour l'avoir déjà utilisée, car je suis curieux de savoir si c'est un modèle officiel et s'il a un nom. Mon approche consiste à implémenter un class XBasepartout où j'ai besoin d'implémenter un template class X, en mettant les pièces dépendantes du type Xet tout le reste XBase.
Fabio A.
6

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.

Robert
la source
5
"les fichiers d'en-tête ne sont PAS compilés" - c'est une façon vraiment étrange de le décrire. Les fichiers d'en-tête peuvent faire partie d'une unité de traduction, tout comme un fichier "c / cpp".
Flexo
2
En fait, c'est presque l'opposé de la vérité, qui est que les fichiers d'en-tête sont très souvent compilés plusieurs fois, alors qu'un fichier source est généralement compilé une fois.
xaxxon
6

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 letypename Tl'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 remplacer Tpar 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 Test remplacé pendant l'étape de compilation et non pas l'étape de liaison, donc si j'essaie de compiler un modèle sans Tê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 qui Test.

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.

Moshe Rabaev
la source
4

Une manière 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 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.

Pranay
la source
2
s / inner_foo / foo / g et inclure foo.tpp à la fin de foo.h. Un fichier de moins.
1

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.

Juan
la source
-2

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:

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

Et dans Utility.cpp:

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

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é.

ClarHandsome
la source
-7

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.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Maintenant, à l'intérieur de votre fichier .template, vous définissez vos fonctions comme vous le feriez normalement.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }
Ni Nisan Nijackle
la source
2
La plupart des gens définiraient un fichier d'en-tête comme étant tout ce qui propage des définitions aux fichiers source. Vous avez donc peut-être décidé d'utiliser l'extension de fichier ".template" mais vous avez écrit un fichier d'en-tête.
Tommy