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

1778

Zitat aus der C ++ - Standardbibliothek: ein Tutorial und ein 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 tragbare Lösung. Sie sind jedoch die bequemste tragbare Lösung.)

MainID
quelle
13
Zwar ist das Einfügen aller Vorlagenfunktionsdefinitionen in die Header-Datei wahrscheinlich die bequemste Methode, um sie zu verwenden, es ist jedoch immer noch nicht klar, was "Inline" in diesem Zitat bewirkt. Dafür müssen keine Inline-Funktionen verwendet werden. "Inline" hat damit absolut nichts zu tun.
Am
7
Buch ist veraltet.
Gerardw
1
Eine Vorlage ist nicht wie eine Funktion, die in Bytecode kompiliert werden kann. Es ist nur ein Muster, um eine solche Funktion zu erzeugen. Wenn Sie eine Vorlage alleine in eine * .cpp-Datei einfügen, müssen Sie nichts kompilieren. Darüber hinaus ist die explizite Instanziierung eigentlich keine Vorlage, sondern der Ausgangspunkt, um aus der Vorlage eine Funktion zu machen, die in der * .obj-Datei endet.
Grat
5
Bin ich der einzige, der das Gefühl hat, dass das Vorlagenkonzept in C ++ dadurch verkrüppelt ist? ...
DragonGamer

Antworten:

1558

Vorsichtsmaßnahme: Es ist nicht erforderlich, die Implementierung in die Header-Datei aufzunehmen. 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 (in diesem Fall int) zu instanziieren . Wenn diese Implementierungen nicht im Header enthalten wären, wären sie nicht zugänglich, und daher könnte der Compiler die Vorlage nicht instanziieren.

Eine übliche Lösung hierfür besteht darin, die Vorlagendeklaration in eine Header-Datei 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 weiterhin von der Deklaration getrennt, ist jedoch für den Compiler zugänglich.

Alternative Lösung

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 .

Luc Touraille
quelle
96
Tatsächlich muss sich die explizite Instanziierung in einer CPP-Datei befinden, die Zugriff auf die Definitionen für alle Mitgliedsfunktionen von Foo hat, und nicht im Header.
Mankarse
11
"Der Compiler muss Zugriff auf die Implementierung der Methoden haben, um sie mit dem Template-Argument (in diesem Fall int) zu instanziieren. Wenn diese Implementierungen nicht im Header enthalten wären, wären sie nicht zugänglich." Aber warum ist eine Implementierung in Die CPP-Datei, auf die der Compiler nicht zugreifen kann? Ein Compiler kann auch auf CPP-Informationen zugreifen. Wie würde er sie sonst in OBJ-Dateien umwandeln? BEARBEITEN: Antwort auf diese Frage ist in dem Link in dieser Antwort bereitgestellt ...
xcrypt
31
Ich denke nicht, dass dies die Frage erklärt, dass der Schlüssel offensichtlich mit der Zusammenstellung UNIT zusammenhängt, die in diesem Beitrag nicht erwähnt wird
Zinking
6
@Gabson: Strukturen und Klassen sind äquivalent mit der Ausnahme, dass der Standardzugriffsmodifikator für Klassen "privat" ist, während er für Strukturen öffentlich ist. Es gibt einige andere kleine Unterschiede, die Sie anhand dieser Frage lernen können .
Luc Touraille
3
Ich habe ganz am Anfang dieser Antwort einen Satz hinzugefügt, um zu verdeutlichen, dass die Frage auf einer falschen Prämisse beruht. Wenn jemand fragt "Warum ist X wahr?" Wenn X tatsächlich nicht wahr ist, sollten wir diese Annahme schnell ablehnen.
Aaron McDaid
250

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 eine explizite Vorlageninstanziierung hinzufügen. Wird verwendet, nachdem die Vorlage definiert und alle Elementfunktionen 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 Überladungen von Nicht-Mitglied-Operatoren haben, müssen Sie möglicherweise dasselbe für diese tun.

Das obige Beispiel ist ziemlich nutzlos, da der Vektor in Headern vollständig definiert ist, außer wenn eine gemeinsame Include-Datei (vorkompilierter Header?) Verwendet wird extern template class vector<int>, um zu verhindern, dass er in allen anderen (1000?) Dateien, die Vektor verwenden , instanziiert wird.

MaHuJa
quelle
51
Pfui. Gute Antwort, aber keine wirklich saubere Lösung. Das Auflisten aller möglichen Typen für eine Vorlage scheint nicht mit dem übereinzustimmen, was eine Vorlage sein soll.
Jiminion
6
Dies kann in vielen Fällen gut sein, verstößt jedoch im Allgemeinen gegen den Zweck der Vorlage, mit der Sie die Klasse mit jeder verwenden können, typeohne sie manuell aufzulisten.
Tomáš Zato - Wiedereinsetzung Monica
7
vectorist kein gutes Beispiel, da ein Container von Natur aus auf "alle" Typen abzielt. Es kommt jedoch sehr häufig vor, dass Sie Vorlagen erstellen, die nur für einen bestimmten Satz von Typen bestimmt sind, z. B. numerische Typen: int8_t, int16_t, int32_t, uint8_t, uint16_t usw. In diesem Fall ist es immer noch sinnvoll, eine Vorlage zu verwenden Es ist jedoch auch möglich und wird meiner Meinung nach empfohlen, sie explizit für die gesamte Gruppe von Typen zu instanziieren.
Onkel Zeiv
Wird verwendet, nachdem die Vorlage definiert wurde und "und alle Elementfunktionen definiert wurden". Vielen Dank !
Vitt Volt
1
Ich habe das Gefühl, dass mir etwas fehlt… Ich habe die explizite Instanziierung für zwei Typen in die .cppDatei der Klasse eingefügt und die beiden Instanziierungen werden aus anderen .cppDateien referenziert, und ich erhalte immer noch den Verknüpfungsfehler, dass die Mitglieder nicht gefunden werden.
Oarfish
250

Dies liegt an der Notwendigkeit einer separaten Kompilierung und daran, dass Vorlagen ein Polymorphismus im Instanziierungsstil sind.

Gehen wir zur Erklärung etwas näher an den Beton heran. 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
    • Verwendet MyClass<int>

Separate Kompilierung bedeutet, dass ich foo.cpp unabhängig von bar.cpp kompilieren kann . Der Compiler erledigt die harte Arbeit der Analyse, Optimierung und Codegenerierung auf jeder Kompilierungseinheit völlig unabhängig. Wir müssen keine Ganzprogrammanalyse durchführen. Es ist nur der Linker, der das gesamte Programm auf einmal bearbeiten 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, das foo.o zu verknüpfen, das ich bereits zusammen mit dem bar.o hatte. Ich habe gerade erst produziert, ohne foo neu kompilieren zu müssen .cpp . foo.cpp könnte sogar in eine dynamische Bibliothek kompiliert, ohne foo.cpp an einem anderen Ort verteilt und mit Code verknüpft werden, den sie Jahre nach dem Schreiben von foo.cpp schreiben .

"Polymorphismus im Instanziierungsstil" bedeutet, dass die Vorlage MyClass<T>keine generische Klasse ist, die zu Code kompiliert werden kann, der für jeden Wert von funktioniert T. Das würde hinzufügen Overhead wie Boxen, um Funktionszeiger auf Verteilern und Konstrukteure weitergeben usw. Die Absicht von C ++ Vorlagen ist zu vermeiden , fast identisch zu schreiben class MyClass_int, class MyClass_floatetc, aber noch in der Lage sein , mit kompilierten Code , um am Ende das ist meist als ob wir hatten jede Version separat geschrieben. Eine Vorlage ist also buchstäblich eine Vorlage. Eine Klassenvorlage ist keine Klasse, sondern ein Rezept zum Erstellen einer neuen Klasse für jede Klasse, auf die Twir stoßen. Eine Vorlage kann nicht in Code kompiliert werden, sondern nur das Ergebnis der Instanziierung der Vorlage kann kompiliert werden.

Wenn also foo.cpp kompiliert wird, kann der Compiler bar.cpp nicht sehen, um zu wissen, dass dies MyClass<int>erforderlich ist. Es kann die Vorlage sehen MyClass<T>, 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 eine erstellen muss MyClass<int>, aber er kann die Vorlage nicht sehen MyClass<T>(nur die Schnittstelle in foo.h ), sodass er sie nicht erstellen kann.

Wenn foo.cpp selbst verwendet MyClass<int>, wird beim Kompilieren von foo.cpp Code dafür generiert. Wenn also bar.o mit foo.o verknüpft ist, können sie angeschlossen werden und funktionieren. Wir können diese Tatsache nutzen, um zu ermöglichen, dass ein endlicher Satz von Vorlageninstanziierungen in einer CPP-Datei implementiert wird, indem eine einzelne Vorlage geschrieben wird. 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 Vorlagenklasse verwendet werden, die der Autor von foo.cpp bereitgestellt hat .

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, mit denen ein solcher Ansatz konfrontiert wäre, weil "Typmodifikator" -Funktionen wie Zeiger und Arrays es sogar nur den eingebauten Typen ermöglichen, eine unendliche Anzahl von Typen hervorzurufen, was passiert, wenn ich jetzt mein Programm erweitere beim Hinzufügen:

  • baz.cpp
    • deklariert und implementiert class BazPrivateund verwendetMyClass<BazPrivate>

Es gibt keine Möglichkeit, dass dies funktionieren könnte, wenn wir es auch nicht tun

  1. Sie müssen foo.cpp jedes Mal neu kompilieren, wenn wir eine andere Datei im Programm ändern , falls eine neue neuartige Instanziierung von hinzugefügt wirdMyClass<T>
  2. Erfordern, dass baz.cpp die vollständige Vorlage von enthält (möglicherweise über Header enthält) MyClass<T>, damit der Compiler MyClass<BazPrivate>während der Kompilierung von baz.cpp generieren kann .

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

Ben
quelle
50
Hervorgehobenes Zitat Eine Vorlage ist buchstäblich eine Vorlage. Eine Klassenvorlage ist keine Klasse, sondern ein Rezept zum Erstellen einer neuen Klasse für jedes T, dem wir begegnen
v.oddou
Ich würde gerne wissen, ob es möglich ist, die expliziten Instanziierungen von einem anderen Ort als dem Header oder der Quelldatei der Klasse aus durchzuführen. Zum Beispiel in main.cpp?
Gromit190
1
@Birger Sie sollten dies von jeder Datei aus tun können, die Zugriff auf die vollständige Vorlagenimplementierung hat (entweder weil sie sich in derselben Datei befindet oder über Header-Includes).
Ben
11
@ajeh Es ist keine Rhetorik. Die Frage lautet: "Warum müssen Sie Vorlagen in einem Header implementieren?". Daher habe ich die technischen Entscheidungen erläutert, die die C ++ - Sprache trifft und die zu dieser Anforderung führen. Bevor ich meine Antwort schrieb, haben andere bereits Problemumgehungen bereitgestellt, die keine vollständigen Lösungen sind, da es keine vollständige Lösung geben kann . Ich war der Meinung, dass diese Antworten durch eine ausführlichere Diskussion des "Warum" -Winkels der Frage ergänzt würden.
Ben
1
Stellen Sie es sich so vor, Leute ... Wenn Sie keine Vorlagen verwenden würden (um effizient zu codieren, was Sie brauchen), würden Sie sowieso nur einige Versionen dieser Klasse anbieten. Sie haben also 3 Möglichkeiten. 1). Verwenden Sie keine Vorlagen. (Wie bei allen anderen Klassen / Funktionen kümmert es niemanden, dass andere die Typen nicht ändern können) 2). Verwenden Sie Vorlagen und dokumentieren Sie, welche Typen sie verwenden können. 3). Geben Sie ihnen den gesamten Implementierungsbonus (Quellbonus 4). Geben Sie ihnen die gesamte Quelle für den Fall, dass sie eine Vorlage aus einer anderen Ihrer Klassen
Puddle
81

Vorlagen müssen vom Compiler instanziiert werden, 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 deklariert a.h, definiert a.cppund verwendet wird b.cpp. Wenn a.cppkompiliert wird, ist nicht unbedingt bekannt, dass für die bevorstehende Kompilierung b.cppeine Instanz der Vorlage erforderlich ist, geschweige denn, welche spezifische 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 sicher, dass es nicht schwierig sein würde, rekursive oder auf andere Weise komplizierte Szenarien zu erstellen. AFAIK, Compiler machen keine solchen Vorausschau. Wie Anton betonte, unterstützen einige Compiler explizite Exportdeklarationen von Vorlageninstanziierungen, aber (noch?) Nicht alle Compiler unterstützen dies.

David Hanak
quelle
1
"Export" ist Standard, aber es ist nur schwer zu implementieren, so dass die meisten Compilerteams dies noch nicht getan haben.
Vava
5
Der Export beseitigt weder die Notwendigkeit der Offenlegung von Quellen noch verringert er die Kompilierungsabhängigkeiten, während Compiler-Builder einen massiven Aufwand erfordern. Deshalb hat Herb Sutter selbst die Compiler-Entwickler gebeten, den Export zu vergessen. Da die benötigte Zeitinvestition besser woanders verbringen würde ...
Pieter
2
Ich glaube also nicht, dass der Export "noch" nicht implementiert ist. Es wird wahrscheinlich nie von jemand anderem als EDG erledigt werden, nachdem die anderen gesehen haben, wie lange es gedauert hat und wie wenig gewonnen wurde
Pieter
3
Wenn Sie das interessiert, heißt das Papier "Warum wir uns keinen Export leisten können". Es ist in seinem Blog ( gotw.ca/publications ) aufgeführt, aber dort gibt es kein PDF (ein kurzer Blick auf Google sollte es jedoch aufdecken )
Pieter
1
Ok, danke für gutes Beispiel und Erklärung. Hier ist jedoch meine Frage: Warum kann der Compiler nicht herausfinden, wo die Vorlage aufgerufen wird, und diese Dateien zuerst kompilieren, bevor die Definitionsdatei kompiliert wird? Ich kann mir vorstellen, dass dies in einem einfachen Fall möglich ist ... Ist die Antwort, dass Interdependenzen die Reihenfolge ziemlich schnell durcheinander bringen?
Vlad
62

Eigentlich vor der C ++ 11 der Standard definierte das exportSchlüsselwort , das wäre es möglich , in einer Header - Datei zu deklarieren Vorlagen machen und sie an anderer Stelle umzusetzen.

Keiner der gängigen Compiler hat dieses Schlüsselwort implementiert. Das einzige, das ich kenne, ist das Frontend der Edison Design Group, das vom Comeau C ++ - Compiler verwendet wird. Bei allen anderen mussten Sie Vorlagen in Header-Dateien schreiben, da der Compiler die Vorlagendefinition für eine ordnungsgemäße Instanziierung benötigt (wie andere bereits betont haben).

Infolgedessen hat das ISO C ++ - Standardkomitee beschlossen, die exportFunktion von Vorlagen mit C ++ 11 zu entfernen .

DevSolar
quelle
6
... und ein paar Jahre später verstand ich endlich , was uns exporttatsächlich gegeben hätte und was nicht ... und jetzt stimme ich den EDG-Leuten voll und ganz zu: Es hätte uns nicht gebracht, was die meisten Leute (ich selbst in '11) enthalten) denke, es würde, und der C ++ - Standard ist ohne besser dran.
DevSolar
4
@ DevSolar: Dieses Papier ist politisch, sich wiederholend und schlecht geschrieben. das ist dort keine übliche Standard-Prosa. Unbeabsichtigt lang und langweilig, auf zehn Seiten im Grunde dreimal so viel zu sagen. Aber ich bin jetzt informiert, dass Export kein Export ist. Das ist eine gute Information!
v.oddou
1
@ v.oddou: Guter Entwickler und guter technischer Redakteur sind zwei getrennte Fähigkeiten. Einige können beides, viele nicht. ;-)
DevSolar
@ v.oddou Das Papier ist nicht nur schlecht geschrieben, es ist Desinformation. Es ist auch eine Wendung in der Realität: Was eigentlich extrem starke Argumente für Exporte sind, wird so gemischt, dass es so klingt, als wären sie gegen den Export: „Bei Vorhandensein des Exports zahlreiche ODR-bezogene Löcher im Standard entdecken. Vor dem Export mussten ODR-Verstöße vom Compiler nicht diagnostiziert werden. Jetzt ist es notwendig, weil Sie interne Datenstrukturen aus verschiedenen Übersetzungseinheiten kombinieren müssen und Sie sie nicht kombinieren können, wenn sie tatsächlich unterschiedliche Dinge darstellen. Sie müssen also die Überprüfung durchführen. “
Neugieriger
" muss jetzt hinzufügen, in welcher Übersetzungseinheit es war, als es passierte " Duh. Wenn Sie gezwungen sind, lahme Argumente zu verwenden, haben Sie kein Argument. Natürlich werden Sie in Ihren Fehlern Dateinamen erwähnen. Was ist los? Dass jemand auf diese BS hereinfällt, ist umwerfend. " Selbst Experten wie James Kanze fällt es schwer zu akzeptieren, dass Export wirklich so ist. " WAS? !!!!
Neugieriger
34

Obwohl Standard-C ++ keine solchen Anforderungen stellt, müssen bei einigen Compilern alle Funktions- und Klassenvorlagen in jeder verwendeten Übersetzungseinheit verfügbar sein. Tatsächlich müssen für diese Compiler die Hauptteile der Vorlagenfunktionen in einer Header-Datei verfügbar gemacht werden. Um es zu 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, mit dem dieses Problem behoben werden soll, das jedoch bei weitem nicht portabel ist.

Anton Gogolev
quelle
Warum kann ich sie nicht in eine CPP-Datei mit dem Schlüsselwort "inline" implementieren?
MainID
2
Sie können, und Sie müssen nicht einmal "inline" setzen. Aber Sie könnten sie nur in dieser CPP-Datei und nirgendwo anders verwenden.
Vava
10
Dies ist fast die genaueste Antwort, außer "das bedeutet, dass diese Compiler nicht zulassen, dass sie in Nicht-Header-Dateien wie CPP-Dateien definiert werden", ist offensichtlich falsch.
Leichtigkeitsrennen im Orbit
28

Vorlagen müssen in Headern verwendet werden, da der Compiler abhängig von den für Vorlagenparameter angegebenen / abgeleiteten Parametern unterschiedliche Versionen des Codes instanziieren muss. Denken Sie daran, dass eine Vorlage keinen Code direkt darstellt, sondern eine Vorlage für mehrere Versionen dieses Codes. Wenn Sie eine Nicht-Vorlagenfunktion in einer .cppDatei kompilieren, kompilieren Sie eine konkrete Funktion / Klasse. Dies ist nicht der Fall für 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 exportSchlüsselwort, die für die separate Kompilierung verwendet werden sollte. Die exportFunktion ist in C++11AFAIK veraltet und wurde nur von einem Compiler implementiert. Sie sollten nicht nutzen export. Eine getrennte Kompilierung ist in C++oder C++11aber möglicherweise nicht möglich C++17, wenn Konzepte es schaffen, könnten wir eine Möglichkeit der getrennten 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. Schauen Sie sich dieses Papier an, das kürzlich auf der Sitzung des Normungsausschusses vorgestellt wurde. Ich denke, dies ist nicht die einzige Anforderung, da Sie 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 zu Modulen auftritt, an der derzeit gearbeitet wird.

Germán Diago
quelle
15

Dies bedeutet, dass die portabelste 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
    }
};
Benoît
quelle
15

Obwohl es oben viele gute Erklärungen gibt, fehlt mir eine praktische Möglichkeit, Vorlagen in Header und Body zu unterteilen.
Mein Hauptanliegen ist es, eine Neukompilierung aller Vorlagenbenutzer zu vermeiden, wenn ich deren Definition ändere.
Alle Vorlageninstanziierungen im Vorlagenkörper zu haben, ist für mich keine praktikable Lösung, da der Vorlagenautor möglicherweise nicht alle weiß, ob seine Verwendung und der Vorlagenbenutzer möglicherweise nicht das Recht haben, sie 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 verlinkt ist).
Jeder Benutzer der Vorlage enthält diese Header-Datei und verwendet das 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).

lafrecciablu
quelle
1
Ich mag diesen Ansatz mit Ausnahme der MyInstantiatedTemplate.hDatei und des hinzugefügten MyInstantiatedTemplateTyps. Es ist ein bisschen sauberer, wenn du das nicht benutzt, imho. Überprüfen Sie meine Antwort auf eine andere Frage, die dies zeigt: stackoverflow.com/a/41292751/4612476
Cameron Tacklind
Dies ist das Beste aus zwei Welten. Ich wünschte, diese Antwort wäre höher bewertet worden! Siehe auch den Link oben für eine etwas sauberere Implementierung derselben Idee.
Wormer
8

Nur um hier etwas Bemerkenswertes hinzuzufügen. Man kann Methoden einer Vorlagenklasse in der Implementierungsdatei gut definieren, wenn sie keine Funktionsvorlagen sind.


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;

    ...
}
Nikos
quelle
2
Für echte Menschen ??? Wenn das stimmt, sollte Ihre Antwort als richtig überprüft werden. Warum braucht jemand all diese hackigen Voodo-Sachen, wenn Sie nur Methoden für Nicht-Template-Mitglieder in .cpp definieren können?
Michael IV
Nun, das funktioniert nicht. Zumindest in MSVC 2019 wird das ungelöste externe Symbol für eine Mitgliedsfunktion der Vorlagenklasse abgerufen.
Michael IV
Ich habe MSVC 2019 nicht zum Testen. Dies ist nach dem C ++ - Standard zulässig. MSVC ist bekannt dafür, dass es sich nicht immer an die Regeln hält. Wenn Sie dies noch nicht getan haben, versuchen Sie es mit Projekteinstellungen -> C / C ++ -> Sprache -> Konformitätsmodus -> Ja (zulässig-).
Nikos
1
Dieses genaue Beispiel funktioniert, aber dann können Sie isEmptyvon keiner anderen Übersetzungseinheit außer myQueue.cpp...
MM
7

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

Eric Shaw
quelle
2
Diese Antwort sollte viel mehr modifiziert werden. Ich habe " unabhängig " denselben Ansatz entdeckt und speziell nach jemandem gesucht, der ihn bereits verwendet hat, da ich gespannt bin, ob es sich um ein offizielles Muster handelt und ob es einen Namen hat. Mein Ansatz ist es, ein zu implementieren, class XBasewo immer ich ein implementieren muss template class X, indem ich die typabhängigen Teile Xund den Rest einfüge XBase.
Fabio A.
6

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

Robert
quelle
5
"Header-Dateien werden NICHT kompiliert" - das ist eine wirklich seltsame Art, es zu beschreiben. Header-Dateien können Teil einer Übersetzungseinheit sein, genau wie eine "c / cpp" -Datei.
Flexo
2
Tatsächlich ist es fast das Gegenteil der Wahrheit, dass Header-Dateien sehr häufig viele Male kompiliert werden, während eine Quelldatei normalerweise einmal kompiliert wird.
Xaxxon
6

Der Compiler generiert Code für jede Vorlageninstanziierung, wenn Sie während des Kompilierungsschritts eine Vorlage verwenden. Beim Kompilieren und Verknüpfen werden CPP-Dateien in reinen Objekt- oder Maschinencode konvertiert, der Verweise oder undefinierte Symbole enthält, da die in Ihrer 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 über eine vollständige ausführbare Datei verfügen.

Da Vorlagen jedoch im Kompilierungsschritt verarbeitet werden müssen, um Code für jede von Ihnen definierte Vorlageninstanziierung zu generieren, funktioniert das einfache Kompilieren einer Vorlage, die von der Header-Datei getrennt ist, nicht, da sie aus genau diesem Grund immer Hand in Hand gehen dass jede Vorlageninstanziierung buchstäblich 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 .cpp die Rohimplementierung ist, sodass alle Implementierungsdateien regelmäßig kompiliert und verknüpft werden können. Die Verwendung von Vorlagen .h ist jedoch eine Blaupause dafür, wie Die Klasse sollte nicht so aussehen, wie das Objekt aussehen soll, was bedeutet, dass eine .cpp-Vorlagendatei keine reguläre Rohimplementierung einer Klasse ist, sondern lediglich eine Blaupause für eine Klasse. Daher kann jede Implementierung einer .h-Vorlagendatei '

Daher werden Vorlagen niemals separat kompiliert und nur dort kompiliert, wo Sie eine konkrete Instanziierung in einer anderen Quelldatei haben. Die konkrete Instanziierung muss jedoch die Implementierung der Vorlagendatei kennen, da einfach die geändert werden musstypename TDie Verwendung eines konkreten Typs in der .h-Datei wird den Job nicht erledigen, da die zu verknüpfende .cpp-Datei später nicht mehr gefunden werden kann, da die Erinnerungsvorlagen abstrakt sind und nicht kompiliert werden können. Daher bin ich gezwungen Um die Implementierung jetzt zu geben, damit ich weiß, was zu kompilieren und zu verknüpfen ist, und jetzt, wo ich die Implementierung habe, wird sie in die beiliegende Quelldatei verknüpft. Grundsätzlich muss ich in dem Moment, in dem ich eine Vorlage instanziiere, eine ganz neue Klasse erstellen, und das kann ich nicht, wenn ich nicht weiß, wie diese Klasse aussehen soll, wenn ich den von mir bereitgestellten Typ verwende, es sei denn, ich benachrichtige den Compiler von Die Vorlagenimplementierung, sodass der Compiler jetzt durch Tmeinen Typ ersetzen und eine konkrete Klasse erstellen kann, die zum Kompilieren und Verknüpfen bereit ist.

Zusammenfassend sind Vorlagen Blaupausen für das Aussehen von Klassen, Klassen Blaupausen für das Aussehen eines Objekts. Ich kann Vorlagen nicht getrennt von ihrer konkreten Instanziierung kompilieren, da der Compiler nur konkrete Typen kompiliert, dh Vorlagen, zumindest in C ++, sind reine Sprachabstraktion. Wir müssen Vorlagen sozusagen de-abstrahieren, und wir geben ihnen einen konkreten Typ, damit unsere Vorlagenabstraktion in eine reguläre Klassendatei umgewandelt und normal kompiliert werden kann. Das Trennen der .h-Datei der Vorlage und der .cpp-Datei der Vorlage ist bedeutungslos. Es ist unsinnig, weil die Trennung von .cpp und .h nur dort erfolgt, wo die .cpp einzeln kompiliert und einzeln mit Vorlagen verknüpft werden kann, da wir sie nicht separat kompilieren können, da Vorlagen eine Abstraktion sind.

Das heißt, typename Tget wird während des Kompilierungsschritts ersetzt, nicht der Verknüpfungsschritt. Wenn ich also versuche, eine Vorlage zu kompilieren, ohne Tals konkreter Werttyp ersetzt zu werden, der für den Compiler völlig bedeutungslos ist und daher kein Objektcode erstellt werden kann, weil dies nicht der Fall ist weiß was Tist.

Es ist technisch möglich, eine Art von Funktionalität zu erstellen, mit der die Datei template.cpp gespeichert und die Typen ausgetauscht werden, wenn sie in anderen Quellen gefunden werden. Ich denke, dass der Standard ein Schlüsselwort enthält export, mit dem Sie Vorlagen in eine separate Datei einfügen können cpp-Datei, aber nicht so viele Compiler implementieren dies tatsächlich.

Nur eine Randnotiz: Wenn Sie Spezialisierungen für eine Vorlagenklasse vornehmen, können Sie den Header von der Implementierung trennen, da eine Spezialisierung per Definition bedeutet, dass ich mich auf einen konkreten Typ spezialisiere, der einzeln kompiliert und verknüpft werden kann.

Moshe Rabaev
quelle
4

Eine Möglichkeit zur getrennten 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 Vorwärtsdeklarationen. foo.tpp hat die Implementierung und enthält inner_foo.h; und foo.h hat nur eine Zeile, um foo.tpp einzuschließen.

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

Ich mache das, weil statische Analysatoren für den Code brechen, wenn die Vorwärtsdeklarationen der Klasse in * .tpp nicht angezeigt werden. Dies ist ärgerlich, wenn Sie Code in eine IDE schreiben oder YouCompleteMe oder andere verwenden.

Pranay
quelle
2
s / inner_foo / foo / g und füge foo.tpp am Ende von foo.h ein. Eine Datei weniger.
1

Ich schlage vor, auf diese gcc-Seite zu schauen, auf der die Kompromisse zwischen dem "cfront" - und dem "borland" -Modell für Vorlageninstanziierungen erläutert werden.

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html

Das "Borland" -Modell entspricht dem, was der Autor vorschlägt, liefert die vollständige Vorlagendefinition und lässt die Dinge mehrmals kompilieren.

Es enthält explizite Empfehlungen zur Verwendung der manuellen und automatischen Vorlageninstanziierung. Mit der Option "-repo" können beispielsweise Vorlagen erfasst werden, die instanziiert werden müssen. Oder Sie deaktivieren die automatische Vorlageninstanziierung mithilfe von "-fno-implicit-templates", um die manuelle Vorlageninstanziierung zu erzwingen.

Nach meiner Erfahrung verlasse ich mich darauf, dass die Vorlagen C ++ Standard Library und Boost für jede Kompilierungseinheit instanziiert werden (unter Verwendung einer Vorlagenbibliothek). Für meine großen Vorlagenklassen führe ich einmal eine manuelle Vorlageninstanziierung für die von mir benötigten Typen durch.

Dies ist mein Ansatz, da ich ein Arbeitsprogramm und keine Vorlagenbibliothek zur Verwendung in anderen Programmen bereitstelle. Der Autor des Buches, Josuttis, arbeitet viel an Vorlagenbibliotheken.

Wenn ich mir wirklich Sorgen um die Geschwindigkeit machen würde, würde ich wahrscheinlich die Verwendung vorkompilierter Header https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html untersuchen

Das gewinnt in vielen Compilern an Unterstützung. Ich denke jedoch, dass vorkompilierte Header mit Template-Header-Dateien schwierig wären.

Juan
quelle
-2

Ein weiterer Grund, warum es eine gute Idee 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 Operator kleiner als (<) implementiert. Es wird ein Compilerfehler ausgegeben, wenn Sie zwei Klasseninstanzen vergleichen, die das "<" nicht implementiert haben.

Wenn Sie die Vorlagendeklaration und -definition trennen, können Sie daher nicht nur die Header-Datei lesen, um die Vor- und Nachteile dieser Vorlage anzuzeigen, um diese API für Ihre eigenen Klassen zu verwenden, obwohl der Compiler Ihnen dies mitteilt Fall, welcher Operator überschrieben werden muss.

ClarHandsome
quelle
-7

Sie können Ihre Vorlagenklasse tatsächlich in einer .template-Datei anstatt in einer .cpp-Datei definieren. Wer sagt, dass man es nur in einer Header-Datei definieren kann, ist falsch. Dies funktioniert bis zu c ++ 98.

Vergessen Sie nicht, dass Ihr Compiler Ihre .template-Datei als C ++ - Datei behandelt, um den Intelli-Sinn zu bewahren.

Hier ist ein Beispiel dafür für eine dynamische Array-Klasse.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Jetzt definieren Sie in Ihrer .template-Datei Ihre Funktionen so, wie Sie es normalerweise tun würden.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }
Ni Nisan Nijackle
quelle
2
Die meisten Leute würden eine Header-Datei als alles definieren, was Definitionen an Quelldateien weitergibt. Sie haben sich möglicherweise für die Dateierweiterung ".template" entschieden, aber eine Header-Datei geschrieben.
Tommy