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


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

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

Warum ist das?

(Klarstellung: Header-Dateien sind nicht die nur tragbare Lösung. Aber sie sind die bequemste portable Lösung.)


1383
2018-01-30 10:06


Ursprung


Antworten:


Es ist nicht Um die Implementierung in die Header-Datei zu stellen, sehen Sie sich die alternative Lösung am Ende dieser Antwort an.

Der Grund dafür, dass Ihr Code fehlschlägt, ist, dass der Compiler beim Instanziieren einer Vorlage eine neue Klasse mit dem angegebenen Template-Argument erstellt. Beispielsweise:

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

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

Wenn Sie diese Zeile lesen, erstellt der Compiler eine neue Klasse (nennen wir sie) FooInt), das dem 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 zu instanziieren (in diesem Fall) int). Wenn diese Implementierungen nicht im Header vorhanden wären, wären sie nicht zugänglich und daher könnte der Compiler die Vorlage nicht instanziieren.

Eine übliche Lösung besteht darin, die Vorlagendeklaration in eine Headerdatei zu schreiben, dann die Klasse 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 ist die Implementierung weiterhin von der Deklaration getrennt, aber für den Compiler zugänglich.

Eine andere Lösung besteht darin, die Implementierung getrennt zu halten und alle erforderlichen Template-Instanzen 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.


1206
2018-01-30 10:26



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

Wenn Sie am Ende der cpp-Datei für die Implementierung eine explizite Instanziierung aller Typen vornehmen, mit denen die Vorlage verwendet wird, kann der Linker sie wie gewöhnlich finden.

Bearbeiten: Hinzufügen eines Beispiels für die explizite Vorlageninstanziierung. Wird verwendet, nachdem die Vorlage definiert wurde und alle Mitgliedsfunktionen definiert wurden.

template class vector<int>;

Dies wird die Klasse und alle ihre Mitgliedsfunktionen (nur) instanzieren (und somit dem Linker verfügbar machen). Eine ähnliche Syntax funktioniert für Template-Funktionen. Wenn Sie also Überladungen von Nicht-Member-Operatoren haben, müssen Sie dies möglicherweise auch tun.

Das obige Beispiel ist ziemlich nutzlos, da Vektor vollständig in Headern definiert ist, außer wenn eine allgemeine Include-Datei (vorkompilierter Header?) Verwendet wird extern template class vector<int> um es davon abzuhalten, es in allen zu instantiieren andere (1000?) Dateien, die Vektor verwenden.


200
2017-08-13 13:49



Dies liegt an der Notwendigkeit einer separaten Kompilierung und daran, dass Vorlagen Instanziierungs-Polymorphismen sind.

Lasst uns zur Erklärung etwas näher an Beton kommen. Sagen wir, ich habe folgende Dateien:

  • foo.h
    • erklärt die Schnittstelle von class MyClass<T>
  • foo.cpp
    • definiert die Implementierung von class MyClass<T>
  • bar.cpp
    • Verwendet MyClass<int>

Separate Kompilierung bedeutet, dass ich kompilieren sollte foo.cpp unabhängig von bar.cpp. Der Compiler erledigt die gesamte harte Arbeit der Analyse, Optimierung und Codegenerierung auf jeder Kompilierungseinheit vollständig unabhängig; Wir müssen keine Ganzprogrammanalyse durchführen. Es ist nur der Linker, der das gesamte Programm auf einmal verarbeiten muss, und die Arbeit des Linkers ist wesentlich einfacher.

bar.cpp muss nicht einmal existieren, wenn ich kompiliere foo.cpp, aber ich sollte immer noch in der Lage sein, die foo.o Ich hatte schon zusammen mit der bar.o Ich habe gerade erst produziert, ohne neu kompilieren zu müssen foo.cpp. foo.cpp könnte sogar in eine dynamische Bibliothek kompiliert werden, die woanders ohne verteilt wird foo.cppund mit dem Code verbunden, den sie Jahre nach dem Schreiben geschrieben haben foo.cpp.

"Instantiierungs-Stil Polymorphismus" bedeutet, dass die Vorlage MyClass<T> ist nicht wirklich eine generische Klasse, die zu Code kompiliert werden kann, der für jeden Wert von funktionieren kann T. Das würde Overhead wie Boxen hinzufügen, Funktionszeiger an Zuweisern und Konstruktoren usw. übergeben müssen. Die Absicht von C ++ - Vorlagen ist es, zu vermeiden, dass sie fast identisch schreiben müssen class MyClass_int, class MyClass_floatusw., aber immer noch in der Lage zu sein, mit kompiliertem Code zu enden, der größtenteils so ist, als ob wir hätten jede Version separat geschrieben. Also eine Vorlage ist buchstäblich eine Vorlage; eine Klassenvorlage ist nicht eine Klasse, es ist ein Rezept zum Erstellen einer neuen Klasse für jeden T wir begegnen. Eine Vorlage kann nicht in Code kompiliert werden, nur das Ergebnis der Instanziierung der Vorlage kann kompiliert werden.

Also wann foo.cpp ist kompiliert, der Compiler kann nicht sehen bar.cpp das zu wissen MyClass<int> wird gebraucht. Es kann die Vorlage sehen MyClass<T>, aber dafür kann kein Code ausgegeben werden (es ist eine Vorlage, keine Klasse). Und wann bar.cpp Ist kompiliert, kann der Compiler sehen, dass es ein erstellen muss MyClass<int>, aber es kann die Vorlage nicht sehen MyClass<T> (nur seine Schnittstelle in foo.h) so kann es es nicht schaffen.

Ob foo.cpp selbst verwendet MyClass<int>, dann Code für das wird während des Kompilierens generiert foo.cpp, also wann bar.o ist verbunden mit foo.o Sie können angeschlossen werden und funktionieren. Wir können diese Tatsache verwenden, um eine endliche Menge von Template-Instanziierungen in einer CPP-Datei durch Schreiben einer einzelnen Vorlage zu implementieren. Aber dafür gibt es keinen Weg bar.cpp die Vorlage verwenden als Vorlage und instanziieren es auf welchen Arten es mag; Es kann nur bereits vorhandene Versionen der Vorlagenklasse verwenden, die der Autor von foo.cpp gedacht, um zu liefern.

Sie könnten denken, dass der Compiler beim Kompilieren einer Vorlage "alle Versionen generieren" sollte, wobei diejenigen, die nie verwendet werden, während der Verknüpfung herausgefiltert werden. Abgesehen von dem enormen Overhead und den extremen Schwierigkeiten würde sich ein solcher Ansatz stellen, da "type modifier" -Features wie Zeiger und Arrays sogar die eingebauten Typen erlauben, eine unendliche Anzahl von Typen zu erzeugen, was passiert, wenn ich jetzt mein Programm erweitere beim Hinzufügen:

  • baz.cpp
    • erklärt und implementiert class BazPrivateund verwendet MyClass<BazPrivate>

Es gibt keinen möglichen Weg, dass dies funktionieren könnte, wenn wir es nicht auch tun

  1. Ich muss neu kompilieren foo.cpp jedes Mal wenn wir uns ändern jede andere Datei im Programm, falls es eine neue Instanziierung von MyClass<T>
  2. Erfordern das baz.cpp enthält (möglicherweise über Header includes) die vollständige Vorlage von MyClass<T>, damit der Compiler generieren kann MyClass<BazPrivate> während der Kompilierung von baz.cpp.

Niemand mag es (1), weil ganze Programmanalyse-Kompilierungssysteme greifen für immer zu kompilieren, und weil es unmöglich macht, kompilierte Bibliotheken ohne den Quellcode zu verteilen. Also haben wir stattdessen (2).


173
2018-05-11 03:54



Vorlagen müssen sein instanziiert durch den Compiler, bevor sie tatsächlich in Objektcode kompiliert werden. Diese Instanziierung kann nur erreicht werden, wenn die Template-Argumente bekannt sind. Stellen Sie sich nun ein Szenario vor, in dem eine Template-Funktion deklariert ist a.hdefiniert in a.cpp und verwendet in b.cpp. Wann a.cpp Ist kompiliert, ist nicht unbedingt bekannt, dass die kompilierung ansteht b.cpp erfordert eine Instanz der Vorlage, geschweige denn welche spezifische Instanz wäre das. Für weitere Header- und Quelldateien kann die Situation schnell komplizierter werden.

Man kann argumentieren, dass Compiler intelligenter gemacht werden können, um für alle Anwendungen des Templates vorauszusehen, aber ich bin mir sicher, dass es nicht schwierig wäre, rekursive oder anderweitig komplizierte Szenarien zu erstellen. AFAIK, Compiler tun solche Vorausschauen nicht. Wie Anton betonte, unterstützen einige Compiler explizite Exportdeklarationen von Template-Instanziierungen, aber nicht alle Compiler unterstützen sie (noch?).


64
2018-01-30 10:23



Tatsächlich haben Versionen des C ++ - Standards vor C ++ 11 das Schlüsselwort export definiert würde ermöglichen es, Vorlagen einfach in einer Header-Datei zu deklarieren und an anderer Stelle zu implementieren.

Leider hat keiner der populären Compiler dieses Schlüsselwort implementiert. Der einzige, den ich kenne, ist das Frontend der Edison Design Group, das vom Comeau C ++ Compiler verwendet wird. Alle anderen bestanden darauf, dass Sie Vorlagen in Header-Dateien schreiben, die die Definition des Codes für die korrekte Instanziierung benötigen (wie bereits andere darauf hingewiesen haben).

Als Folge entschied das ISO C ++ Standard Komitee, die export Feature von Vorlagen beginnend mit C ++ 11.


47
2018-01-30 13:38



Obwohl Standard-C ++ keine solche Anforderung hat, erfordern einige Compiler, dass alle Funktions- und Klassenvorlagen in jeder verwendeten Übersetzungseinheit verfügbar gemacht werden müssen. Für diese Compiler müssen die Körper der Vorlagenfunktionen in einer Header-Datei verfügbar gemacht werden. Um es zu wiederholen: Das heißt, diese Compiler erlauben nicht, dass sie in Nicht-Header-Dateien wie .cpp-Dateien definiert werden

Da ist ein Export Stichwort, das dieses Problem abschwächen soll, aber es ist nicht annähernd tragbar.


31
2018-01-30 10:15



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

Es gab eine Funktion mit der export Schlüsselwort, das für die separate Kompilierung verwendet werden sollte. Das export Feature ist veraltet in C++11 und, AFAIK, nur ein Compiler implementiert es. Sie sollten nicht Gebrauch machen export. Separate Kompilierung ist nicht möglich in C++ oder C++11 aber vielleicht in C++17Wenn Konzepte es schaffen, könnten wir eine Art der separaten Kompilierung haben.

Damit eine separate Kompilierung erreicht werden kann, muss eine separate Überprüfung des Vorlagenkörpers möglich sein. Es scheint, dass eine Lösung mit Konzepten möglich ist. Schau dir das an Papier- vor kurzem präsentiert auf der Standards commitee Sitzung. Ich denke, das ist nicht die einzige Voraussetzung, da Sie immer noch Code für den Vorlagencode im Benutzercode instanziieren müssen.

Das separate Kompilierungsproblem für Templates Ich denke, es ist auch ein Problem, das bei der Migration auf Module entsteht, die gerade bearbeitet wird.


26
2018-05-12 16:42



Dies bedeutet, dass die am besten portierbare Methode zum Definieren von Methodenimplementierungen von Vorlagenklassen darin besteht, sie innerhalb der Vorlagenklassendefinition zu definieren.

template < typename ... >
class MyClass
{

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

13
2018-01-30 10:53



Auch wenn es oben viele gute Erklärungen gibt, fehlt mir eine praktische Möglichkeit, Vorlagen in Header und Body zu trennen.
Mein Hauptanliegen ist es, die Neukompilierung aller Vorlagenbenutzer zu vermeiden, wenn ich ihre Definition ändere.
Alle Template-Instanziierungen im Template-Body sind für mich keine praktikable Lösung, da der Template-Autor möglicherweise nicht alles weiß, wenn seine Verwendung und der Template-Benutzer nicht berechtigt ist, ihn zu modifizieren.
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 Template-Verwendung gibt es einen Typedef in seiner eigenen Header-Datei (generiert aus dem UML-Modell). Sein Körper enthält die Instanziierung (die in einer Bibliothek endet, die am Ende verknüpft ist).
Jeder Benutzer der Vorlage enthält diese Header-Datei und verwendet den 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

MeineTemplate.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).


9
2018-05-12 14:02