web-dev-qa-db-de.com

Warum können Vorlagen nur in der Header-Datei implementiert werden?

Zitat aus Die C++ - Standardbibliothek: Tutorial und Handbuch :

Die einzige tragbare Möglichkeit, Vorlagen zu verwenden, besteht derzeit darin, sie mithilfe von Inline-Funktionen in Header-Dateien zu implementieren.

Warum ist das?

(Klarstellung: Header-Dateien sind nicht die einzige portable Lösung. Sie sind jedoch die bequemste portable Lösung.)

1618
MainID

Es ist nicht erforderlich , die Implementierung in die Header-Datei einzufügen. Die alternative Lösung finden Sie am Ende dieser Antwort.

Der Grund, warum Ihr Code fehlschlägt, ist, dass der Compiler beim Instanziieren einer Vorlage eine neue Klasse mit dem angegebenen Vorlagenargument erstellt. Zum Beispiel:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Beim Lesen dieser Zeile erstellt der Compiler eine neue Klasse (nennen wir sie FooInt), die der folgenden entspricht:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

Folglich muss der Compiler Zugriff auf die Implementierung der Methoden haben, um sie mit dem Template-Argument instanziieren zu können (in diesem Fall int). Wenn diese Implementierungen nicht im Header enthalten wären, wäre kein Zugriff auf sie möglich, und der Compiler könnte die Vorlage nicht instanziieren.

Eine gebräuchliche Lösung besteht darin, die Vorlagendeklaration in eine Headerdatei zu schreiben, die Klasse dann in eine Implementierungsdatei (z. B. .tpp) zu implementieren und diese Implementierungsdatei am Ende des Headers einzufügen.

// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

Auf diese Weise wird die Implementierung immer noch von der Deklaration getrennt, ist jedoch für den Compiler zugänglich.

Eine andere Lösung besteht darin, die Implementierung getrennt zu halten und alle benötigten Vorlageninstanzen explizit zu instanziieren:

// Foo.h

// no implementation
template <typename T> struct Foo { ... };

//----------------------------------------    
// Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Wenn meine Erklärung nicht klar genug ist, können Sie sich die C++ Super-FAQ zu diesem Thema ansehen.

1415
Luc Touraille

Viele richtige Antworten hier, aber ich wollte dies hinzufügen (der Vollständigkeit halber):

Wenn Sie am Ende der Implementierungs-CPP-Datei alle Typen, mit denen die Vorlage verwendet wird, explizit instanziieren, kann der Linker sie wie gewohnt finden.

Bearbeiten: Beispiel für explizite Vorlageninstanziierung hinzufügen. Wird verwendet, nachdem die Vorlage definiert und alle Mitgliedsfunktionen definiert wurden.

template class vector<int>;

Dadurch werden die Klasse und alle ihre Mitgliedsfunktionen (nur) instanziiert (und somit dem Linker zur Verfügung gestellt). Eine ähnliche Syntax funktioniert für Vorlagenfunktionen. Wenn Sie also Operatorüberladungen haben, die keine Mitglieder sind, müssen Sie dies möglicherweise auch für diese tun.

Das obige Beispiel ist ziemlich nutzlos, da der Vektor in Headern vollständig definiert ist, es sei denn, eine gemeinsame Include-Datei (vorkompilierter Header?) Verwendet extern template class vector<int>, um zu verhindern, dass sie in allen anderen instantiiert (1000?) Dateien, die einen Vektor verwenden.

230
MaHuJa

Dies liegt an der Notwendigkeit einer separaten Kompilierung und daran, dass Vorlagen einen instanziierungsähnlichen Polymorphismus aufweisen.

Kommen wir der Erklärung etwas näher. Angenommen, ich habe die folgenden Dateien:

  • foo.h
    • deklariert die Schnittstelle von class MyClass<T>
  • foo.cpp
    • definiert die Implementierung von class MyClass<T>
  • bar.cpp
    • benutzt MyClass<int>

Separate Kompilierung bedeutet, dass ich in der Lage sein sollte, foo.cpp unabhängig von bar.cpp zu kompilieren. Der Compiler erledigt die harte Arbeit der Analyse, Optimierung und Codegenerierung auf jeder Kompilierungseinheit völlig unabhängig. Wir müssen keine Analyse des gesamten Programms durchführen. Es ist nur der Linker, der das gesamte Programm auf einmal handhaben muss, und die Arbeit des Linkers ist wesentlich einfacher.

bar.cpp muss nicht einmal existieren, wenn ich foo.cpp kompiliere, aber ich sollte trotzdem in der Lage sein, foo.o zu verlinken Ich hatte bereits zusammen mit dem bar.o ich habe gerade erst produziert, ohne dass ich foo.cpp neu kompilieren musste. foo.cpp könnte sogar in eine dynamische Bibliothek kompiliert werden, ohne foo.cpp woanders verteilt und mit Code verknüpft werden, den sie Jahre nachdem ich foo geschrieben habe. cpp.

"Instanziierungs-artiger Polymorphismus" bedeutet, dass die Vorlage MyClass<T> keine generische Klasse ist, die zu Code kompiliert werden kann, der für jeden Wert von T funktioniert. Dies würde zusätzlichen Aufwand verursachen, z. B. das Boxen, das Übergeben von Funktionszeigern an Allokatoren und Konstruktoren usw. Mit C++ - Vorlagen soll vermieden werden, dass nahezu identische class MyClass_int, class MyClass_float usw. geschrieben werden müssen in der Lage sein, kompilierten Code zu erhalten, der meist so aussieht, als hätten wir jede Version separat geschrieben . Eine Vorlage ist also wörtlich eine Vorlage; Eine Klassenvorlage ist keine Klasse, sondern ein Rezept zum Erstellen einer neuen Klasse für jedes T, auf das wir stoßen. Eine Vorlage kann nicht in Code kompiliert werden, sondern nur das Ergebnis der Instanziierung der Vorlage.

Wenn also foo.cpp kompiliert wird, kann der Compiler bar.cpp nicht erkennen, dass MyClass<int> benötigt wird. Es kann die Vorlage MyClass<T> sehen, aber es kann keinen Code dafür ausgeben (es ist eine Vorlage, keine Klasse). Und wenn bar.cpp kompiliert wird, kann der Compiler sehen, dass er einen MyClass<int> erstellen muss, aber er kann die Vorlage MyClass<T> nicht sehen (nur die Schnittstelle in - foo.h) also kann es nicht erstellt werden.

Wenn foo.cpp selbst MyClass<int> verwendet, wird der Code dafür beim Kompilieren von foo.cpp generiert, also wenn bar.o = ist verknüpft mit foo.o sie können angeschlossen werden und funktionieren. Wir können diese Tatsache nutzen, um einen endlichen Satz von Vorlageninstanzen in einer CPP-Datei zu implementieren, indem wir eine einzelne Vorlage schreiben. Es gibt jedoch keine Möglichkeit für bar.cpp, die Vorlage als Vorlage zu verwenden und sie auf beliebigen Typen zu instanziieren. Es können nur bereits vorhandene Versionen der Klasse mit Vorlagen verwendet werden, die der Autor von foo.cpp zur Verfügung gestellt hat.

Sie könnten denken, dass der Compiler beim Kompilieren einer Vorlage "alle Versionen generieren" sollte, wobei diejenigen, die niemals verwendet werden, beim Verknüpfen herausgefiltert werden. Abgesehen von dem enormen Overhead und den extremen Schwierigkeiten, mit denen ein solcher Ansatz konfrontiert sein würde, weil "Typmodifikator" -Funktionen wie Zeiger und Arrays es selbst den eingebauten Typen ermöglichen, eine unendliche Anzahl von Typen zu erzeugen, was passiert, wenn ich mein Programm jetzt erweitere beim Hinzufügen:

  • baz.cpp
    • deklariert und implementiert class BazPrivate und verwendet MyClass<BazPrivate>

Es gibt keine Möglichkeit, dass dies funktioniert, wenn wir es nicht tun

  1. Müssen foo.cpp jedes Mal neu kompilieren, wenn wir eine andere Datei im Programm ändern , falls es eine neue neuartige Instanziierung hinzufügt von MyClass<T>
  2. Fordern Sie an, dass baz.cpp (möglicherweise über Header Includes) die vollständige Vorlage von MyClass<T> enthält, damit der Compiler MyClass<BazPrivate> beim Kompilieren von baz.cpp.

Niemand mag (1), weil das Kompilieren von Compilierungssystemen für die gesamte Programmanalyse ewig dauert und weil es unmöglich ist, kompilierte Bibliotheken ohne den Quellcode zu verteilen Code. Also haben wir stattdessen (2).

218
Ben

Vorlagen müssen vom Compiler instanziiert sein, bevor sie tatsächlich in Objektcode kompiliert werden. Diese Instanziierung kann nur erreicht werden, wenn die Vorlagenargumente bekannt sind. Stellen Sie sich nun ein Szenario vor, in dem eine Vorlagenfunktion in a.h deklariert, in a.cpp definiert und in b.cpp verwendet wird. Wenn a.cpp kompiliert wird, ist nicht unbedingt bekannt, dass für die bevorstehende Kompilierung b.cpp eine Instanz der Vorlage erforderlich ist, geschweige denn, welche bestimmte Instanz dies wäre. Bei mehr Header- und Quelldateien kann die Situation schnell komplizierter werden.

Man kann argumentieren, dass Compiler intelligenter gemacht werden können, um für alle Verwendungen der Vorlage nach vorne zu schauen, aber ich bin mir sicher, dass es nicht schwierig sein würde, rekursive oder auf andere Weise komplizierte Szenarien zu erstellen. AFAIK, Compiler machen solche Vorausschau nicht. Wie Anton betonte, unterstützen einige Compiler explizite Exportdeklarationen von Template-Instantiierungen, aber (noch?) Nicht alle Compiler.

74
David Hanak

Tatsächlich hat der Standard vor C++ 11 das Schlüsselwort export definiert, mit dem would Vorlagen in einer Header-Datei deklariert und an anderer Stelle implementiert werden können.

Keiner der populären Compiler hat dieses Schlüsselwort implementiert. Ich kenne nur das Frontend der Edison Design Group, das vom Comeau C++ - Compiler verwendet wird. In allen anderen Fällen mussten Sie Vorlagen in Header-Dateien schreiben, da der Compiler die Vorlagendefinition für die ordnungsgemäße Instanziierung benötigt (wie bereits erwähnt).

Aus diesem Grund hat das ISO C++ - Standardkomitee beschlossen, die Funktion export von Vorlagen mit C++ 11 zu entfernen.

58
DevSolar

Obwohl Standard-C++ keine solchen Anforderungen stellt, erfordern einige Compiler, dass alle Funktions- und Klassenvorlagen in jeder von ihnen verwendeten Übersetzungseinheit verfügbar gemacht werden müssen. Tatsächlich müssen für diese Compiler die Hauptteile der Vorlagenfunktionen in einer Header-Datei verfügbar gemacht werden. Wiederholen: Das bedeutet, dass diese Compiler nicht zulassen, dass sie in Nicht-Header-Dateien wie CPP-Dateien definiert werden

Es gibt ein export Schlüsselwort, das dieses Problem mindern soll, aber es ist bei weitem nicht portabel.

34
Anton Gogolev

Vorlagen müssen in Kopfzeilen verwendet werden, da der Compiler abhängig von den für Vorlagenparameter angegebenen/abgeleiteten Parametern verschiedene Versionen des Codes instanziieren muss. Denken Sie daran, dass eine Vorlage nicht direkt Code darstellt, sondern eine Vorlage für mehrere Versionen dieses Codes. Wenn Sie eine Nicht-Vorlagenfunktion in einer .cpp -Datei kompilieren, kompilieren Sie eine konkrete Funktion/Klasse. Dies ist nicht der Fall bei Vorlagen, die mit verschiedenen Typen instanziiert werden können, dh, beim Ersetzen von Vorlagenparametern durch konkrete Typen muss konkreter Code ausgegeben werden.

Es gab eine Funktion mit dem Schlüsselwort export, die für die separate Kompilierung verwendet werden sollte. Die Funktion export ist in C++11 veraltet, und, AFAIK, nur ein Compiler hat sie implementiert. Sie sollten export nicht verwenden. Eine separate Kompilierung ist in C++ oder C++11 nicht möglich, aber vielleicht in C++17, wenn Konzepte es schaffen, können wir eine Art separate Kompilierung haben.

Damit eine separate Kompilierung erreicht werden kann, muss eine separate Überprüfung des Schablonenkörpers möglich sein. Mit Konzepten scheint eine Lösung möglich zu sein. Schauen Sie sich dieses Papier an, das kürzlich auf der Sitzung des Normenausschusses vorgestellt wurde. Ich denke, dies ist nicht die einzige Voraussetzung, da Sie immer noch Code für den Vorlagencode im Benutzercode instanziieren müssen.

Das separate Kompilierungsproblem für Vorlagen ist vermutlich auch ein Problem, das bei der Migration auf Module auftritt, die derzeit bearbeitet wird.

27
Germán Diago

Dies bedeutet, dass die portabelste Methode zum Definieren von Methodenimplementierungen von Vorlagenklassen darin besteht, sie in der Definition der Vorlagenklasse zu definieren.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
15
Benoît

Obwohl es oben viele gute Erklärungen gibt, vermisse ich eine praktische Möglichkeit, Vorlagen in Header und Body zu trennen.
Mein Hauptanliegen ist es, die Neukompilierung aller Benutzer von Vorlagen zu vermeiden, wenn ich deren Definition ändere.
Es ist für mich keine praktikable Lösung, alle Vorlageninstanziierungen im Vorlagenkörper zu haben, da der Vorlagenautor möglicherweise nicht alle Informationen zu seiner Verwendung hat und der Vorlagenbenutzer möglicherweise nicht das Recht hat, diese zu ändern.
Ich habe den folgenden Ansatz gewählt, der auch für ältere Compiler funktioniert (gcc 4.3.4, aCC A.03.13).

Für jede Vorlagenverwendung gibt es ein typedef in einer eigenen Header-Datei (generiert aus dem UML-Modell). Sein Körper enthält die Instanziierung (die in einer Bibliothek endet, die am Ende eingebunden ist).
Jeder Benutzer der Vorlage schließt diese Header-Datei ein und verwendet die typedef.

Ein schematisches Beispiel:

MyTemplate.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

main.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

Auf diese Weise müssen nur die Vorlageninstanziierungen neu kompiliert werden, nicht alle Vorlagenbenutzer (und Abhängigkeiten).

12
lafrecciablu

Wenn das Problem die zusätzliche Kompilierungszeit und die Binärgröße ist, die beim Kompilieren der .h-Datei als Teil aller damit verwendeten .cpp-Module entstehen, können Sie in vielen Fällen dafür sorgen, dass die Template-Klasse von einer nicht vorlagenbasierten Basisklasse für abgeleitet wird Nicht typabhängige Teile der Schnittstelle und diese Basisklasse können in der CPP-Datei implementiert werden.

6
Eric Shaw

Das ist genau richtig, da der Compiler wissen muss, um welchen Typ es sich handelt. Vorlagenklassen, Funktionen, Aufzählungen usw. müssen daher auch in der Header-Datei implementiert werden, wenn sie öffentlich gemacht werden sollen oder Teil einer Bibliothek (statisch oder dynamisch) sein sollen, da Header-Dateien im Gegensatz zu den c/cpp-Dateien, die NICHT kompiliert werden sind. Wenn der Compiler den Typ nicht kennt, kann er ihn nicht kompilieren. In .Net ist dies möglich, da alle Objekte von der Object-Klasse abgeleitet sind. Das ist nicht .Net.

6
Robert

Nur um hier etwas Bemerkenswertes hinzuzufügen. Man kann Methoden einer Klasse mit Vorlagen in der Implementierungsdatei definieren, wenn es sich nicht um Funktionsvorlagen handelt.


myQueue.hpp:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}
3
Nikos

Eine Möglichkeit für eine separate Implementierung ist wie folgt.

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

inner_foo hat die Forward-Deklarationen. foo.tpp hat die Implementierung und beinhaltet inner_foo.h; und foo.h wird nur eine Zeile haben, um foo.tpp einzuschließen.

Bei der Kompilierung wird der Inhalt von foo.h nach foo.tpp kopiert, und anschließend wird die gesamte Datei nach foo.h kopiert und anschließend kompiliert. Auf diese Weise gibt es keine Einschränkungen und die Benennung ist im Austausch für eine zusätzliche Datei konsistent.

Ich mache das, weil statische Analysatoren für den Code brechen, wenn es die Forward-Deklarationen der Klasse in * .tpp nicht sieht. Dies ist ärgerlich, wenn Sie Code in IDE schreiben oder YouCompleteMe oder andere verwenden.

2
Pranay

Der Compiler generiert Code für jede Vorlageninstanziierung, wenn Sie während des Kompilierungsschritts eine Vorlage verwenden. Während des Kompilierungs- und Verknüpfungsprozesses werden CPP-Dateien in reinen Objekt- oder Maschinencode konvertiert, der Verweise oder undefinierte Symbole enthält, da die in der Datei main.cpp enthaltenen H-Dateien noch keine Implementierung haben. Diese können mit einer anderen Objektdatei verknüpft werden, die eine Implementierung für Ihre Vorlage definiert, sodass Sie eine vollständige ausführbare Datei a.out haben.

Da jedoch Vorlagen im Kompilierungsschritt verarbeitet werden müssen, um Code für jede von Ihnen definierte Vorlageninstanziierung zu generieren, funktioniert das einfache Kompilieren einer Vorlage unabhängig von der Headerdatei aus diesem Grund nicht, da sie immer Hand in Hand gehen dass jede Template-Instanziierung im wahrsten Sinne des Wortes eine ganz neue Klasse ist. In einer regulären Klasse können Sie .h und .cpp trennen, da .h eine Blaupause dieser Klasse ist und die .cpp die unformatierte Implementierung ist, sodass alle Implementierungsdateien regelmäßig kompiliert und verknüpft werden können Die Klasse sollte nicht so aussehen, wie das Objekt aussehen sollte. Dies bedeutet, dass eine CPP-Vorlagendatei keine normale Implementierung einer Klasse ist, sondern lediglich eine Blaupause für eine Klasse. Eine Implementierung einer H-Vorlagendatei kann daher nicht kompiliert werden Sie brauchen etwas Konkretes zum Kompilieren, Vorlagen sind in diesem Sinne abstrakt.

Aus diesem Grund werden Vorlagen niemals separat kompiliert und nur dann kompiliert, wenn Sie eine konkrete Instanz in einer anderen Quelldatei haben. Die konkrete Instanziierung muss jedoch die Implementierung der Vorlagendatei kennen, da das einfache Ändern von typename T mithilfe eines konkreten Typs in der .h-Datei den Job nicht erledigt, da die zu verlinkende .cpp vorhanden ist Ich kann es später nicht finden, da die Vorlagen abstrakt sind und nicht kompiliert werden können. Daher bin ich gezwungen, die Implementierung jetzt anzugeben, damit ich weiß, was zu kompilieren und zu verknüpfen ist, und jetzt habe ich die Implementierung, mit der sie verknüpft wird die beiliegende Quelldatei. In dem Moment, in dem ich eine Vorlage instanziiere, muss ich eine ganz neue Klasse erstellen, und das kann ich nicht, wenn ich nicht weiß, wie diese Klasse aussehen soll, wenn ich den von mir angegebenen Typ verwende, es sei denn, ich mache dem Compiler von Bescheid Der Compiler kann nun T durch meinen Typ ersetzen und eine konkrete Klasse erstellen, die kompiliert und verknüpft werden kann.

Zusammenfassend sind Vorlagen Blaupausen dafür, wie Klassen aussehen sollen, Klassen Blaupausen dafür, wie ein Objekt aussehen soll. Ich kann Vorlagen nicht getrennt von ihrer konkreten Instanziierung kompilieren, da der Compiler nur konkrete Typen kompiliert, dh Vorlagen, zumindest in C++, sind reine Sprachabstraktionen. Wir müssen Vorlagen sozusagen de-abstrahieren, und wir geben ihnen einen konkreten Typ, mit dem wir uns befassen müssen, damit unsere Vorlagenabstraktion in eine reguläre Klassendatei umgewandelt werden kann und diese wiederum normal kompiliert werden kann. Das Trennen der H-Vorlagendatei und der CPP-Vorlagendatei ist bedeutungslos. Es ist unsinnig, weil die Trennung von .cpp und .h nur dann möglich ist, wenn die .cpp einzeln kompiliert und mit Vorlagen verknüpft werden kann, da wir sie nicht separat kompilieren können, da Vorlagen eine Abstraktion sind und wir daher immer dazu gezwungen sind Setze die Abstraktion immer zusammen mit der konkreten Instanziierung, über die die konkrete Instanziierung immer Bescheid wissen muss

Das bedeutet, dass typename T während des Kompilierungsschritts ersetzt wird und nicht der Verknüpfungsschritt. Wenn ich also versuche, eine Vorlage zu kompilieren, ohne dass T als konkreter Werttyp ersetzt wird, funktioniert dies nicht, da dies die Definition der Vorlagen ist Ein Prozess zur Kompilierungszeit, und bei der Metaprogrammierung dreht sich alles um die Verwendung dieser Definition.

2
Moshe Rabaev

Ein weiterer Grund, warum es sinnvoll ist, sowohl Deklarationen als auch Definitionen in Header-Dateien zu schreiben, ist die Lesbarkeit. Angenommen, es gibt eine solche Vorlagenfunktion in Utility.h:

template <class T>
T min(T const& one, T const& theOther);

Und in der Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

Dies erfordert, dass jede T-Klasse hier den Kleiner-als-Operator (<) implementiert. Es wird ein Compilerfehler ausgegeben, wenn Sie zwei Klasseninstanzen vergleichen, die das "<" nicht implementiert haben.

Wenn Sie also die Vorlagendeklaration und -definition trennen, können Sie nicht nur die Header-Datei lesen, um die Vor- und Nachteile dieser Vorlage zu sehen, um diese API für Ihre eigenen Klassen zu verwenden. Der Compiler teilt dies Ihnen jedoch mit Fall darüber, welcher Operator überschrieben werden muss.

0
ClarHandsome