Beim Kompilieren einer C ++ - Vorlagenklasse, die zwischen a .hpp
und .cpp
file 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;
}
ld
ist 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 .hpp
Datei verschieben - ich sollte es nicht müssen, oder?
Ist die einzig vernünftige Lösung, alles in der .cpp
Datei in die .hpp
Datei 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.cpp
zu stack.hpp
und mit ihr geschehen.
Antworten:
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.
quelle
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:
Alle Nicht-Template-Methoden des Stapels werden instanziiert, und der Verknüpfungsschritt funktioniert einwandfrei.
quelle
template class stack<int>;
.Sie können es auf diese Weise tun
Dies wurde in Daniweb diskutiert
Auch in den FAQ, aber mit C ++ Export-Schlüsselwort.
quelle
include
Einecpp
Datei 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 - einehpp
oder eine andere Erweiterung erhalten (z. B.tpp
), um sehr deutlich zu machen, was vor sich geht, Verwirrung über diemakefile
Ausrichtung auf tatsächlichecpp
Dateien zu beseitigen usw..cpp
Datei eine schreckliche Idee ist?cpp
(oder odercc
oderc
oder 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.#include
Wenn Sie eine tatsächlichecpp
Datei erstellen, wird Ihr Bildschirm zu Recht schnell mit mehreren Definitionsfehlern gefüllt. in diesem Fall, wie es ist ein Grund,#include
escpp
war nur die falsche Wahl der Verlängerung..cpp
Erweiterung für eine solche Verwendung zu verwenden. Aber ein anderes Wort zu verwenden.tpp
ist völlig in Ordnung, was dem gleichen Zweck dienen würde, aber eine andere Erweiterung zum leichteren / schnelleren Verständnis verwenden würde?cpp
/cc
/ etc vermieden werden muss, aber es ist eine gute Idee, den Einsatz etwas anderes alshpp
- zum Beispieltpp
,tcc
usw. - so dass Sie den Rest des Dateinamen wiederverwenden können und zeigen an, dass dietpp
Datei, obwohl es wirkt wie ein Header, hält die Out-of-Line-Implementierung der Template-Deklarationen im entsprechendenhpp
. 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 hatNein, es ist nicht möglich. Nicht ohne das
export
Schlü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.
quelle
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.
quelle
Nur wenn du
#include "stack.cpp
am Ende biststack.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.quelle
cpp
(cc
oder 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 benutzetpp
dafür, die handlich auch stehen könnent
emp
spät imp
lementation (out-of-line - Definitionen). Ich habe hier mehr darüber erfahrenManchmal 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.
quelle
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.)
quelle
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:
quelle
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.
quelle
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.
quelle
Eine andere Möglichkeit ist, etwas zu tun wie:
Ich mag diesen Vorschlag aus Stilgründen nicht, aber er passt vielleicht zu Ihnen.
quelle
cpp
, um Verwechslungen mit den tatsächlichen Quelldateien zu vermeiden . Häufige Vorschläge sindtpp
undtcc
.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
quelle
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:
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
quelle
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
quelle