Diviser les classes C ++ modèles en fichiers .hpp / .cpp - est-ce possible?


96

Je reçois des erreurs en essayant de compiler une classe de modèle C ++ qui est divisée entre un .hppet un .cppfichier:

$ 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;
}

ldest bien sûr correct: les symboles ne sont pas inclus stack.o.

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

La seule solution raisonnable est-elle de déplacer tout ce qui se trouve dans le .cppfichier vers le .hppfichier, et de tout inclure simplement, plutôt que de créer un lien en tant que fichier objet autonome? Cela semble terriblement moche! Dans ce cas, je pourrais aussi bien revenir à mon état précédent et renommage stack.cppà stack.hppet faire avec elle.


Il existe deux excellentes solutions de contournement lorsque vous voulez vraiment garder votre code caché (dans un fichier binaire) ou le garder propre. Il est nécessaire de réduire la généralité bien que dans la première situation. Il est expliqué ici: stackoverflow.com/questions/495021/…
Sheric

Réponses:


151

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 façons 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 d'en-tête et de bibliothèque pour masquer l'implémentation, ce n'est tout simplement pas possible .

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

En fait, il faut comprendre que la classe modèle n'est pas du tout une classe mais un modèle pour une 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 disposition de la mémoire ne peut pas être créée, les instructions pour la définition de méthode ne peuvent pas être générées. Souvenez-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 en fait la taille de l'objet qui, en cas de classe de modèle, n'est pas disponible pour le compilateur à moins que 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 elle ne générera aucun code pour la classe 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 génération échoue.

Maintenant, quelle est l'alternative pour masquer les détails d'implémentation importants? Comme nous le savons tous, l'objectif principal derrière la séparation de l'interface de l'implémentation est de cacher les détails de l'implémentation sous forme binaire. C'est là que vous devez séparer les structures de données et les algorithmes. Vos classes de modèles doivent représenter uniquement des structures de données et non 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 qui fonctionneraient sur les classes de modèles ou simplement les utiliser pour contenir des données. La classe de modèle contiendrait en fait moins de code pour affecter, obtenir 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.


2
"il faut comprendre que la classe modèle n'est pas du tout une classe" - n'était-ce pas l'inverse? Le modèle de classe est un modèle. La "classe de modèle" est parfois utilisée à la place de "l'instanciation d'un modèle", et serait une classe réelle.
Xupicor

Juste pour référence, il n'est pas correct de dire qu'il n'y a pas de solutions de contournement! Séparer les structures de données des méthodes est également une mauvaise idée car elle s'oppose à l'encapsulation. Il existe une excellente solution de contournement que vous pouvez utiliser dans certaines situations (je crois que la plupart) ici: stackoverflow.com/questions/495021/…
Sheric

@Xupicor, vous avez raison. Techniquement, «modèle de classe» est ce que vous écrivez pour pouvoir instancier une «classe de modèle» et son objet correspondant. Cependant, je crois que dans une terminologie générique, utiliser les deux termes de manière interchangeable ne serait pas du tout faux, la syntaxe pour définir le "modèle de classe" lui-même commence par le mot "modèle" et non par "classe".
Sharjith N.

@Sheric, je n'ai pas dit qu'il n'y avait pas de solutions de contournement. En fait, tout ce qui est disponible ne sont que des solutions de contournement pour imiter la séparation de l'interface et de l'implémentation dans le cas de classes de modèles. Aucune de ces solutions de contournement ne fonctionne sans instancier une classe de modèle typée spécifique. Cela dissout de toute façon tout le point de généricité de l'utilisation des modèles de classe. Séparer les structures de données des algorithmes n'est pas la même chose que séparer les structures de données des méthodes. Les classes de structure de données peuvent très bien avoir des méthodes telles que des constructeurs, des getters et des setters.
Sharjith N.,

La chose la plus proche que je viens de trouver pour faire ce travail est d'utiliser une paire de fichiers .h / .hpp, et #include "filename.hpp" à la fin du fichier .h définissant votre classe de modèle. (sous votre accolade fermante pour la définition de classe avec le point-virgule). Cela les sépare au moins structurellement au niveau des fichiers, et est autorisé car à la fin, le compilateur copie / colle votre code .hpp sur votre #include "filename.hpp".
Artorias2718 le

90

Il est possible, aussi longtemps que vous savez ce que vous allez Instantiations besoin.

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

template class stack<int>;

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


7
En pratique, la plupart des gens utilisent un fichier cpp séparé pour cela - quelque chose comme stackinstantiations.cpp.
Nemanja Trifunovic

@NemanjaTrifunovic pouvez-vous donner un exemple de ce à quoi ressemblerait stackinstantiations.cpp?
qwerty9967

3
En fait , il existe d' autres solutions: codeproject.com/Articles/48575/...
sleepsort

@ Benoît J'ai eu une erreur d'erreur: attendu unqualified-id avant ';' pile de modèles de jetons <int>; Est-ce que tu sais pourquoi? Merci!
camino du

3
En fait, la syntaxe correcte est template class stack<int>;.
Paul Baltescu

8

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

Aussi dans la FAQ mais en utilisant le mot clé d'exportation C ++.


5
includeun cppfichier est généralement une mauvaise idée. même si vous avez une raison valable pour cela, le fichier - qui n'est en réalité qu'un en-tête glorifié - devrait recevoir une hppextension ou une extension différente (par exemple tpp) pour clarifier ce qui se passe, éliminer la confusion autour du makefileciblage des cpp fichiers réels , etc.
underscore_d

@underscore_d Pouvez-vous expliquer pourquoi l'inclusion d'un .cppfichier est une mauvaise idée?
Abbas

1
@Abbas parce que l'extension cpp(ou cc, ou c, ou autre) indique que le fichier est une partie de l'implémentation, que l'unité de traduction résultante (sortie du préprocesseur) est compilable séparément et que le contenu du fichier n'est compilé qu'une seule fois. cela n'indique pas que le fichier est une partie réutilisable de l'interface, à inclure arbitrairement n'importe où. #includeun fichier réel cpp remplirait rapidement votre écran de multiples erreurs de définition, et à juste titre. dans ce cas, comme il y a une raison à #includecela, cppc'était juste le mauvais choix d'extension.
underscore_d

@underscore_d Donc, fondamentalement, il est faux d'utiliser l' .cppextension pour une telle utilisation. Mais utiliser un autre mot à dire .tppest tout à fait correct, qui servirait le même but mais utiliserait une extension différente pour une compréhension plus facile / plus rapide?
Abbas

1
@Abbas Oui, cpp/ cc/ etc doivent être évités, mais il est une bonne idée d'utiliser autre chose que hpp- par exemple tpp, tccetc. - afin que vous puissiez réutiliser le reste du nom de fichier et indiquer que le tppfichier, bien qu'il agit comme un en- tête, contient l'implémentation hors ligne des déclarations de modèle dans le fichier hpp. Donc, cet article commence par une bonne prémisse - séparer les déclarations et les définitions en 2 fichiers différents, ce qui peut être plus facile à grok / grep ou parfois est nécessaire en raison des dépendances circulaires IME - mais se termine mal en suggérant que le 2ème fichier a une mauvaise extension
underscore_d

6

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

Le mieux que vous puissiez faire est de placer vos implémentations de fonction dans un fichier «.tcc» ou «.tpp», et #incluez le fichier .tcc à la fin de votre fichier .hpp. Cependant, ce n'est que cosmétique; c'est toujours la même chose que l'implémentation de tout dans les fichiers d'en-tête. C'est simplement le prix que vous payez pour utiliser des modèles.


3
Votre réponse n'est pas correcte. Vous pouvez générer du code à partir d'une classe de modèle dans un fichier cpp, étant donné que vous savez quels arguments de modèle utiliser. Voir ma réponse pour plus d'informations.
Benoît

2
C'est vrai, mais cela vient avec la restriction sérieuse d'avoir besoin de mettre à jour le fichier .cpp et de le recompiler chaque fois qu'un nouveau type est introduit qui utilise le modèle, ce qui n'est probablement pas ce que l'OP avait à l'esprit.
Charles Salvia

3

Je pense 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:

L'un est pour la simple élégance. Nous aimons tous écrire du code qui est difficile à lire, à gérer et qui est réutilisable plus tard.

L'autre est la réduction des temps de compilation.

Je suis actuellement (comme toujours) un logiciel de simulation de codage en conjonction avec OpenCL et nous aimons conserver le code afin qu'il puisse être exécuté en utilisant des types float (cl_float) ou double (cl_double) selon les besoins en fonction de la capacité HW. 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. La modification de la précision souhaitée nécessite la recompilation de l'application. Puisqu'il n'y a pas de vrais types d'exécution, nous devons vivre avec cela pour le moment. Heureusement, les noyaux OpenCL sont compilés à l'exécution, et une simple sizeof (REAL) nous permet de modifier le temps d'exécution du code du noyau en conséquence.

Le problème beaucoup plus important est que même si l'application est modulaire, lors du développement de classes auxiliaires (telles que celles qui précalculent les constantes de simulation) doivent également être modélisées. Ces classes apparaissent toutes au moins une fois en haut de l'arborescence des dépendances de classe, 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 j'apporte un changement mineur à la classe d'usine, l'ensemble le logiciel doit être reconstruit. C'est très ennuyeux, mais je n'arrive pas à trouver une meilleure solution.


2

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


4
Si vous faites cela, vous voudrez ajouter #ifndef STACK_CPP (et ses amis) à votre fichier stack.cpp.
Stephen Newell

Battez-moi à cette suggestion. Je ne préfère pas non plus cette approche pour des raisons de style.
luke

2
Oui, dans un tel cas, le 2ème fichier ne devrait certainement pas recevoir l'extension cpp( ccou quoi que ce soit) car c'est un contraste frappant avec son rôle réel. Il devrait plutôt recevoir une extension différente qui indique que c'est (A) un en-tête et (B) un en-tête à inclure au bas d'un autre en-tête. Je l' utilise tpppour cela, qui peut également haut la main pour se tem pfin im plementation (définitions hors ligne). J'ai parlé plus à ce sujet ici: stackoverflow.com/questions/1724036/…
underscore_d

2

Parfois, il est possible d'avoir la plupart de l'implémentation cachée dans le fichier cpp, si vous pouvez extraire les fonctionnalités communes de tous les paramètres de modèle dans une classe non-modèle (éventuellement de type non sécurisé). L'en-tête contiendra alors les appels de redirection vers cette classe. Une approche similaire est utilisée lors de la lutte contre le problème de "gonflement du modèle".


+1 - même si cela ne fonctionne pas très bien la plupart du temps (du moins, pas aussi souvent que je le souhaite)
peterchen

2

Si vous savez avec quels types 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 de les exporter à travers des DLL (!) Mais il est assez difficile d'obtenir la bonne syntaxe (combinaisons spécifiques à MS de __declspec (dllexport) et du mot-clé d'exportation).

Nous l'avons utilisé dans une bibliothèque mathématique / geom qui avait un modèle double / float, mais qui contenait beaucoup de code. (Je l'ai cherché sur Google à l'époque, je n'ai pas ce code aujourd'hui.)


2

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

La manière simple et naturelle est de 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 et méthode de modèle dont vous avez besoin, le compilateur les générera là-bas pour une utilisation tout au long de 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
}

8
Vous n'avez pas besoin de la fonction dummey: utilisez 'template stack <int>;' Cela force une instanciation du modèle dans l'unité de compilation actuelle. Très utile si vous définissez un modèle mais que vous ne voulez que quelques implémentations spécifiques dans une bibliothèque partagée.
Martin York

@Martin: y compris toutes les fonctions membres? C'est fantastique. Vous devez ajouter cette suggestion au thread "Fonctionnalités C ++ cachées".
Mark Ransom

@LokiAstari J'ai trouvé un article à ce sujet au cas où quelqu'un voudrait en savoir plus: cplusplus.com/forum/articles/14272
Andrew Larsson

1

Vous devez avoir tout dans le fichier hpp. Le problème est que les classes ne sont réellement créées que lorsque le compilateur voit qu'elles sont nécessaires à un AUTRE fichier cpp - il doit donc avoir 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 diviser mes modèles en une partie générique non basée sur un modèle (qui peut être divisée entre cpp / hpp) et la partie modèle spécifique au type qui hérite de la classe non basée sur un modèle.


0

Étant donné que les modèles sont compilés au besoin, cela force une restriction pour les projets multi-fichiers: l'implémentation (définition) d'une classe ou d'une fonction de modèle doit être 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 à la fois l'interface et l'implémentation dans tout fichier qui utilise les modèles.


0

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 une question de style, mais elle peut vous convenir.


1
Le 2ème en-tête glorifié inclus doit au moins avoir une extension autre que cpppour éviter toute confusion avec les fichiers source réels . Les suggestions courantes incluent tppet tcc.
underscore_d

0

Le mot clé 'export' est le moyen de séparer l'implémentation du modèle de la déclaration du modèle. Cela a été introduit dans la norme C ++ sans implémentation existante. En temps voulu, seuls quelques compilateurs l'ont mis en œuvre. Lisez des informations détaillées dans l' article Inform IT sur l'exportation


1
C'est presque une réponse de lien seulement, et ce lien est mort.
underscore_d

0

1) Rappelez-vous 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 qui peut ê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 concrètement et spécifiquement dans les fichiers .h et .cpp. Ainsi, le compilateur aura besoin d'informations sur tous les types de données utilisés dans la classe avant de compiler / traduire  générer l'objet / code machine Les classes de modèle n'ont aucune information sur le type de données spécifique avant que l'utilisateur de la classe instancie un objet en passant les données requises type:

        TClass<int> myObj;

3) Ce n'est qu'après cette instanciation que le complicateur génère la version spécifique de la classe de modèle pour correspondre au (x) type (s) 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. Il doit donc 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é dans un type de données spécifique puis compilé


0

L'endroit où vous voudrez peut-être faire cela est lorsque vous créez une combinaison de bibliothèque et d'en-tête et que vous masquez l'implémentation à l'utilisateur. Par conséquent, l'approche suggérée consiste à utiliser une instanciation explicite, car vous savez ce que votre logiciel est censé fournir et vous pouvez masquer les implémentations.

Quelques informations utiles sont ici: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Pour votre même exemple: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Production:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

Cependant, je n'aime pas entièrement cette approche, car cela permet à l'application de se tirer une balle dans le pied, en passant des types de données incorrects à la classe modèle. Par exemple, dans la fonction main, vous pouvez passer d'autres types qui peuvent être implicitement convertis en int comme s.Push (1.2); et c'est tout simplement mauvais à mon avis.


-3

Je travaille avec Visual studio 2010, si vous souhaitez diviser vos fichiers en .h et .cpp, incluez votre en-tête cpp à la fin du fichier .h

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.