Templates in C ++ – classn in .hpp / .cpp-Dateien aufteilen – ist das möglich?

Ich .hpp Fehler beim Versuch, eine C ++ Template-class zu kompilieren, die zwischen einer .hpp und .cpp Datei aufgeteilt ist:

 $ 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::stack()' main.cpp:(.text+0x1c): undefined reference to 'stack::~stack()' collect2: ld returned 1 exit status make: *** [program] Error 1 

Hier ist mein Code:

stack.hpp :

 #ifndef _STACK_HPP #define _STACK_HPP template  class stack { public: stack(); ~stack(); }; #endif 

stack.cpp :

 #include  #include "stack.hpp" template  stack::stack() { std::cerr << "Hello, stack " << this << "!" << std::endl; } template  stack::~stack() { std::cerr << "Goodbye, stack " << this << "." << std::endl; } 

main.cpp :

 #include "stack.hpp" int main() { stack s; return 0; } 

ld ist natürlich korrekt: die Symbole sind nicht in stack.o .

Die Antwort auf diese Frage hilft nicht, da ich bereits so vorgehe, wie es heißt.
Dieser könnte helfen, aber ich möchte nicht jede einzelne Methode in die .hpp Datei verschieben – ich sollte es nicht tun müssen, oder?

Ist die einzige vernünftige Lösung, alles in der .cpp Datei in die .cpp Datei zu .hpp und einfach alles .hpp , anstatt als eigenständige Objektdatei einzubinden? Das scheint furchtbar hässlich! In diesem Fall könnte ich genauso gut zu meinem vorherigen Zustand zurückkehren und stack.cpp in stack.hpp umbenennen und damit fertig werden.

   

Es ist nicht möglich, die Implementierung einer Template-class in eine separate cpp-Datei zu schreiben und zu kompilieren. Alle Möglichkeiten, dies zu tun, sind Workarounds, um die Verwendung separater CPP-Dateien nachzuahmen, aber praktisch, wenn Sie eine Template-classnbibliothek schreiben und sie mit Header- und Lib-Dateien verteilen möchten, ist dies einfach nicht möglich .

Um zu wissen, warum, schauen wir uns den Kompilierungsprozess an. Die Header-Dateien werden niemals kompiliert. Sie sind nur vorverarbeitet. Der vorverarbeitete Code wird dann mit der cpp-Datei verknüpft, die tatsächlich kompiliert wird. Wenn der Compiler nun das passende Speicherlayout für das Objekt erzeugen muss, muss er den Datentyp der Template-class kennen.

Tatsächlich muss man verstehen, dass die Vorlagenklasse überhaupt keine class ist, sondern eine Vorlage für eine class, deren Deklaration und Definition vom Compiler zur Kompilierzeit generiert wird, nachdem die Informationen des Datentyps aus dem Argument abgerufen wurden. Solange das Speicherlayout nicht erstellt werden kann, können die statementen für die Methodendefinition nicht generiert werden. Denken Sie daran, dass das erste Argument der classnmethode der Operator ‘this’ ist. Alle classnmethoden werden in einzelne Methoden mit dem Namen Mangling und dem ersten Parameter als Objekt, auf dem sie operiert, umgewandelt. Das Argument ‘this’ gibt die Größe des Objekts an, die für den Compiler nicht verfügbar ist, es sei denn, der Benutzer instanziiert das Objekt mit einem gültigen Argument type. Wenn Sie in diesem Fall die Methodendefinitionen in eine separate cpp-Datei schreiben und versuchen, sie zu kompilieren, wird die Objektdatei selbst nicht mit den classninformationen generiert. Die Kompilierung wird nicht fehlschlagen, es würde die Objektdatei generieren, aber es wird kein Code für die Vorlagenklasse in der Objektdatei generiert. 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 hinter der Trennung der Schnittstelle von der Implementierung darin, Implementierungsdetails in binärer Form zu verbergen. Hier müssen Sie die Datenstrukturen und Algorithmen trennen. Ihre Vorlagenklassen müssen nur Datenstrukturen darstellen, nicht die Algorithmen. Auf diese Weise können Sie wertvollere Implementierungsdetails in separaten nicht-templateten classnbibliotheken ausblenden, deren classn in den Vorlagenklassen funktionieren, oder sie einfach zum Speichern von Daten verwenden. 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 werden.

Ich hoffe, diese Diskussion wäre hilfreich.

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

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

 template class stack; 

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

Sie können es auf diese Weise tun

 // xyz.h #ifndef _XYZ_ #define _XYZ_ template  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 FAQ, aber mit C ++ Export-Schlüsselwort.

Nein, es ist nicht möglich. Nicht ohne das export Keyword, das eigentlich gar nicht existiert.

Das Beste, was Sie tun können, ist, Ihre functionsimplementierungen in eine “.tcc” – oder “.tpp” -Datei zu stellen und # die .tcc-Datei am Ende Ihrer .hpp-Datei zu enthalten. Dies ist jedoch nur kosmetisch; Es ist immer noch das Gleiche wie alles in Header-Dateien zu implementieren. Dies ist einfach der Preis, den Sie für die Verwendung von Vorlagen zahlen.

Ich glaube, es gibt zwei Hauptgründe dafür, templated code in einen Header und ein cpp zu trennen:

Man ist für die bloße Eleganz. Wir alle schreiben gerne Code, der später gelesen, verwaltet und wiederverwendet werden kann.

Anderes ist die Reduzierung der Kompilierungszeiten.

Ich bin momentan (wie immer) in der Programmierung von Simulationssoftware in Verbindung mit OpenCL, und wir behalten gerne Code, so dass er abhängig von der HW-Fähigkeit je nach Bedarf mit float (cl_float) oder doppelten (cl_double) Typen ausgeführt werden kann. Im Moment wird dies mit einem #define REAL am Anfang des Codes gemacht, aber das ist nicht sehr elegant. Um die gewünschte Genauigkeit zu ändern, muss die Anwendung neu kompiliert werden. Da es keine echten Laufzeittypen gibt, müssen wir vorerst damit leben. Glücklicherweise sind OpenCL-coreel kompilierte Runtime, und eine einfache Größe (REAL) erlaubt uns, die Laufzeit des coreel-Codes entsprechend zu ändern.

Das viel größere Problem ist, dass, obwohl die Anwendung modular ist, beim Entwickeln von Hilfsklassen (wie diejenigen, die Simulationskonstanten vorberechnen) ebenfalls Vorlagen erstellt werden müssen. Diese classn erscheinen alle mindestens einmal oben in der classnabhängigkeitsstruktur, da die letzte Template-class Simulation eine Instanz einer dieser Factory-classn hat, was bedeutet, dass praktisch jedes Mal, wenn ich eine kleine Änderung an der Factory-class vornehme, die gesamte class Software muss neu erstellt werden. Das ist sehr ärgerlich, aber ich kann keine bessere Lösung finden.

Manchmal ist es möglich, den Großteil der Implementierung in der cpp-Datei zu verbergen, wenn Sie die allgemeine functionalität foo aller Template-Parameter in eine Nicht-Template-class extrahieren können (möglicherweise type-unsafe). Der Header enthält dann Umleitungsaufrufe für diese class. Ein ähnlicher Ansatz wird verwendet, wenn man mit dem “Template Bloat” -Problem kämpft.

Wenn Sie wissen, mit welchen Typen Ihr Stack verwendet wird, können Sie sie 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 Syntax richtig zu machen (MS-spezifische Kombinationen von __declspec (dllexport) und dem Schlüsselwort export).

Wir haben das in einer mathematischen / geom-Bibliothek verwendet, die Double / Float-Vorlagen verwendet, aber ziemlich viel Code hat. (Ich habe zu der Zeit dafür gegoogelt, aber habe diesen Code heute nicht.)

Das Problem besteht darin, dass eine Vorlage keine tatsächliche class generiert, sondern lediglich eine Vorlage , die dem Compiler mitteilt, wie eine class generiert wird. Sie müssen eine konkrete class generieren.

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

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

neu stack.cpp:

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

Sie müssen alles in der hpp-Datei haben. Das Problem besteht darin, dass die classn erst erstellt werden, wenn der Compiler erkennt, dass sie von einer anderen cpp-Datei benötigt werden. Daher muss der gesamte Code verfügbar sein, um die zu diesem Zeitpunkt erstellte class zu kompilieren.

Eine Sache, die ich tue, ist zu versuchen, meine Vorlagen in einen generischen nicht-templatierten Teil zu teilen (der zwischen cpp / hpp aufgeteilt werden kann) und den typspezifischen Templatenteil, der die nicht-templated class erbt.

Nur wenn Sie #include "stack.cpp am Ende von stack.hpp #include "stack.cpp 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 von normalem Code zu unterscheiden .

Das ist eine ziemlich alte Frage, aber ich finde es interessant, die Präsentation von Arthur O’Dwyer auf der cppcon 2016 zu sehen . Gute Erklärung, viel Thema abgedeckt, ein Muss.

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 sowohl die Schnittstelle als auch die Implementierung in jede Datei eingeschlossen sein müssen, die die Vorlagen verwendet.

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

 #ifndef _STACK_HPP #define _STACK_HPP template  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 nicht als eine Frage des Stils, aber es kann Ihnen passen.

Das Schlüsselwort ‘export’ ist die Möglichkeit, die Template-Implementierung von der Template-Deklaration zu trennen. Dies wurde im C ++ – Standard ohne eine vorhandene Implementierung eingeführt. Zu gegebener Zeit haben nur ein paar Compiler es tatsächlich implementiert. Lesen Sie die ausführlichen Informationen im Inform IT-Artikel zum Thema Export

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

2) Nicht-Template-classn haben alle Variablen konkret und spezifisch in .h und .cpp Dateien definiert. Daher benötigt der Compiler Informationen über alle Datentypen, die in der class vor dem Kompilieren / Übersetzen verwendet werden. Generieren des Objekt- / Maschinencodes Template-classn haben keine Informationen über den spezifischen Datentyp, bevor der Benutzer der class ein Objekt instanziiert, das die erforderlichen Daten übergibt Art:

  TClass myObj; 

3) Erst nach dieser Instanziierung generiert der Compiler die spezifische Version der Template-class, um sie dem / den übergebenen Datentyp (en) anzupassen.

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

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 ein