Dividere le classi C ++ basate su modelli in file .hpp / .cpp: è possibile?


96

Ricevo errori nel tentativo di compilare una classe template C ++ suddivisa tra un file .hppe .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  

Ecco il mio codice:

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è ovviamente corretto: i simboli non sono presenti stack.o.

La risposta a questa domanda non aiuta, poiché sto già facendo come si dice.
Questo potrebbe aiutare, ma non voglio spostare ogni singolo metodo nel .hppfile: non dovrei farlo, vero?

L'unica soluzione ragionevole è spostare tutto nel .cppfile nel .hppfile e includere semplicemente tutto, invece di collegarlo come un file oggetto autonomo? Sembra terribilmente brutto! In quel caso, potrei anche tornare al mio stato precedente e rinominare stack.cppper stack.hppe da fare con esso.


Ci sono due ottime soluzioni alternative per quando vuoi davvero mantenere il tuo codice nascosto (in un file binario) o tenerlo pulito. È necessario ridurre la generalità anche se nella prima situazione. Si è spiegato qui: stackoverflow.com/questions/495021/...
Sheric

Risposte:


151

Non è possibile scrivere l'implementazione di una classe template in un file cpp separato e compilare. Tutti i modi per farlo, se qualcuno afferma, sono soluzioni alternative per imitare l'uso di un file cpp separato ma praticamente se si intende scrivere una libreria di classi modello e distribuirla con file di intestazione e lib per nascondere l'implementazione, semplicemente non è possibile .

Per sapere perché, esaminiamo il processo di compilazione. I file di intestazione non vengono mai compilati. Sono solo preelaborati. Il codice preelaborato viene quindi copiato con il file cpp che viene effettivamente compilato. Ora, se il compilatore deve generare il layout di memoria appropriato per l'oggetto, deve conoscere il tipo di dati della classe template.

In realtà si deve capire che la classe template non è affatto una classe ma un template per una classe la cui dichiarazione e definizione viene generata dal compilatore in fase di compilazione dopo aver ottenuto le informazioni del tipo di dati dall'argomento. Finché non è possibile creare il layout di memoria, le istruzioni per la definizione del metodo non possono essere generate. Ricorda che il primo argomento del metodo di classe è l'operatore "this". Tutti i metodi di classe vengono convertiti in metodi individuali con modifica del nome e il primo parametro come oggetto su cui opera. L'argomento 'this' è che in realtà indica la dimensione dell'oggetto che, in caso di classe template, non è disponibile per il compilatore a meno che l'utente non installi l'oggetto con un argomento di tipo valido. In questo caso, se metti le definizioni del metodo in un file cpp separato e provi a compilarlo, il file oggetto stesso non verrà generato con le informazioni sulla classe. La compilazione non fallirà, genererà il file oggetto ma non genererà alcun codice per la classe template nel file oggetto. Questo è il motivo per cui il linker non riesce a trovare i simboli nei file oggetto e la generazione non riesce.

Qual è l'alternativa per nascondere importanti dettagli di implementazione? Come tutti sappiamo l'obiettivo principale dietro la separazione dell'interfaccia dall'implementazione è nascondere i dettagli dell'implementazione in forma binaria. Qui è dove devi separare le strutture dati e gli algoritmi. Le classi del modello devono rappresentare solo le strutture di dati, non gli algoritmi. Ciò consente di nascondere dettagli di implementazione più preziosi in librerie di classi separate non templatizzate, le classi all'interno delle quali funzionerebbero sulle classi modello o semplicemente le userebbero per contenere dati. La classe template conterrebbe effettivamente meno codice per assegnare, ottenere e impostare i dati. Il resto del lavoro sarebbe svolto dalle classi di algoritmi.

Spero che questa discussione possa essere utile.


2
"si deve capire che la classe template non è affatto una classe" - non era il contrario? Il modello di classe è un modello. La "classe modello" viene talvolta utilizzata al posto di "istanziazione di un modello" e sarebbe una classe effettiva.
Xupicor

Solo per riferimento, non è corretto dire che non ci sono soluzioni alternative! Anche separare le strutture dati dai metodi è una cattiva idea in quanto si oppone all'incapsulamento. C'è una grande soluzione alternativa che puoi usare in alcune situazioni (credo la maggior parte) qui: stackoverflow.com/questions/495021/…
Sheric

@Xupicor, hai ragione. Tecnicamente "Class Template" è ciò che scrivi in ​​modo da poter istanziare una "Template Class" e il suo oggetto corrispondente. Tuttavia, credo che in una terminologia generica, utilizzare entrambi i termini in modo intercambiabile non sarebbe poi così sbagliato, la sintassi per definire il "modello di classe" stessa inizia con la parola "modello" e non "classe".
Sharjith N.

@Sheric, non ho detto che non ci sono soluzioni alternative. In effetti, tutto ciò che è disponibile sono solo soluzioni alternative per simulare la separazione tra interfaccia e implementazione in caso di classi modello. Nessuna di queste soluzioni alternative funziona senza creare un'istanza di una classe di modello tipizzata specifica. Ciò comunque dissolve l'intero punto di genericità dell'uso dei modelli di classe. Separare le strutture dati dagli algoritmi non è la stessa cosa che separare le strutture dati dai metodi. Le classi di strutture dati possono benissimo avere metodi come costruttori, getter e setter.
Sharjith N.

La cosa più vicina che ho appena trovato per fare questo lavoro è usare un paio di file .h / .hpp e #include "filename.hpp" alla fine del file .h che definisce la classe del modello. (sotto la parentesi graffa di chiusura per la definizione della classe con il punto e virgola). Questo almeno li separa strutturalmente a livello di file, ed è consentito perché alla fine, il compilatore copia / incolla il tuo codice .hpp sul tuo #include "filename.hpp".
Artorias2718

90

Si è possibile, a patto che si sa cosa istanze che si sta per necessità.

Aggiungi il seguente codice alla fine di stack.cpp e funzionerà:

template class stack<int>;

Tutti i metodi di stack non modello verranno istanziati e il passaggio di collegamento funzionerà correttamente.


7
In pratica, la maggior parte delle persone usa un file cpp separato per questo, qualcosa come stackinstantiations.cpp.
Nemanja Trifunovic

@NemanjaTrifunovic puoi fare un esempio di come apparirebbe stackinstantiations.cpp?
qwerty9967

3
In realtà ci sono altre soluzioni: codeproject.com/Articles/48575/…
sleepsort

@ Benoît Ho ricevuto un errore di errore: atteso unqualified-id prima di ';' stack di modelli di token <int>; Sai perché? Grazie!
camino

3
In realtà, la sintassi corretta è template class stack<int>;.
Paul Baltescu

8

Puoi farlo in questo modo

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

Questo è stato discusso in Daniweb

Anche nelle FAQ ma utilizzando la parola chiave di esportazione C ++.


5
includeing un cppfile è generalmente un'idea terribile. anche se hai una ragione valida per questo, al file - che in realtà è solo un'intestazione glorificata - dovrebbe essere data un'estensione hppo un'estensione diversa (ad esempio tpp) per rendere molto chiaro cosa sta succedendo, rimuovere la confusione intorno a makefiles che mirano a file reali cpp , ecc.
underscore_d

@underscore_d Potresti spiegare perché includere un .cppfile è un'idea terribile?
Abbas

1
@Abbas perché l'estensione cpp(o cc, o c, o qualsiasi altra cosa) indica che il file è una parte dell'implementazione, che l'unità di traduzione risultante (output del preprocessore) è compilabile separatamente e che il contenuto del file viene compilato una sola volta. non indica che il file è una parte riutilizzabile dell'interfaccia, da includere arbitrariamente ovunque. #includel'inserimento di un file effettivo cpp riempirebbe rapidamente lo schermo con più errori di definizione, e giustamente. in questo caso, in quanto v'è un motivo per #includeesso, cppera solo la scelta sbagliata di estensione.
underscore_d

@underscore_d Quindi fondamentalmente è sbagliato usare solo l' .cppestensione per tale uso. Ma usare un'altra parola .tppva completamente bene, che servirebbe allo stesso scopo ma utilizzare un'estensione diversa per una comprensione più facile / rapida?
Abbas

1
@Abbas Sì, cpp/ cc/ etc devono essere evitati, ma è una buona idea di utilizzare qualcosa di diverso hpp- per esempio tpp, tcce così via - in modo da poter riutilizzare il resto del nome del file e indicare che il tppfile anche se si comporta come un colpo di testa, contiene l'implementazione fuori linea delle dichiarazioni del modello nel file corrispondente hpp. Quindi questo post inizia con una buona premessa - separare dichiarazioni e definizioni in 2 file diversi, che può essere più facile da grok / grep o talvolta è richiesto a causa di dipendenze circolari IME - ma poi finisce male suggerendo che il secondo file ha un'estensione sbagliata
underscore_d

6

No, non è possibile. Non senza la exportparola chiave, che a tutti gli effetti non esiste realmente.

Il meglio che puoi fare è mettere le implementazioni delle tue funzioni in un file ".tcc" o ".tpp" e #include il file .tcc alla fine del tuo file .hpp. Tuttavia questo è solo cosmetico; è sempre lo stesso dell'implementazione di tutto nei file di intestazione. Questo è semplicemente il prezzo da pagare per l'utilizzo dei modelli.


3
La tua risposta non è corretta. Puoi generare codice da una classe template in un file cpp, dato che sai quali argomenti del template usare. Vedere la mia risposta per ulteriori informazioni.
Benoît

2
Vero, ma questo viene fornito con la seria limitazione di dover aggiornare il file .cpp e ricompilarlo ogni volta che viene introdotto un nuovo tipo che utilizza il modello, che probabilmente non è quello che l'OP aveva in mente.
Charles Salvia

3

Credo che ci siano due ragioni principali per provare a separare il codice basato su modelli in un'intestazione e in un cpp:

Uno è per pura eleganza. A tutti noi piace scrivere codice che sia facile da leggere, gestire e riutilizzabile in seguito.

Altro è la riduzione dei tempi di compilazione.

Attualmente (come sempre) sto codificando un software di simulazione in combinazione con OpenCL e ci piace mantenere il codice in modo che possa essere eseguito utilizzando i tipi float (cl_float) o double (cl_double) secondo necessità a seconda delle capacità HW. In questo momento questo viene fatto usando un #define REAL all'inizio del codice, ma non è molto elegante. La modifica della precisione desiderata richiede la ricompilazione dell'applicazione. Dal momento che non esistono tipi di runtime reali, dobbiamo convivere con questo per il momento. Fortunatamente i kernel OpenCL sono runtime compilati e un semplice sizeof (REAL) ci consente di modificare di conseguenza il runtime del codice del kernel.

Il problema molto più grande è che anche se l'applicazione è modulare, quando si sviluppano classi ausiliarie (come quelle che pre-calcolano le costanti di simulazione) devono essere anche modellate. Queste classi appaiono tutte almeno una volta in cima all'albero delle dipendenze della classe, poiché la simulazione della classe modello finale avrà un'istanza di una di queste classi factory, il che significa che praticamente ogni volta che apporto una piccola modifica alla classe factory, l'intera il software deve essere ricostruito. Questo è molto fastidioso, ma non riesco a trovare una soluzione migliore.


2

Solo se #include "stack.cppalla fine di stack.hpp. Consiglierei questo approccio solo se l'implementazione è relativamente grande e se rinomini il file .cpp con un'altra estensione, per differenziarlo dal codice normale.


4
Se stai facendo questo, ti consigliamo di aggiungere #ifndef STACK_CPP (e amici) al tuo file stack.cpp.
Stephen Newell,

Battimi a questo suggerimento. Anch'io non preferisco questo approccio per motivi di stile.
Luca

2
Sì, in tal caso, al secondo file non dovrebbe assolutamente essere assegnata l'estensione cpp( cco qualsiasi altra cosa) perché è in netto contrasto con il suo vero ruolo. Dovrebbe invece essere data un'estensione diversa che indica che è (A) un'intestazione e (B) un'intestazione da includere nella parte inferiore di un'altra intestazione. Io uso tppper questo, che può tranquillamente anche significare tem plate im plementation (definizioni fuori linea). Ho divagato di più su questo qui: stackoverflow.com/questions/1724036/…
underscore_d

2

A volte è possibile avere la maggior parte dell'implementazione nascosta nel file cpp, se è possibile estrarre funzionalità comuni da tutti i parametri del modello in una classe non modello (possibilmente non sicura per il tipo). Quindi l'intestazione conterrà le chiamate di reindirizzamento a quella classe. Un approccio simile viene utilizzato quando si combatte con il problema del "template bloat".


+1 - anche se la maggior parte delle volte non funziona molto bene (almeno, non tutte le volte che desidero)
peterchen

2

Se sai con quali tipi verrà utilizzato il tuo stack, puoi istanziarli esplicitamente nel file cpp e conservare lì tutto il codice pertinente.

È anche possibile esportarli attraverso DLL (!), Ma è piuttosto difficile ottenere la sintassi corretta (combinazioni specifiche di MS di __declspec (dllexport) e la parola chiave export).

L'abbiamo usato in una libreria matematica / geom che utilizzava modelli double / float, ma aveva un bel po 'di codice. (All'epoca ho cercato su Google per questo, ma oggi non ho quel codice.)


2

Il problema è che un modello non genera una classe reale, è solo un modello dice al compilatore come generare una classe. Devi generare una classe concreta.

Il modo più semplice e naturale è inserire i metodi nel file di intestazione. ma c'è un altro modo.

Nel tuo file .cpp, se hai un riferimento a ogni istanza di modello e metodo di cui hai bisogno, il compilatore li genererà lì per l'uso durante il tuo progetto.

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
Non hai bisogno della funzione dummey: usa 'template stack <int>;' Ciò forza un'istanziazione del modello nell'unità di compilazione corrente. Molto utile se definisci un modello ma desideri solo un paio di implementazioni specifiche in una libreria condivisa.
Martin York

@ Martin: comprese tutte le funzioni membro? È fantastico. Dovresti aggiungere questo suggerimento al thread "funzionalità C ++ nascoste".
Mark Ransom

@LokiAstari Ho trovato un articolo su questo nel caso qualcuno volesse saperne di più: cplusplus.com/forum/articles/14272
Andrew Larsson

1

Devi avere tutto nel file hpp. Il problema è che le classi non vengono effettivamente create finché il compilatore non vede che sono necessarie a un ALTRO file cpp, quindi deve avere tutto il codice disponibile per compilare la classe basata su modelli in quel momento.

Una cosa che tendo a fare è provare a dividere i miei modelli in una parte generica non basata su modelli (che può essere divisa tra cpp / hpp) e la parte modello specifica del tipo che eredita la classe non basata su modelli.


0

Poiché i modelli vengono compilati quando richiesto, ciò impone una restrizione per i progetti con più file: l'implementazione (definizione) di una classe o funzione modello deve essere nello stesso file della sua dichiarazione. Ciò significa che non possiamo separare l'interfaccia in un file di intestazione separato e che dobbiamo includere sia l'interfaccia che l'implementazione in qualsiasi file che utilizza i modelli.


0

Un'altra possibilità è fare qualcosa come:

#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

Non mi piace questo suggerimento per una questione di stile, ma potrebbe essere adatto a te.


1
La seconda intestazione glorificata inclusa dovrebbe almeno avere un'estensione diversa da quella cppper evitare confusione con i file sorgente effettivi . I suggerimenti comuni includono tppe tcc.
underscore_d

0

La parola chiave "export" è il modo per separare l'implementazione del modello dalla dichiarazione del modello. Questo è stato introdotto nello standard C ++ senza un'implementazione esistente. A tempo debito solo un paio di compilatori lo hanno effettivamente implementato. Leggere le informazioni approfondite nell'articolo Inform IT sull'esportazione


1
Questa è quasi una risposta solo di collegamento e quel collegamento è morto.
underscore_d

0

1) Ricorda che il motivo principale per separare i file .he .cpp è nascondere l'implementazione della classe come codice Obj compilato separatamente che può essere collegato al codice dell'utente che includeva un .h della classe.

2) Le classi non template hanno tutte le variabili concretamente e specificatamente definite nei file .h e .cpp. Quindi il compilatore avrà le informazioni necessarie su tutti i tipi di dati usati nella classe prima di compilare / tradurre  generare l'oggetto / codice macchina Le classi dei modelli non hanno informazioni sul tipo di dati specifico prima che l'utente della classe installi un oggetto che passa i dati richiesti genere:

        TClass<int> myObj;

3) Solo dopo questa istanza, il compilatore genera la versione specifica della classe template per abbinare i tipi di dati passati.

4) Pertanto, .cpp NON può essere compilato separatamente senza conoscere il tipo di dati specifico dell'utente. Quindi deve rimanere come codice sorgente all'interno di ".h" finché l'utente non specifica il tipo di dati richiesto, quindi può essere generato su un tipo di dati specifico e quindi compilato


0

Il punto in cui potresti volerlo fare è quando crei una libreria e una combinazione di intestazione e nascondi l'implementazione all'utente. Pertanto, l'approccio suggerito consiste nell'utilizzare l'istanza esplicita, poiché si sa cosa dovrebbe fornire il software e si possono nascondere le implementazioni.

Alcune informazioni utili sono qui: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Per il tuo stesso esempio: 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;
}

Produzione:

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

Tuttavia non mi piace del tutto questo approccio, perché consente all'applicazione di spararsi ai piedi, passando tipi di dati errati alla classe basata su modelli. Ad esempio, nella funzione main, puoi passare altri tipi che possono essere convertiti implicitamente in int come s.Push (1.2); e questo è solo un male secondo me.


-3

Sto lavorando con Visual Studio 2010, se desideri dividere i tuoi file in .h e .cpp, includi la tua intestazione cpp alla fine del file .h

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.