Aufteilen von C ++ - Vorlagenklassen in .hpp / .cpp-Dateien - ist das möglich?

96

Beim Kompilieren einer C ++ - Vorlagenklasse, die zwischen a .hppund .cppfile aufgeteilt ist, werden Fehler angezeigt :

$ 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  

Hier ist mein 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;
}

ldist natürlich richtig: die symbole sind nicht in stack.o.

Die Antwort auf diese Frage hilft nicht weiter, da ich bereits das tue, was es sagt.
Dies könnte helfen, aber ich möchte nicht jede einzelne Methode in die .hppDatei verschieben - ich sollte es nicht müssen, oder?

Ist die einzig vernünftige Lösung, alles in der .cppDatei in die .hppDatei zu verschieben und einfach alles einzuschließen, anstatt es als eigenständige Objektdatei zu verknüpfen? Das scheint schrecklich hässlich! In diesem Fall könnte ich zurückkommen und zu meinem vorherigen Zustand und Umbenennungs stack.cppzu stack.hppund mit ihr geschehen.

exscape
quelle
Es gibt zwei großartige Problemumgehungen, wenn Sie Ihren Code wirklich versteckt (in einer Binärdatei) oder sauber halten möchten. Es ist notwendig, die Allgemeinheit zu reduzieren, obwohl in der ersten Situation. Es wird hier erklärt: stackoverflow.com/questions/495021/…
Sheric

Antworten:

151

Es ist nicht möglich, die Implementierung einer Vorlagenklasse in eine separate CPP-Datei zu schreiben und zu kompilieren. Alle Möglichkeiten, dies zu tun, sind Problemumgehungen, um die Verwendung einer separaten CPP-Datei nachzuahmen. Wenn Sie jedoch beabsichtigen, eine Vorlagenklassenbibliothek zu schreiben und sie mit Header- und Lib-Dateien zu verteilen, um die Implementierung zu verbergen, ist dies einfach nicht möglich .

Um zu wissen warum, schauen wir uns den Kompilierungsprozess an. Die Header-Dateien werden niemals kompiliert. Sie werden nur vorverarbeitet. Der vorverarbeitete Code wird dann mit der tatsächlich kompilierten CPP-Datei zusammengelegt. Wenn der Compiler nun das entsprechende Speicherlayout für das Objekt generieren muss, muss er den Datentyp der Vorlagenklasse kennen.

Tatsächlich muss verstanden werden, dass die Vorlagenklasse überhaupt keine Klasse ist, sondern eine Vorlage für eine Klasse, deren Deklaration und Definition vom Compiler zur Kompilierungszeit generiert wird, nachdem die Informationen des Datentyps aus dem Argument abgerufen wurden. Solange das Speicherlayout nicht erstellt werden kann, können die Anweisungen für die Methodendefinition nicht generiert werden. Denken Sie daran, dass das erste Argument der Klassenmethode der Operator 'this' ist. Alle Klassenmethoden werden in einzelne Methoden mit Name Mangling und dem ersten Parameter als Objekt konvertiert, mit dem sie arbeiten. Das Argument 'this' gibt die Größe des Objekts an, das für den Compiler nicht verfügbar ist, es sei denn, der Benutzer instanziiert das Objekt mit einem gültigen Typargument. In diesem Fall wird die Objektdatei selbst nicht mit den Klasseninformationen generiert, wenn Sie die Methodendefinitionen in eine separate CPP-Datei einfügen und versuchen, sie zu kompilieren. Die Kompilierung schlägt nicht fehl, sie generiert die Objektdatei, generiert jedoch keinen Code für die Vorlagenklasse in der Objektdatei. Dies ist der Grund, warum der Linker die Symbole in den Objektdateien nicht finden kann und der Build fehlschlägt.

Was ist nun die Alternative, um wichtige Implementierungsdetails zu verbergen? Wie wir alle wissen, besteht das Hauptziel der Trennung von Schnittstelle und Implementierung darin, Implementierungsdetails in binärer Form zu verbergen. Hier müssen Sie die Datenstrukturen und Algorithmen trennen. Ihre Vorlagenklassen dürfen nur Datenstrukturen darstellen, nicht die Algorithmen. Auf diese Weise können Sie wertvollere Implementierungsdetails in separaten Klassenbibliotheken ohne Vorlage verbergen, in denen die Vorlagenklassen funktionieren oder die nur zum Speichern von Daten verwendet werden. Die Vorlagenklasse würde tatsächlich weniger Code zum Zuweisen, Abrufen und Festlegen von Daten enthalten. Der Rest der Arbeit würde von den Algorithmusklassen erledigt.

Ich hoffe, diese Diskussion wäre hilfreich.

Sharjith N.
quelle
2
"Es muss verstanden werden, dass die Vorlagenklasse überhaupt keine Klasse ist" - war es nicht umgekehrt? Klassenvorlage ist eine Vorlage. "Vorlagenklasse" wird manchmal anstelle der "Instanziierung einer Vorlage" verwendet und ist eine tatsächliche Klasse.
Xupicor
Nur als Referenz ist es nicht richtig zu sagen, dass es keine Problemumgehungen gibt! Das Trennen von Datenstrukturen von Methoden ist ebenfalls eine schlechte Idee, da die Kapselung dem entgegenwirkt. Es gibt eine großartige Problemumgehung, die Sie in einigen Situationen (ich glaube die meisten) hier verwenden können: stackoverflow.com/questions/495021/…
Sheric
@ Xupicor, du hast recht. Technisch gesehen schreiben Sie "Klassenvorlage", damit Sie eine "Vorlagenklasse" und das entsprechende Objekt instanziieren können. Ich glaube jedoch, dass in einer generischen Terminologie, bei der beide Begriffe austauschbar verwendet werden, nicht allzu falsch wäre. Die Syntax zum Definieren der "Klassenvorlage" selbst beginnt mit dem Wort "Vorlage" und nicht mit "Klasse".
Sharjith N.
@Sheric, ich habe nicht gesagt, dass es keine Problemumgehungen gibt. Tatsächlich sind nur Problemumgehungen verfügbar, um die Trennung von Schnittstelle und Implementierung bei Vorlagenklassen nachzuahmen. Keine dieser Problemumgehungen funktioniert ohne Instanziierung einer bestimmten typisierten Vorlagenklasse. Das löst ohnehin den ganzen Sinn der Verwendung von Klassenvorlagen auf. Das Trennen von Datenstrukturen von Algorithmen ist nicht dasselbe wie das Trennen von Datenstrukturen von Methoden. Datenstrukturklassen können sehr gut Methoden wie Konstruktoren, Getter und Setter haben.
Sharjith N.
Das nächste, was ich gerade gefunden habe, um diese Arbeit zu machen, ist die Verwendung eines Paares von .h / .hpp-Dateien und #include "filename.hpp" am Ende der .h-Datei, die Ihre Vorlagenklasse definiert. (unter Ihrer schließenden Klammer für die Klassendefinition mit dem Semikolon). Dies trennt sie zumindest strukturell fileweise und ist zulässig, da der Compiler am Ende Ihren .hpp-Code über Ihr #include "filename.hpp" kopiert / einfügt.
Artorias2718
90

Es ist möglich, solange Sie wissen, welche Instanziierungen Sie benötigen werden.

Fügen Sie den folgenden Code am Ende von stack.cpp hinzu und es wird funktionieren:

template class stack<int>;

Alle Nicht-Template-Methoden des Stapels werden instanziiert, und der Verknüpfungsschritt funktioniert einwandfrei.

Benoît
quelle
7
In der Praxis verwenden die meisten Leute dafür eine separate CPP-Datei - so etwas wie stackinstantiations.cpp.
Nemanja Trifunovic
@NemanjaTrifunovic Kannst du ein Beispiel geben, wie stackinstantiations.cpp aussehen würde?
QWERTY9967
3
Tatsächlich gibt es andere Lösungen: codeproject.com/Articles/48575/…
sleepsort
@ Benoît Ich habe einen Fehlerfehler erhalten: Unqualifizierte-ID vor ';' erwartet Token-Vorlagenstapel <int>; Weißt du, warum? Vielen Dank!
Camino
3
Eigentlich ist die richtige Syntax template class stack<int>;.
Paul Baltescu
8

Sie können es auf diese Weise tun

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

Dies wurde in Daniweb diskutiert

Auch in den FAQ, aber mit C ++ Export-Schlüsselwort.

Sadanand
quelle
5
includeEine cppDatei zu erstellen ist im Allgemeinen eine schreckliche Idee. Selbst wenn Sie einen gültigen Grund dafür haben, sollte die Datei - die eigentlich nur ein verherrlichter Header ist - eine hppoder eine andere Erweiterung erhalten (z. B. tpp), um sehr deutlich zu machen, was vor sich geht, Verwirrung über die makefileAusrichtung auf tatsächliche cpp Dateien zu beseitigen usw.
underscore_d
@underscore_d Können Sie erklären, warum das Einfügen einer .cppDatei eine schreckliche Idee ist?
Abbas
1
@Abbas, weil die Erweiterung cpp(oder oder ccoder coder was auch immer) angibt, dass die Datei ein Teil der Implementierung ist, dass die resultierende Übersetzungseinheit (Präprozessorausgabe) separat kompilierbar ist und dass der Inhalt der Datei nur einmal kompiliert wird. Es bedeutet nicht, dass die Datei ein wiederverwendbarer Teil der Schnittstelle ist, der willkürlich an einer beliebigen Stelle eingefügt werden soll. #includeWenn Sie eine tatsächliche cpp Datei erstellen, wird Ihr Bildschirm zu Recht schnell mit mehreren Definitionsfehlern gefüllt. in diesem Fall, wie es ist ein Grund, #includees cppwar nur die falsche Wahl der Verlängerung.
underscore_d
@underscore_d Grundsätzlich ist es also falsch, nur die .cppErweiterung für eine solche Verwendung zu verwenden. Aber ein anderes Wort zu verwenden .tppist völlig in Ordnung, was dem gleichen Zweck dienen würde, aber eine andere Erweiterung zum leichteren / schnelleren Verständnis verwenden würde?
Abbas
1
@Abbas Ja, cpp/ cc/ etc vermieden werden muss, aber es ist eine gute Idee, den Einsatz etwas anderes als hpp- zum Beispiel tpp, tccusw. - so dass Sie den Rest des Dateinamen wiederverwenden können und zeigen an, dass die tppDatei, obwohl es wirkt wie ein Header, hält die Out-of-Line-Implementierung der Template-Deklarationen im entsprechenden hpp. Dieser Beitrag beginnt also mit einer guten Prämisse - das Trennen von Deklarationen und Definitionen in zwei verschiedene Dateien, die einfacher zu erfassen / zu analysieren sind oder manchmal aufgrund zirkulärer Abhängigkeiten erforderlich sind. IME endet jedoch schlecht, indem vorgeschlagen wird, dass die zweite Datei eine falsche Erweiterung hat
underscore_d
6

Nein, es ist nicht möglich. Nicht ohne das exportSchlüsselwort, das in jeder Hinsicht nicht wirklich existiert.

Das Beste, was Sie tun können, ist, Ihre Funktionsimplementierungen in einer ".tcc" - oder ".tpp" -Datei abzulegen und die .tcc-Datei am Ende Ihrer .hpp-Datei einzuschließen. Dies ist jedoch nur kosmetisch; Es ist immer noch dasselbe wie alles in Header-Dateien zu implementieren. Dies ist einfach der Preis, den Sie für die Verwendung von Vorlagen zahlen.

Charles Salvia
quelle
3
Ihre Antwort ist nicht korrekt. Sie können Code aus einer Vorlagenklasse in einer CPP-Datei generieren, sofern Sie wissen, welche Vorlagenargumente verwendet werden sollen. Weitere Informationen finden Sie in meiner Antwort.
Benoît
2
Es stimmt, aber dies ist mit der schwerwiegenden Einschränkung verbunden, dass die CPP-Datei jedes Mal aktualisiert und neu kompiliert werden muss, wenn ein neuer Typ eingeführt wird, der die Vorlage verwendet. Dies ist wahrscheinlich nicht das, was das OP im Sinn hatte.
Charles Salvia
3

Ich glaube, es gibt zwei Hauptgründe für den Versuch, Vorlagencode in einen Header und einen CPP zu trennen:

Einer ist für bloße Eleganz. Wir alle schreiben gerne Code, der einfach zu lesen, zu verwalten und später wiederverwendbar ist.

Ein weiterer Grund ist die Verkürzung der Kompilierungszeiten.

Ich codiere derzeit (wie immer) Simulationssoftware in Verbindung mit OpenCL und wir behalten gerne Code bei, damit er je nach HW-Fähigkeit je nach Bedarf mit float (cl_float) oder double (cl_double) ausgeführt werden kann. Im Moment wird dies mit einem #define REAL am Anfang des Codes durchgeführt, aber dies ist nicht sehr elegant. Um die gewünschte Genauigkeit zu ändern, muss die Anwendung neu kompiliert werden. Da es keine echten Laufzeitarten gibt, müssen wir vorerst damit leben. Glücklicherweise sind OpenCL-Kernel zur Laufzeit kompiliert, und eine einfache Größe von (REAL) ermöglicht es uns, die Laufzeit des Kernel-Codes entsprechend zu ändern.

Das viel größere Problem besteht darin, dass, obwohl die Anwendung modular aufgebaut ist, bei der Entwicklung von Hilfsklassen (z. B. solchen, die Simulationskonstanten vorberechnen) ebenfalls Vorlagen erstellt werden müssen. Diese Klassen werden alle mindestens einmal oben im Klassenabhängigkeitsbaum angezeigt, da die endgültige Vorlagenklasse Simulation eine Instanz einer dieser Factory-Klassen enthält, was bedeutet, dass praktisch jedes Mal, wenn ich eine geringfügige Änderung an der Factory-Klasse vornehme, die gesamte Software muss neu erstellt werden. Das ist sehr ärgerlich, aber ich kann keine bessere Lösung finden.

Meteorhead
quelle
2

Nur wenn du #include "stack.cppam Ende bist stack.hpp. Ich würde diesen Ansatz nur empfehlen, wenn die Implementierung relativ groß ist und Sie die CPP-Datei in eine andere Erweiterung umbenennen, um sie vom normalen Code zu unterscheiden.

lyricat
quelle
4
In diesem Fall möchten Sie Ihrer Datei stack.cpp #ifndef STACK_CPP (und Freunde) hinzufügen.
Stephen Newell
Schlagen Sie mich zu diesem Vorschlag. Auch ich bevorzuge diesen Ansatz aus Stilgründen nicht.
Luke
2
Ja, in einem solchen Fall sollte die 2. Datei definitiv nicht die Erweiterung cpp( ccoder was auch immer) erhalten, da dies einen starken Kontrast zu ihrer tatsächlichen Rolle darstellt. Stattdessen sollte eine andere Erweiterung angegeben werden, die angibt, dass (A) ein Header und (B) ein Header am unteren Rand eines anderen Headers enthalten sein soll. Ich benutze tppdafür, die handlich auch stehen können tem pspät im plementation (out-of-line - Definitionen). Ich habe hier mehr darüber erfahren
underscore_d
2

Manchmal ist es möglich, den größten Teil der Implementierung in der CPP-Datei zu verbergen, wenn Sie allgemeine Funktionen für alle Vorlagenparameter in eine Nicht-Vorlagenklasse extrahieren können (möglicherweise typunsicher). Der Header enthält dann Umleitungsaufrufe an diese Klasse. Ein ähnlicher Ansatz wird verwendet, wenn mit dem Problem "Aufblähen von Vorlagen" gekämpft wird.

Konstantin Tenzin
quelle
+1 - obwohl es die meiste Zeit nicht sehr gut funktioniert (zumindest nicht so oft wie ich es wünsche)
Peterchen
2

Wenn Sie wissen, mit welchen Typen Ihr Stack verwendet wird, können Sie diese explizit in der CPP-Datei instanziieren und den gesamten relevanten Code dort aufbewahren.

Es ist auch möglich, diese über DLLs (!) Zu exportieren, aber es ist ziemlich schwierig, die richtige Syntax zu finden (MS-spezifische Kombinationen von __declspec (dllexport) und dem Schlüsselwort export).

Wir haben das in einer Mathe / Geom-Bibliothek verwendet, die Double / Float vorsah, aber ziemlich viel Code hatte. (Ich habe damals danach gegoogelt, habe diesen Code heute allerdings nicht.)

Macke
quelle
2

Das Problem ist, dass eine Vorlage keine tatsächliche Klasse generiert, sondern lediglich eine Vorlage , die dem Compiler sagt, wie eine Klasse generiert werden soll. Sie müssen eine konkrete Klasse generieren.

Der einfache und natürliche Weg besteht darin, die Methoden in die Header-Datei einzufügen. Aber es gibt noch einen anderen Weg.

Wenn Sie in Ihrer CPP-Datei einen Verweis auf jede erforderliche Vorlageninstanziierung und -methode haben, generiert der Compiler diese dort zur Verwendung in Ihrem gesamten Projekt.

neue 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
}
Mark Ransom
quelle
8
Sie benötigen die Dummey-Funktion nicht: Verwenden Sie 'Template Stack <int>;' Dies erzwingt eine Instanziierung der Vorlage in die aktuelle Kompilierungseinheit. Sehr nützlich, wenn Sie eine Vorlage definieren, aber nur einige bestimmte Implementierungen in einer gemeinsam genutzten Bibliothek wünschen.
Martin York
@ Martin: einschließlich aller Mitgliedsfunktionen? Das ist fantastisch. Sie sollten diesen Vorschlag zum Thread "Versteckte C ++ - Funktionen" hinzufügen.
Mark Ransom
@LokiAstari Ich habe einen Artikel dazu gefunden, falls jemand mehr erfahren möchte: cplusplus.com/forum/articles/14272
Andrew Larsson
1

Sie müssen alles in der HPP-Datei haben. Das Problem ist, dass die Klassen erst erstellt werden, wenn der Compiler feststellt, dass sie von einer ANDEREN CPP-Datei benötigt werden. Daher muss der gesamte Code verfügbar sein, um die Vorlagenklasse zu diesem Zeitpunkt zu kompilieren.

Eine Sache, die ich normalerweise mache, ist zu versuchen, meine Vorlagen in einen generischen Teil ohne Vorlage (der zwischen cpp / hpp aufgeteilt werden kann) und den typspezifischen Vorlagenteil, der die Klasse ohne Vorlage erbt, aufzuteilen.

Aaron
quelle
0

Da Vorlagen bei Bedarf kompiliert werden, erzwingt dies eine Einschränkung für Projekte mit mehreren Dateien: Die Implementierung (Definition) einer Vorlagenklasse oder -funktion muss sich in derselben Datei wie ihre Deklaration befinden. Das bedeutet, dass wir die Schnittstelle nicht in einer separaten Header-Datei trennen können und dass wir sowohl die Schnittstelle als auch die Implementierung in jede Datei aufnehmen müssen, die die Vorlagen verwendet.

ChadNC
quelle
0

Eine andere Möglichkeit ist, etwas zu tun wie:

#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

Ich mag diesen Vorschlag aus Stilgründen nicht, aber er passt vielleicht zu Ihnen.

Luke
quelle
1
Der verherrlichte zweite Header sollte mindestens eine andere Erweiterung haben cpp, um Verwechslungen mit den tatsächlichen Quelldateien zu vermeiden . Häufige Vorschläge sind tppund tcc.
underscore_d
0

Mit dem Schlüsselwort 'export' können Sie die Vorlagenimplementierung von der Vorlagendeklaration trennen. Dies wurde im C ++ - Standard ohne vorhandene Implementierung eingeführt. Zu gegebener Zeit haben es nur wenige Compiler tatsächlich implementiert. Ausführliche Informationen finden Sie unter Inform IT-Artikel zum Export

Shailesh Kumar
quelle
1
Dies ist fast eine reine Linkantwort, und dieser Link ist tot.
underscore_d
0

1) Denken Sie daran, dass der Hauptgrund für die Trennung von .h- und .cpp-Dateien darin besteht, die Klassenimplementierung als separat kompilierten Obj-Code auszublenden, der mit dem Code des Benutzers verknüpft werden kann, der ein .h der Klasse enthält.

2) Nicht-Vorlagenklassen haben alle Variablen konkret und spezifisch in .h- und .cpp-Dateien definiert. Der Compiler benötigt also vor dem Kompilieren / Übersetzen die erforderlichen Informationen zu allen in der Klasse verwendeten Datentypen.  Generieren des Objekt- / Maschinencodes Vorlagenklassen haben keine Informationen zum spezifischen Datentyp, bevor der Benutzer der Klasse ein Objekt instanziiert, das die erforderlichen Daten übergibt Art:

        TClass<int> myObj;

3) Erst nach dieser Instanziierung generiert der Complier die spezifische Version der Vorlagenklasse, die den übergebenen Datentypen entspricht.

4) Daher kann .cpp NICHT separat kompiliert werden, ohne den benutzerspezifischen Datentyp zu kennen. Es muss also als Quellcode innerhalb von ".h" bleiben, bis der Benutzer den erforderlichen Datentyp angibt. Anschließend kann es für einen bestimmten Datentyp generiert und dann kompiliert werden

Aaron01
quelle
-3

Ich arbeite mit Visual Studio 2010, wenn Sie Ihre Dateien in .h und .cpp aufteilen möchten, fügen Sie Ihren cpp-Header am Ende der .h-Datei hinzu

Ahmad
quelle