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





cpp function (12)


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 einer .cpp Datei kompilieren, 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 ein Feature mit dem Schlüsselwort export , das für die separate Kompilierung verwendet werden sollte. Die export ist in C++11 veraltet, und in AFAIK wurde nur ein Compiler implementiert. Sie sollten den export nicht nutzen. Separate Kompilierung ist nicht möglich in C++ oder C++11 aber vielleicht in C++17 , wenn 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. Werfen Sie einen Blick auf dieses paper kurzem auf der Normenkonferenz vorgestellt wurde. 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.

Zitat aus der C ++ Standard-Bibliothek: ein Tutorial und 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 einzige portable Lösung. Aber sie sind die bequemste portable Lösung.)




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 in ihnen Referenzen oder nicht definierte Symbole enthält, da die .h-Dateien, die in Ihrer main.cpp enthalten sind, keine Implementierung YET haben. Diese sind bereit, mit einer anderen Objektdatei verknüpft zu werden, die eine Implementierung für Ihre Vorlage definiert und somit eine vollständige a.out-Programmdatei enthält. Da Templates jedoch im Kompilierungsschritt verarbeitet werden müssen, um Code für jede Template-Instanziierung zu generieren, die Sie in Ihrem Hauptprogramm ausführen, hilft die Verknüpfung nicht, da Sie die Datei main.cpp in main.o kompilieren und dann Ihre Vorlage .cpp kompilieren in template.o und dann wird das Verknüpfen nicht den Vorlagenzweck erreichen, da ich verschiedene Template-Instanziierung mit der gleichen Template-Implementierung verknüpfe! Und Templates sollen das Gegenteil tun, dh EINE Implementierung haben, aber viele verfügbare Instanzen über die Verwendung einer Klasse erlauben.

Bedeutung typename T get wird während des Kompilierungsschrittes ersetzt, nicht der Verknüpfungsschritt. Wenn ich also versuche, eine Vorlage zu kompilieren, ohne T als konkreten typename T zu ersetzen, wird es nicht funktionieren, da dies die Definition von Templates ist Bei der Meta-Programmierung geht es darum, diese Definition zu verwenden.




Das ist genau richtig, weil der Compiler wissen muss, um welchen Typ es sich handelt. Also Template-Klassen, Funktionen, Enums, etc .. muss auch in der Header-Datei implementiert werden, wenn es veröffentlicht werden soll oder Teil einer Bibliothek (statisch oder dynamisch), da Header-Dateien NICHT kompiliert werden im Gegensatz zu den c / cpp-Dateien sind. Wenn der Compiler den Typ nicht kennt, kann er nicht kompiliert werden. In .Net kann es, weil alle Objekte von der Object-Klasse abgeleitet sind. Dies ist nicht .Net.




Eine Möglichkeit, eine separate Implementierung zu haben, 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 enthält inner_foo.h; und foo.h wird nur eine Zeile haben, um foo.tpp einzubinden.

Zur Kompilierzeit wird der Inhalt von foo.h nach foo.tpp kopiert und dann wird die gesamte Datei nach foo.h kopiert, wonach sie kompiliert wird. Auf diese Weise gibt es keine Einschränkungen und die Benennung ist konsistent, im Austausch für eine zusätzliche Datei.

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




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 dies 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, das dieses Problem abschwächen soll, aber es ist noch lange nicht tragbar.




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
    • deklariert die Schnittstelle der class MyClass<T>
  • foo.cpp
    • definiert die Implementierung der class MyClass<T>
  • bar.cpp
    • verwendet MyClass<int>

Separate Kompilierung bedeutet, dass ich foo.cpp unabhängig von bar.cpp kompilieren sollte . 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 vorhanden sein, wenn ich foo.cpp kompiliere, aber ich sollte immer noch in der Lage sein, die foo.o, die ich bereits hatte, mit der bar.o zu verknüpfen, die ich gerade produziert habe, ohne foo neu kompilieren zu müssen .cpp . foo.cpp könnte sogar in eine dynamische Bibliothek kompiliert werden, irgendwo anders verteilt werden ohne foo.cpp , und mit Code verbunden werden, den sie schreiben, Jahre nachdem ich foo.cpp geschrieben habe .

"Instantiierungs-Stil Polymorphismus" bedeutet, dass die Vorlage MyClass<T> nicht wirklich eine generische Klasse ist, die zu Code kompiliert werden kann, der für jeden Wert von T . Das würde Overhead wie Boxen hinzufügen, Funktionszeiger an Allokatoren und Konstruktoren übergeben usw. Die Absicht von C ++ - Templates ist es, zu vermeiden, dass fast identische class MyClass_int , class MyClass_float , usw. geschrieben werden müssen, aber trotzdem enden können kompilierter Code, der meistens so ist, als hätten wir jede Version einzeln geschrieben. Eine Vorlage ist also buchstäblich eine Vorlage. Eine Klassenvorlage ist keine Klasse, sondern ein Rezept für das Erstellen einer neuen Klasse für jedes T wir stoßen. Eine Vorlage kann nicht in Code kompiliert werden, 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 MyClass<int> benötigt wird. Es kann die Vorlage 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 MyClass<int> erstellen muss, aber er kann die Vorlage MyClass<T> (nur seine Schnittstelle in foo.h ) nicht sehen, so dass er nicht erstellen kann es.

Wenn foo.cpp selbst MyClass<int> , dann wird Code für das generiert, während foo.cpp kompiliert wird. Wenn also bar.o mit foo.o verknüpft ist, können sie verbunden 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 es gibt keine Möglichkeit für bar.cpp , die Vorlage als Vorlage zu verwenden und sie in beliebigen Typen zu instanziieren. Es kann nur vorbestehende Versionen der Vorlagenklasse verwenden, die der Autor von foo.cpp gedacht 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 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
    • deklariert und implementiert die class BazPrivate und verwendet MyClass<BazPrivate>

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

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

Niemand mag es (1), weil Kompiliersysteme für die gesamte Programmanalyse dauernd kompiliert werden müssen und weil es unmöglich ist, kompilierte Bibliotheken ohne den Quellcode zu verteilen. Also haben wir stattdessen (2).




Wenn das Problem die zusätzliche Kompilierungszeit und die Größe der binären Größe ist, die durch das Kompilieren von .h als Teil aller verwendeten .cpp-Module erzeugt wird, können Sie in vielen Fällen die Template-Klasse von einer nicht-templatisierten Basisklasse abziehen Nicht typabhängige Teile der Schnittstelle, und diese Basisklasse kann ihre Implementierung in der .cpp-Datei haben.




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?) Den extern template class vector<int> damit er nicht in allen anderen (1000?) Dateien instanziiert wird das verwenden Vektor.




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
    }
};



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 das Recht hat, 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).




Tatsächlich haben Versionen des C ++ - Standards vor C ++ 11 das Schlüsselwort export definiert, das es einfach machen würde , Vorlagen in einer Header-Datei zu deklarieren und sie 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).

Daher entschied das Standardkomitee von ISO C ++, die export von Vorlagen, die mit C ++ 11 beginnen, zu entfernen.




Vorlagenvorlagen sollen einem "Konzept" folgen (Eingabe-Iterator, Vorwärts-Iterator, usw.), wobei die tatsächlichen Details des Konzepts vollständig durch die Implementierung der Vorlagenfunktion / -klasse und nicht durch die Klasse des Typs definiert werden verwendet mit der Vorlage, die eine etwas Anti-Verwendung von OOP ist.

Ich denke, Sie missverstehen die beabsichtigte Verwendung von Konzepten durch Vorlagen. Vorwärts-Iterator zum Beispiel ist ein sehr gut definiertes Konzept. Um die Ausdrücke zu finden, die gültig sein müssen, damit eine Klasse ein Forward-Iterator sein kann, und ihre Semantik, einschließlich der Komplexität der Berechnungen, sehen Sie sich den Standard oder unter http://www.sgi.com/tech/stl/ForwardIterator.html (Sie müssen den Links zu Input, Output und Trivial Iterator folgen, um alles zu sehen).

Dieses Dokument ist eine perfekte Schnittstelle, und "die tatsächlichen Details des Konzepts" sind genau dort definiert. Sie sind nicht durch die Implementierungen von Vorwärts-Iteratoren definiert, und sie sind auch nicht durch die Algorithmen definiert, die Vorwärts-Iteratoren verwenden.

Die Unterschiede in der Handhabung von Schnittstellen zwischen STL und Java sind dreifach:

1) STL definiert gültige Ausdrücke unter Verwendung des Objekts, während Java Methoden definiert, die auf dem Objekt aufrufbar sein müssen. Natürlich kann ein gültiger Ausdruck ein Methodenaufruf sein (member function), muss aber nicht sein.

2) Java-Interfaces sind Laufzeitobjekte, während STL-Konzepte zur Laufzeit auch mit RTTI nicht sichtbar sind.

3) Wenn Sie die erforderlichen gültigen Ausdrücke für ein STL-Konzept nicht gültig machen können, erhalten Sie einen nicht angegebenen Kompilierungsfehler, wenn Sie eine Vorlage mit dem Typ instanziieren. Wenn Sie eine erforderliche Methode einer Java-Schnittstelle nicht implementieren können, erhalten Sie einen spezifischen Kompilierungsfehler, der besagt.

Dieser dritte Teil ist, wenn Sie eine Art (Compile-Time) "Ente Typisierung" mögen: Schnittstellen können implizit sein. In Java sind Interfaces etwas explizit: Eine Klasse "ist" nur dann iterierbar, wenn sie Iterable implementiert. Der Compiler kann prüfen, ob die Signaturen seiner Methoden vorhanden und korrekt sind, aber die Semantik ist immer noch implizit (dh sie sind entweder dokumentiert oder nicht, aber nur mehr Code (Komponententests) kann Ihnen sagen, ob die Implementierung korrekt ist).

In C ++, wie in Python, sind sowohl Semantik als auch Syntax implizit, obwohl Sie in C ++ (und in Python, wenn Sie den stark schreibenden Präprozessor erhalten) Hilfe vom Compiler bekommen. Wenn ein Programmierer eine Java-ähnliche explizite Deklaration von Schnittstellen durch die implementierende Klasse benötigt, dann besteht die Standardmethode darin, Typmerkmale zu verwenden (und Mehrfachvererbung kann verhindern, dass dies zu ausführlich ist). Was im Vergleich zu Java fehlt, ist eine einzelne Vorlage, die ich mit meinem Typ instanziieren kann und die nur dann kompiliert wird, wenn alle erforderlichen Ausdrücke für meinen Typ gültig sind. Dies würde mir sagen, ob ich alle erforderlichen Bits implementiert habe, "bevor ich es benutze". Das ist eine Annehmlichkeit, aber es ist nicht der Kern von OOP (und es testet immer noch keine Semantik, und Code zum Testen von Semantiken würde natürlich auch die Gültigkeit der betreffenden Ausdrücke testen).

STL kann oder sollte nicht ausreichend für Ihren Geschmack sein, aber es trennt sicher die Schnittstelle sauber von der Implementierung. Es verfügt nicht über die Fähigkeit von Java, Reflektionen über Schnittstellen auszuführen, und es meldet, dass die Schnittstellenanforderungen unterschiedlich sind.

Sie können die Funktion angeben ... erwartet einen Forward Iterator nur, indem Sie sich die Definition ansehen, in der Sie entweder die Implementierung oder die Dokumentation für ...

Persönlich denke ich, dass implizite Typen eine Stärke sind, wenn sie angemessen verwendet werden. Der Algorithmus sagt, was er mit seinen Template-Parametern macht, und der Implementierer stellt sicher, dass diese Dinge funktionieren: Es ist genau der gemeinsame Nenner dessen, was "Interfaces" tun sollten. Außerdem ist es unwahrscheinlich, dass Sie STL verwenden, sagen wir, std::copy , wenn Sie die Vorwärtsdeklaration in einer Header-Datei finden. Programmierer sollten anhand ihrer Dokumentation herausfinden, was eine Funktion annimmt und nicht nur die Funktionssignatur. Dies gilt für C ++, Python oder Java. Es gibt Einschränkungen in Bezug darauf, was mit der Eingabe in einer beliebigen Sprache erreicht werden kann, und es wäre ein Fehler, zu versuchen, die Eingabe zu verwenden, um etwas zu tun, was sie nicht tut (Semantik überprüfen).

Das heißt, STL-Algorithmen benennen ihre Template-Parameter normalerweise so, dass klar ist, welches Konzept benötigt wird. Dies dient jedoch dazu, nützliche zusätzliche Informationen in der ersten Zeile der Dokumentation bereitzustellen, um Weiterleitungsdeklarationen nicht informativer zu machen. Es gibt mehr Dinge, die Sie wissen müssen, als in den Typen der Parameter eingekapselt werden können, also müssen Sie die Dokumente lesen. (Zum Beispiel in Algorithmen, die einen Eingabebereich und einen Ausgabe-Iterator verwenden, hat der Ausgabe-Iterator wahrscheinlich genug "Platz" für eine bestimmte Anzahl von Ausgaben basierend auf der Größe des Eingabebereichs und möglicherweise der darin enthaltenen Werte. )

Hier ist Bjarne auf explizit deklarierten Schnittstellen: http://www.artima.com/cppsource/cpp0xP.html

In Generics muss ein Argument eine Klasse sein, die von einer Schnittstelle abgeleitet ist (die C ++ - Entsprechung zur Schnittstelle ist eine abstrakte Klasse), die in der Definition des generischen Objekts angegeben ist. Das bedeutet, dass alle generischen Argumenttypen in eine Hierarchie passen müssen. Das, das Designs unnötige Beschränkungen auferlegt, erfordert unvernünftige Voraussicht seitens der Entwickler. Wenn Sie beispielsweise einen generischen Code schreiben und eine Klasse definieren, können Benutzer meine Klasse nicht als Argument für Ihren generischen Code verwenden, es sei denn, ich wusste von der angegebenen Schnittstelle und hatte meine Klasse daraus abgeleitet. Das ist starr.

Wenn man es andersherum betrachtet, kann man mit der Eingabe von Enten eine Schnittstelle implementieren, ohne zu wissen, dass die Schnittstelle existiert. Oder jemand kann eine Schnittstelle absichtlich so schreiben, dass Ihre Klasse sie implementiert, nachdem er Ihre Dokumente konsultiert hat, um zu sehen, dass sie nicht nach etwas fragen, was Sie nicht bereits tun. Das ist flexibel.







c++ templates c++-faq