Question Pourquoi les modèles peuvent-ils être implémentés uniquement 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 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?

(Clarification: les fichiers d'en-tête ne sont pas les seulement solution portable. Mais ils sont la solution portable la plus pratique.)


1383
2018-01-30 10:06


origine


Réponses:


C'est ne pas 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 de l'échec de votre code est que, lors de l'instanciation d'un modèle, le compilateur crée une nouvelle classe avec l'argument template 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-le 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, 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 par conséquent le compilateur ne serait pas capable d'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 à séparer l'implémentation et à instancier explicitement toutes les instances de modèle dont vous avez 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 à C ++ Super-FAQ sur ce sujet.


1206
2018-01-30 10:26



Beaucoup de bonnes réponses ici, mais je voulais ajouter ceci (pour l'exhaustivité):

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

Edit: Ajout d'un exemple d'instanciation de modèle explicite. Utilisé après la définition du modèle et toutes les fonctions membres ont été définies.

template class vector<int>;

Cela va instancier (et donc mettre à la disposition de l'éditeur de liens) la classe et toutes ses fonctions membres (seulement). 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 la même chose 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é?) Utilise extern template class vector<int> afin de l'empêcher de l'instancier dans tous les autre (1000?) Fichiers qui utilisent le vecteur.


200
2017-08-13 13:49



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

Permet de se rapprocher un peu du béton pour une explication. Dites 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 être capable de 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 de faire une analyse complète du programme. C'est seulement l'éditeur de liens qui doit gérer l'ensemble du programme en une fois, et le travail du lieur est sensiblement plus facile.

bar.cpp n'a même pas besoin d'exister quand je compile foo.cpp, mais je devrais être capable de lier le foo.o J'ai déjà eu avec le bar.o Je viens juste 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é avec le code qu'ils écrivent des années après que j'ai écrit foo.cpp.

"Polymorphisme de style d'instanciation" signifie que le modèle MyClass<T> n'est pas vraiment une classe générique qui peut être compilé au code qui peut fonctionner pour toute valeur de T. Cela ajouterait des frais généraux tels que la boxe, le besoin de passer des pointeurs de fonction aux allocateurs et aux constructeurs, etc. L'intention des templates C ++ est d'éviter d'avoir à écrire presque identique class MyClass_int, class MyClass_float, etc, mais pour être encore en mesure de se retrouver avec un code compilé qui est la plupart du temps comme si nous avait écrit chaque version séparément. Donc, un modèle est Littéralement un modèle; un modèle de classe est ne pas une classe, c'est une recette pour créer une nouvelle classe pour chaque T 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é.

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 ça (c'est un template, 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) donc il ne peut pas le créer.

Si foo.cpp lui-même utilise MyClass<int>, alors le code pour cela sera généré lors de la compilation foo.cpp, donc quand 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 utiliser le template en tant que modèle et instanciez-le sur tous les types qu'il aime; il ne peut utiliser que des versions préexistantes de la classe modèle que l'auteur de foo.cpp pensé à fournir.

Vous pourriez 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 la liaison. Mis à part l'énorme surcharge et les difficultés extrêmes une telle approche serait confrontée parce que les fonctionnalités de type "modificateur" comme les pointeurs et les tableaux permettent même seulement les types intégrés pour donner lieu à un nombre infini de types, ce qui se passe quand j'étend mon programme en ajoutant:

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

Il n'y a pas de moyen possible que cela pourrait fonctionner à moins que nous non plus

  1. Avoir à recompiler foo.cpp chaque fois que nous changeons tout autre fichier dans le programme, dans le cas où il a ajouté une nouvelle instanciation de MyClass<T>
  2. Exiger que baz.cpp contient (éventuellement via en-tête inclut) 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 programmes entiers pour toujours compiler, et parce qu'il rend impossible de distribuer des bibliothèques compilées sans le code source. Nous avons donc (2) à la place.


173
2018-05-11 03:54



Les modèles doivent être instancié par le compilateur avant de les compiler en code objet. Cette instanciation ne peut être réalisée 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éfini 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, et encore moins quelle instance spécifique serait-ce. Pour plus de fichiers d'en-tête et de source, la situation peut rapidement devenir plus compliquée.

On peut soutenir que les compilateurs peuvent être 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 compliqués. AFAIK, les compilateurs ne font pas de telles perspectives. Comme l'a souligné Anton, certains compilateurs supportent des déclarations d'export explicites d'instanciations de modèles, mais tous les compilateurs ne le supportent pas (encore?).


64
2018-01-30 10:23



En fait, les versions du standard C ++ avant C ++ 11 définissaient le mot-clé 'export', aurait permettent de déclarer simplement des modèles dans un fichier d'en-tête et de les implémenter ailleurs.

Malheureusement, aucun des compilateurs populaires n'a implémenté ce mot-clé. Le seul que je connaisse est le frontend écrit par Edison Design Group, qui est utilisé par le compilateur Comeau C ++. Tous les autres ont insisté pour que vous écriviez des modèles dans des fichiers d'en-tête, nécessitant la définition du code 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 export caractéristique des modèles commençant par C ++ 11.


47
2018-01-30 13:38



Bien que le standard C ++ n'ait pas cette exigence, certains compilateurs exigent que tous les modèles de fonctions et de classes doivent être disponibles dans chaque unité de traduction utilisée. En effet, pour ces compilateurs, les corps des fonctions du modèle doivent être rendus 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 y a un exportation mot-clé qui est censé atténuer ce problème, mais il est loin d'être portable.


31
2018-01-30 10:15



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. Rappelez-vous 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-template 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, le code concret doit être émis lors du remplacement des paramètres du modèle par des types concrets.

Il y avait une caractéristique avec le export mot-clé qui devait être utilisé pour une compilation séparée. le export fonctionnalité est dépréciée dans C++11 et, AFAIK, un seul compilateur l'a implémenté. Vous ne devriez pas utiliser export. La compilation séparée n'est pas possible dans C++ ou C++11 mais peut-être C++17, si les concepts le font, nous pourrions avoir un moyen de compilation séparé.

Pour qu'une compilation séparée soit réalisée, une vérification séparée du corps du gabarit doit être possible. Il semble qu'une solution soit possible avec des concepts. Regarde ça papier récemment présenté au réunion du comité des normes. Je pense que ce n'est pas la seule exigence, car vous devez toujours instancier le code du code du modèle dans le code utilisateur.

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


26
2018-05-12 16:42



Cela signifie que la façon la plus portable de définir des implémentations de méthodes de classes de modèles 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
    }
};

13
2018-01-30 10:53



Même s'il y a 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 de modèles, quand je change sa définition.
Avoir toutes les instanciations de gabarit dans le corps du gabarit n'est pas une solution viable pour moi, car l'auteur du gabarit peut ne pas tout savoir si son utilisation et l'utilisateur du gabarit n'ont pas le droit de le modifier.
J'ai pris l'approche suivante, qui fonctionne aussi pour les compilateurs plus anciens (gcc 4.3.4, aCC A.03.13).

Pour chaque utilisation de template, 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 se termine 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, pas tous les utilisateurs de modèles (et les dépendances).


9
2018-05-12 14:02



C'est exactement correct car le compilateur doit savoir de quel type il s'agit pour l'allocation. Donc, les classes, fonctions, énumérations, etc .. doivent également être implémentées dans le fichier header 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 sont. Si le compilateur ne connaît pas le type, il ne peut pas le compiler. En .Net, cela est possible parce que tous les objets dérivent de la classe Object. Ce n'est pas .Net.


6
2017-09-17 03:40