it-swarm.com.de

Aufteilen von Templates mit C++ in .hpp/.cpp-Dateien?

Ich bekomme Fehler beim Kompilieren einer C++ - Vorlagenklasse, die zwischen einer .hpp- und einer .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<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;
}

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

Die Antwort auf diese Frage hilft nicht, da ich schon mache, wie es heißt.
Dieses könnte helfen, aber ich möchte nicht jede einzelne Methode in die .hpp-Datei verschieben - ich sollte es nicht müssen, oder?

Ist die einzige vernünftige Lösung, um alles in der .cpp-Datei in die .hpp-Datei zu verschieben und einfach alles einzuschließen, anstatt als eigenständige Objektdatei zu verknüpfen? 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 sein.

83
exscape

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, wenn jemand behauptet, Problemumgehungen, um die Verwendung separater cpp-Dateien nachzuahmen, aber praktisch, wenn Sie eine Vorlagenklassenbibliothek schreiben und diese mit Header- und lib-Dateien zum Ausblenden der Implementierung verteilen möchten, ist dies einfach nicht möglich . 

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

Tatsächlich muss verstanden werden, dass die Template-Klasse überhaupt keine Klasse ist, sondern eine Vorlage für eine Klasse, deren Deklaration und Definition vom Compiler zur Kompilierzeit generiert werden, nachdem die Informationen des Datentyps aus dem Argument abgerufen wurden. Solange das Speicherlayout nicht erstellt werden kann, können keine Anweisungen für die Methodendefinition generiert werden. Denken Sie daran, dass das erste Argument der Klassenmethode der Operator 'this' ist. Alle Klassenmethoden werden in einzelne Methoden mit dem Namensverlust und dem ersten Parameter als Objekt konvertiert, mit dem sie bearbeitet werden. Das Argument 'this' gibt an, wie groß die Größe des Objekts ist, dessen Vorlageklasse 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 einer separaten cpp-Datei ablegen 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. Aus diesem Grund kann der Linker die Symbole in den Objektdateien nicht finden und der Build schlägt fehl. 

Was ist nun die Alternative, um wichtige Implementierungsdetails auszublenden? Wie wir alle wissen, besteht das Hauptziel beim Trennen der Schnittstelle von der Implementierung darin, Implementierungsdetails in binärer Form zu verbergen. Hier müssen Sie die Datenstrukturen und Algorithmen voneinander trennen. Ihre Vorlagenklassen müssen nur Datenstrukturen darstellen, nicht die Algorithmen. Auf diese Weise können Sie wertvollere Implementierungsdetails in separaten Nicht-Templatized-Klassenbibliotheken ausblenden. Die Klassen, in denen die Vorlagenklassen arbeiten, können für die Vorlagenklassen verwendet werden oder sie werden nur zum Speichern von Daten verwendet. 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.

136
Sharjith N.

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

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.

82
Benoît

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 FAQ aber mit C++ - Exportschlüsselwort.

8
Sadanand

Nein, es ist nicht möglich. Nicht ohne das export-Schlüsselwort, das eigentlich nicht existiert. 

Sie können die Funktionsimplementierungen am besten in eine ".tcc" - oder ".tpp" -Datei einfügen und die .tcc-Datei am Ende Ihrer .hpp-Datei einfügen. Dies ist jedoch nur kosmetisch; Es ist immer noch dasselbe, als würde man alles in Header-Dateien implementieren. Dies ist einfach der Preis, den Sie für die Verwendung von Vorlagen bezahlen.

6
Charles Salvia

Ich glaube, dass es zwei Hauptgründe gibt, um zu versuchen, Templated Code in einen Header und einen CPP zu trennen:

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

Andere ist die Reduzierung der Kompilierungszeiten.

Ich programmiere (wie immer) Simulationssoftware in Verbindung mit OpenCL und wir behalten den Code so, dass er je nach HW-Fähigkeit mit Float-Typen (cl_float) oder Double (cl_double) 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 werden OpenCL-Kernel zur Laufzeit kompiliert, und mit einer einfachen Größe (REAL) können wir die Kernel-Code-Laufzeit entsprechend ändern.

Das viel größere Problem ist, dass, obwohl die Anwendung modular ist, bei der Entwicklung von Hilfsklassen (z. B. solche, die Simulationskonstanten vorberechnen), auch Templates erstellt werden müssen. Diese Klassen werden alle mindestens einmal im oberen Bereich des Klassenabhängigkeitsbaums angezeigt, da die endgültige Vorlagenklasse Simulation eine Instanz dieser Factory-Klassen besitzt, was bedeutet, dass praktisch jedes Mal, wenn ich eine kleine Änderung an der Factory-Klasse vornehme, die gesamte Klasse Software muss neu erstellt werden. Das ist sehr ärgerlich, aber ich finde keine bessere Lösung.

3
Meteorhead

Manchmal ist es möglich, den Großteil der Implementierung in der cpp-Datei zu verbergen, wenn Sie die allgemeine Funktionalität für alle Template-Parameter in eine Nicht-Template-Klasse (möglicherweise typunsicher) extrahieren. Der Header enthält dann Umleitungsaufrufe an diese Klasse. Ein ähnlicher Ansatz wird verwendet, wenn mit dem "Schablonenaufblähungsproblem" gekämpft wird.

2

Das Problem ist, dass eine Vorlage keine tatsächliche Klasse generiert, es ist nur eine template, die dem Compiler sagt, wie eine Klasse erzeugt wird. Sie müssen eine konkrete Klasse generieren.

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

Wenn Sie in Ihrer .cpp-Datei einen Verweis auf jede benötigte Vorlageninstanziierung und -methode haben, generiert der Compiler diese dort zur Verwendung in Ihrem 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
}
2
Mark Ransom

Wenn Sie wissen, mit welchen Typen Ihr Stack verwendet wird, können Sie sie 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 Exportschlüsselwort).

Wir haben das in einer math/geom lib verwendet, die Double/Float-Templates enthielt, aber ziemlich viel Code hatte. (Ich habe damals gegoogelt, aber ich habe diesen Code heute nicht.)

2
Macke

Dies ist eine ziemlich alte Frage, aber ich denke, es ist interessant, die Präsentation von Arthur O'Dwyer auf der cppcon 2016 ..__ zu sehen. Gute Erklärung, viele Themen und ein Muss.

1
FreeYourSoul

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 Klasse mit Vorlagen zu kompilieren.

Ich neige dazu, zu versuchen, meine Vorlagen in einen generischen, nicht mit Vorlagen versehenen Teil (der in cpp/hpp aufgeteilt werden kann) und den typspezifischen Vorlagenabschnitt aufzuteilen, der die nicht mit Vorlagen versehene Klasse erbt.

1
Aaron

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

1
lyricat

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 befinden wie ihre Deklaration. Das bedeutet, dass wir die Schnittstelle nicht in einer separaten Header-Datei trennen können und dass wir Schnittstelle und Implementierung in jede Datei einfügen müssen, die die Vorlagen verwendet.

0
ChadNC

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 eine .h-Klasse enthält.

2) Bei Nicht-Vorlagen-Klassen sind alle Variablen in .h- und .cpp-Dateien konkret und spezifisch definiert. Der Compiler hat also vor dem Kompilieren/Übersetzen die erforderlichen Informationen zu allen in der Klasse verwendeten Datentypen.  Generieren des Objekt-/Maschinencodes Vorlagen-Klassen haben keine Informationen zu dem jeweiligen Datentyp, bevor der Benutzer der Klasse ein Objekt übergeben kann, das ein Objekt passiert der benötigte Datentyp: 

        TClass<int> myObj;

3) Erst nach dieser Instantiierung generiert der Complier die spezifische Version der Schablonenklasse, um mit den übergebenen Datentypen übereinzustimmen.

4) Daher kann .cpp NICHT separat kompiliert werden, ohne den spezifischen Datentyp des Benutzers zu kennen. Es muss also als Quellcode innerhalb von „.h“ verbleiben, bis der Benutzer den erforderlichen Datentyp dann angibt. Er kann zu einem bestimmten Datentyp generiert und dann kompiliert werden

0
Aaron01

Eine andere Möglichkeit ist, etwas zu tun:

#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 Gründen des Stils nicht, aber er kann Ihnen gefallen.

0
luke

Mit dem Schlüsselwort 'export' können Sie die Implementierung der Vorlage von der Deklaration der Vorlage trennen. Dies wurde im C++ - Standard ohne eine vorhandene Implementierung eingeführt. Zu gegebener Zeit haben nur wenige Compiler es tatsächlich implementiert. Lesen Sie ausführliche Informationen unter Informieren Sie IT-Artikel über den Export

0
Shailesh Kumar