web-dev-qa-db-fra.com

Fractionner les classes C++ basées sur des modèles en fichiers .hpp/.cpp - est-ce possible?

Je reçois des erreurs en essayant de compiler une classe de modèle C++ divisée en un fichier .hpp et .cpp:

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

Voici mon code:

stack.hpp:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp:

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp:

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ld est bien sûr correct: les symboles ne sont pas dans stack.o.

La réponse à cette question n'aide pas, comme je le fais déjà comme il est dit.
Celui-ci pourrait aider, mais je ne veux pas déplacer chaque méthode dans le fichier .hpp - je n'aurais pas à le faire, devrais-je?

La seule solution raisonnable est-elle de déplacer tout le contenu du fichier .cpp dans le fichier .hpp et de simplement tout inclure plutôt que de créer un lien en tant que fichier objet autonome? Cela semble terriblement laid! Dans ce cas, je pourrais aussi bien revenir à mon état précédent et renommer stack.cpp en stack.hpp et en finir.

83
exscape

Il n'est pas possible d'écrire l'implémentation d'une classe de modèle dans un fichier cpp séparé et de la compiler. Toutes les manières de le faire, si quelqu'un le prétend, sont des solutions de contournement pour imiter l'utilisation d'un fichier cpp séparé, mais pratiquement si vous avez l'intention d'écrire une bibliothèque de classes de modèles et de la distribuer avec des fichiers en-tête et lib pour masquer la mise en œuvre, il est tout simplement impossible . 

Pour savoir pourquoi, examinons le processus de compilation. Les fichiers d'en-tête ne sont jamais compilés. Ils sont seulement prétraités. Le code prétraité est ensuite associé au fichier cpp qui est réellement compilé. Maintenant, si le compilateur doit générer la structure de mémoire appropriée pour l'objet, il doit connaître le type de données de la classe de modèle. 

En réalité, il faut comprendre que la classe de modèle n'est pas une classe du tout, mais un modèle de classe dont la déclaration et la définition sont générées par le compilateur au moment de la compilation après avoir obtenu les informations du type de données à partir de l'argument. Tant que la structure de la mémoire ne peut pas être créée, les instructions pour la définition de la méthode ne peuvent pas être générées. Rappelez-vous que le premier argument de la méthode de classe est l'opérateur 'this'. Toutes les méthodes de classe sont converties en méthodes individuelles avec le nom mangling et le premier paramètre comme objet sur lequel il opère. L'argument 'this' indique la taille de l'objet, ce qui, en cas de définition de la classe de modèle, n'est pas disponible pour le compilateur, sauf si l'utilisateur instancie l'objet avec un argument de type valide. Dans ce cas, si vous placez les définitions de méthode dans un fichier cpp séparé et essayez de le compiler, le fichier objet lui-même ne sera pas généré avec les informations de classe. La compilation n'échouera pas, elle générera le fichier objet mais ne générera aucun code pour la classe de modèle dans le fichier objet. C'est la raison pour laquelle l'éditeur de liens est incapable de trouver les symboles dans les fichiers objets et la construction échoue. 

Maintenant, quelle est l'alternative pour masquer les détails d'implémentation importants? Comme nous le savons tous, l’objectif principal de la séparation de l’interface de l’implémentation est de masquer les détails de l’implémentation sous forme binaire. C'est ici que vous devez séparer les structures de données et les algorithmes. Vos classes de modèles ne doivent représenter que des structures de données, pas les algorithmes. Cela vous permet de masquer des détails d'implémentation plus précieux dans des bibliothèques de classes distinctes non modélisées, les classes à l'intérieur de celles-ci pouvant fonctionner sur les classes de modèle ou les utiliser simplement pour contenir des données. La classe de modèle contiendrait en réalité moins de code à affecter, récupérer et définir des données. Le reste du travail serait effectué par les classes d'algorithmes. 

J'espère que cette discussion sera utile.

136
Sharjith N.

Il est possible, tant que vous savez quelles instanciations vous allez avoir besoin.

Ajoutez le code suivant à la fin de stack.cpp et ça fonctionnera:

template class stack<int>;

Toutes les méthodes de pile sans modèle seront instanciées et l'étape de liaison fonctionnera correctement.

77
Benoît

Vous pouvez le faire de cette façon

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Cela a été discuté dans Daniweb

Egalement dans FAQ mais en utilisant le mot-clé d'exportation C++.

8
Sadanand

Non, ce n'est pas possible Pas sans le mot clé export qui, à toutes fins utiles, n'existe pas vraiment. 

Le mieux que vous puissiez faire est de placer vos implémentations de fonctions dans un fichier ".tcc" ou ".tpp", et d'inclure # le fichier .tcc à la fin de votre fichier .hpp. Cependant, ceci n’est que cosmétique; c'est toujours la même chose que de tout mettre en œuvre dans les fichiers d'en-tête. C'est simplement le prix que vous payez pour utiliser des modèles.

6
Charles Salvia

Je crois qu'il y a deux raisons principales pour essayer de séparer le code basé sur un modèle en un en-tête et un cpp:

On est pour la simple élégance. Nous aimons tous écrire du code qu'il est difficile de lire, de gérer et qui est réutilisable plus tard.

Autre est la réduction des temps de compilation.

Je suis actuellement (comme toujours) en train de coder un logiciel de simulation avec OpenCL et nous aimons conserver le code afin qu'il puisse être exécuté avec des types float (cl_float) ou double (cl_double) selon les besoins, en fonction de la capacité matérielle. Pour le moment, cela se fait en utilisant un #define REAL au début du code, mais ce n’est pas très élégant. Changer la précision souhaitée nécessite une recompilation de l'application. Comme il n’existe pas de types d’exécution réels, nous devons vivre avec cela pour le moment. Heureusement, les noyaux OpenCL sont compilés à l'exécution, et un simple sizeof (REAL) nous permet de modifier le code d'exécution du noyau en conséquence.

Le problème beaucoup plus important est que, même si l'application est modulaire, il faut également utiliser des modèles lors du développement de classes auxiliaires (telles que celles qui précalculent les constantes de simulation). Ces classes apparaissent toutes au moins une fois en haut de l’arborescence des dépendances de classes, car la classe de modèle finale Simulation aura une instance de l’une de ces classes d’usine, ce qui signifie que pratiquement chaque fois que je modifie légèrement la classe d’usine, le logiciel doit être reconstruit. C'est très agaçant, mais je n'arrive pas à trouver une meilleure solution.

3
Meteorhead

Il est parfois possible de cacher la plupart des implémentations dans le fichier cpp, si vous pouvez extraire les fonctionnalités communes de tous les paramètres de gabarit dans une classe non-gabarit (éventuellement avec type-unsafe). Alors en-tête contiendra les appels de redirection à cette classe. Une approche similaire est utilisée lorsqu’on se bat avec le problème de "template bloat".

2
Konstantin Tenzin

Le problème est qu'un modèle ne génère pas de classe réelle, c'est juste un template indiquant au compilateur comment générer une classe. Vous devez générer une classe concrète.

La méthode la plus simple et la plus naturelle consiste à placer les méthodes dans le fichier d’en-tête. Mais il y a un autre chemin.

Dans votre fichier .cpp, si vous avez une référence à chaque instanciation de modèle et à chaque méthode dont vous avez besoin, le compilateur les générera pour utilisation dans votre projet.

new stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}
2
Mark Ransom

Si vous connaissez les types avec lesquels votre pile sera utilisée, vous pouvez les instancier explicitement dans le fichier cpp et y conserver tout le code pertinent.

Il est également possible d'exporter celles-ci à travers des DLL (!), Mais il est assez difficile d'obtenir la syntaxe correcte (combinaisons spécifiques à MS de __declspec (dllexport) et du mot clé export).

Nous avons utilisé cela dans une librairie math/geom utilisant un modèle double/float, mais contenant beaucoup de code. (J'ai cherché sur Google à l'époque, mais je n'ai pas ce code aujourd'hui.)

2
Macke

C'est une question assez ancienne, mais je pense qu'il est intéressant de regarder la présentation de Arthur O'Dwyer au cppcon 2016 . Bonne explication, beaucoup de sujet couvert, à surveiller absolument.

1
FreeYourSoul

Vous devez tout avoir dans le fichier hpp. Le problème est que les classes ne sont pas créées tant que le compilateur n'a pas constaté qu'elles sont nécessaires à un fichier OTHER cpp. Il doit donc disposer de tout le code disponible pour compiler la classe basée sur un modèle à ce moment-là.

Une chose que j’ai tendance à faire est d’essayer de scinder mes modèles en une partie générique non modélisée (qui peut être scindée entre cpp/hpp) et en une partie de modèle spécifique au type qui hérite de la classe non modélisée.

1
Aaron

Seulement si vous #include "stack.cpp à la fin de stack.hpp. Je ne recommanderais cette approche que si l'implémentation est relativement grande et si vous renommez le fichier .cpp en une autre extension, afin de le différencier du code standard.

1
lyricat

Comme les modèles sont compilés lorsque cela est nécessaire, cela impose une restriction pour les projets multi-fichiers: l'implémentation (définition) d'une classe de modèles ou d'une fonction doit figurer dans le même fichier que sa déclaration. Cela signifie que nous ne pouvons pas séparer l'interface dans un fichier d'en-tête séparé et que nous devons inclure l'interface et la mise en œuvre dans tout fichier utilisant les modèles.

0
ChadNC

1) N'oubliez pas que la raison principale pour séparer les fichiers .h et .cpp est de masquer l'implémentation de la classe en tant que code Obj compilé séparément pouvant être lié au code de l'utilisateur contenant un .h de la classe.

2) Les classes non modèles ont toutes les variables définies de manière concrète et spécifique dans les fichiers .h et .cpp. Ainsi, le compilateur aura les informations nécessaires sur tous les types de données utilisés dans la classe avant la compilation/traduction  la génération du code objet/machine. le type de données requis: 

        TClass<int> myObj;

3) Ce n’est qu’après cette instanciation que le fournisseur génère la version spécifique de la classe de modèles correspondant aux types de données transmis.

4) Par conséquent, .cpp NE PEUT PAS être compilé séparément sans connaître le type de données spécifique à l'utilisateur. Donc, il doit rester en tant que code source dans “.h” jusqu'à ce que l'utilisateur spécifie le type de données requis, puis, il peut être généré pour un type de données spécifique puis compilé

0
Aaron01

Une autre possibilité est de faire quelque chose comme:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

Je n'aime pas cette suggestion pour des raisons de style, mais cela peut vous convenir.

0
luke

Le mot clé "export" est le moyen de séparer l'implémentation de modèle de la déclaration de modèle. Cela a été introduit en standard C++ sans implémentation existante. En temps voulu, seuls deux compilateurs l’ont réellement implémenté. Lire des informations détaillées sur Informer un article informatique sur l'exportation

0
Shailesh Kumar