Speichern von C ++ - Vorlagenfunktionsdefinitionen in einer CPP-Datei

526

Ich habe einen Vorlagencode, den ich lieber in einer CPP-Datei als inline im Header gespeichert hätte. Ich weiß, dass dies möglich ist, solange Sie wissen, welche Vorlagentypen verwendet werden. Zum Beispiel:

.h Datei

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

CPP-Datei

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Beachten Sie die letzten beiden Zeilen - die Funktion foo :: do template wird nur mit ints und std :: strings verwendet. Diese Definitionen bedeuten also, dass die App verknüpft wird.

Meine Frage ist - ist das ein böser Hack oder funktioniert das mit anderen Compilern / Linkern? Ich verwende diesen Code derzeit nur mit VS2008, möchte ihn jedoch auf andere Umgebungen portieren.

rauben
quelle
22
Ich hatte keine Ahnung, dass dies möglich ist - ein interessanter Trick! Es hätte einigen jüngsten Aufgaben erheblich geholfen, dies zu wissen - Prost!
Xan
69
Die Sache, die mich stampft, ist die Verwendung doals Kennung: p
Quentin
Ich habe etwas Ähnliches mit gcc gemacht, aber immer noch recherchiert
Nick
16
Dies ist kein "Hack", sondern eine Vorwärtsdekleration. Dies hat einen Platz im Standard der Sprache; Also ja, es ist in jedem standardkonformen Compiler erlaubt.
Ahmet Ipkin
1
Was ist, wenn Sie Dutzende von Methoden haben? Können Sie dies template class foo<int>;template class foo<std::string>;am Ende der CPP-Datei tun?
Ignorant

Antworten:

231

Das von Ihnen beschriebene Problem kann durch Definieren der Vorlage in der Kopfzeile oder über den oben beschriebenen Ansatz gelöst werden.

Ich empfehle, die folgenden Punkte aus dem C ++ FAQ Lite zu lesen :

Sie gehen sehr detailliert auf diese (und andere) Vorlagenprobleme ein.

Aaron N. Tubbs
quelle
39
Um die Antwort zu ergänzen, beantwortet der Link, auf den verwiesen wird, die Frage positiv, dh es ist möglich, das zu tun, was Rob vorgeschlagen hat, und den Code portabel zu haben.
Ivotron
161
Können Sie die relevanten Teile einfach in der Antwort selbst posten? Warum ist eine solche Referenzierung überhaupt auf SO erlaubt? Ich habe keine Ahnung, wonach ich in diesem Link suchen soll, da er seitdem stark verändert wurde.
Ident
124

Für andere auf dieser Seite, die sich fragen, wie die richtige Syntax (wie ich) für die explizite Vorlagenspezialisierung (oder zumindest in VS2008) lautet, ist die folgende ...

In Ihrer .h-Datei ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

Und in Ihrer CPP-Datei

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
Namespace Sid
quelle
15
Meinen Sie "für explizite CLASS-Template-Spezialisierung". In diesem Fall deckt das jede Funktion ab, die die Vorlagenklasse hat?
Arthur
@Arthur scheint nicht, ich habe einige Vorlagenmethoden im Header und die meisten anderen Methoden in cpp, funktioniert gut. Sehr schöne Lösung.
user1633272
Im Fall des Fragestellers haben sie eine Funktionsvorlage, keine Klassenvorlage.
user253751
23

Dieser Code ist wohlgeformt. Sie müssen nur darauf achten, dass die Definition der Vorlage zum Zeitpunkt der Instanziierung sichtbar ist. Um den Standard zu zitieren, § 14.7.2.4:

Die Definition einer nicht exportierten Funktionsvorlage, einer nicht exportierten Elementfunktionsvorlage oder einer nicht exportierten Elementfunktion oder eines statischen Datenelements einer Klassenvorlage muss in jeder Übersetzungseinheit vorhanden sein, in der sie explizit instanziiert wird.

Konrad Rudolph
quelle
2
Was bedeutet nicht exportiert ?
Dan Nissenbaum
1
@Dan Nur innerhalb der Kompilierungseinheit sichtbar, nicht außerhalb. Wenn Sie mehrere Kompilierungseinheiten miteinander verknüpfen, können exportierte Symbole über diese hinweg verwendet werden (und müssen eine einzige oder zumindest bei Vorlagen konsistente Definitionen enthalten, andernfalls stoßen Sie auf UB).
Konrad Rudolph
Vielen Dank. Ich dachte, dass alle Funktionen (standardmäßig) außerhalb der Kompilierungseinheit sichtbar sind. Wenn ich zwei Kompilierungseinheiten habe a.cpp(Definition der Funktion a() {}) und b.cpp(Definition der Funktion b() { a() }), wird dies erfolgreich verknüpft. Wenn ich recht habe, dann scheint das obige Zitat nicht für den typischen Fall zu gelten ... irre ich mich irgendwo?
Dan Nissenbaum
@ Dan Trivial Gegenbeispiel: inlineFunktionen
Konrad Rudolph
1
@ Dan Funktionsvorlagen sind implizit inline. Der Grund dafür ist, dass es ohne ein standardisiertes C ++ - ABI schwierig ist, den Effekt zu definieren, den dies sonst hätte.
Konrad Rudolph
15

Dies sollte überall dort gut funktionieren, wo Vorlagen unterstützt werden. Die explizite Instanziierung von Vorlagen ist Teil des C ++ - Standards.

Mondschatten
quelle
13

Ihr Beispiel ist korrekt, aber nicht sehr portabel. Es gibt auch eine etwas sauberere Syntax, die verwendet werden kann (wie von @ namespace-sid hervorgehoben).

Angenommen, die Vorlagenklasse ist Teil einer Bibliothek, die gemeinsam genutzt werden soll. Sollten andere Versionen der Vorlagenklasse kompiliert werden? Soll der Bibliotheksverwalter alle möglichen Vorlagenverwendungen der Klasse vorwegnehmen?

Ein alternativer Ansatz ist eine geringfügige Abweichung von dem, was Sie haben: Fügen Sie eine dritte Datei hinzu, die die Vorlagenimplementierungs- / Instanziierungsdatei ist.

foo.h Datei

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

foo.cpp Datei

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

foo-impl.cpp Datei

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Die einzige Einschränkung ist, dass Sie dem Compiler mitteilen müssen, dass er kompilieren soll, foo-impl.cppanstatt dass foo.cppdas Kompilieren des letzteren nichts bewirkt.

Natürlich können Sie mehrere Implementierungen in der dritten Datei oder mehrere Implementierungsdateien für jeden Typ haben, den Sie verwenden möchten.

Dies ermöglicht viel mehr Flexibilität beim Freigeben der Vorlagenklasse für andere Zwecke.

Dieses Setup reduziert auch die Kompilierungszeiten für wiederverwendete Klassen, da Sie nicht in jeder Übersetzungseinheit dieselbe Headerdatei neu kompilieren.

Cameron Tacklind
quelle
Was kauft dir das? Sie müssen foo-impl.cpp noch bearbeiten, um eine neue Spezialisierung hinzuzufügen.
MK.
Trennung von Implementierungsdetails (auch als Definitionen in bezeichnet foo.cpp), aus denen Versionen tatsächlich kompiliert werden (in foo-impl.cpp), und Deklarationen (in foo.h). Ich mag es nicht, dass die meisten C ++ - Vorlagen vollständig in Header-Dateien definiert sind. Dies steht im Widerspruch zum C / C ++ - Standard von Paaren c[pp]/hfür jede Klasse / jeden Namespace / jede von Ihnen verwendete Gruppierung. Die Leute scheinen immer noch monolithische Header-Dateien zu verwenden, einfach weil diese Alternative nicht weit verbreitet oder bekannt ist.
Cameron Tacklind
1
@MK. Ich habe die expliziten Vorlageninstanziierungen zunächst am Ende der Definition in die Quelldatei eingefügt, bis ich weitere Instanziierungen an anderer Stelle benötigte (z. B. Komponententests mit einem Mock als Schablonentyp). Diese Trennung ermöglicht es mir, weitere Instanziierungen extern hinzuzufügen. Außerdem funktioniert es immer noch, wenn ich das Original als h/cppPaar behalte, obwohl ich die ursprüngliche Liste der Instanziierungen in einem Include-Guard umgeben musste, aber ich konnte das wie gewohnt kompilieren foo.cpp. Ich bin jedoch noch ziemlich neu in C ++ und würde mich interessieren, ob diese gemischte Verwendung zusätzliche Einschränkungen aufweist.
Thirdwater
3
Ich denke, es ist vorzuziehen, foo.cppund zu entkoppeln foo-impl.cpp. Nicht #include "foo.cpp"in der foo-impl.cppDatei; Fügen Sie stattdessen die Deklaration hinzu, extern template class foo<int>;um foo.cppzu verhindern, dass der Compiler die Vorlage beim Kompilieren instanziiert foo.cpp. Stellen Sie sicher, dass das Build-System beide .cppDateien erstellt und beide Objektdateien an den Linker übergibt. Dies hat mehrere Vorteile: a) Es ist klar, foo.cppdass es keine Instanziierung gibt; b) Änderungen an foo.cpp erfordern keine Neukompilierung von foo-impl.cpp.
Shmuel Levine
3
Dies ist ein sehr guter Ansatz für das Problem der Vorlagendefinitionen, der das Beste aus beiden Welten herausholt - Header-Implementierung und Instanziierung für häufig verwendete Typen. Die einzige Änderung, die ich an diesem Setup vornehmen würde, ist das Umbenennen foo.cppin foo_impl.hund foo-impl.cppin just foo.cpp. Ich würde auch typedefs für instantiations von hinzufügen foo.cppzu foo.h, ebenfalls using foo_int = foo<int>;. Der Trick besteht darin, den Benutzern zwei Header-Schnittstellen zur Auswahl zu stellen. Wenn der Benutzer eine vordefinierte Instanziierung benötigt, schließt er ein foo.h, wenn der Benutzer etwas benötigt, das nicht in der richtigen Reihenfolge ist foo_impl.h.
Wormer
5

Dies ist definitiv kein böser Hack, aber beachten Sie, dass Sie dies (die explizite Vorlagenspezialisierung) für jede Klasse / jeden Typ tun müssen, den Sie mit der angegebenen Vorlage verwenden möchten. Bei VIELEN Typen, die eine Vorlageninstanziierung anfordern, kann Ihre CPP-Datei VIELE Zeilen enthalten. Um dieses Problem zu beheben, können Sie in jedem Projekt, das Sie verwenden, eine TemplateClassInst.cpp verwenden, damit Sie besser steuern können, welche Typen instanziiert werden. Offensichtlich ist diese Lösung nicht perfekt (auch bekannt als Silver Bullet), da Sie möglicherweise die ODR brechen :).

Rot XIII
quelle
Sind Sie sicher, dass es die ODR brechen wird? Wenn sich die Instanziierungszeilen in TemplateClassInst.cpp auf die identische Quelldatei beziehen (die die Definitionen der Vorlagenfunktionen enthält), wird dadurch nicht garantiert, dass die ODR nicht verletzt wird, da alle Definitionen identisch sind (auch wenn sie wiederholt werden)?
Dan Nissenbaum
Bitte, was ist ODR?
entfernbar
4

Im neuesten Standard gibt es ein Schlüsselwort ( export), mit dem dieses Problem behoben werden kann. Es ist jedoch in keinem anderen mir bekannten Compiler als Comeau implementiert.

Siehe dazu die FAQ-Lite .

Ben Collins
quelle
2
AFAIK, der Export ist tot, weil sie jedes Mal mit immer neueren Problemen konfrontiert sind, wenn sie das letzte lösen, was die Gesamtlösung immer komplizierter macht. Und das Schlüsselwort "export" ermöglicht es Ihnen sowieso nicht, von einem CPP zu "exportieren" (immer noch von H. Sutter). Also sage ich: Halten Sie nicht den Atem an ...
paercebal
2
Um den Export zu implementieren, benötigt der Compiler weiterhin die vollständige Vorlagendefinition. Alles, was Sie gewinnen, ist, es in einer Art kompilierter Form zu haben. Aber es hat wirklich keinen Sinn.
Zan Lynx
2
... und es ist vom Standard weg , wegen übermäßiger Komplikationen für minimalen Gewinn.
DevSolar
4

Dies ist eine Standardmethode zum Definieren von Vorlagenfunktionen. Ich denke, es gibt drei Methoden, die ich zum Definieren von Vorlagen lese. Oder wahrscheinlich 4. Jeder mit Vor- und Nachteilen.

  1. In Klassendefinition definieren. Ich mag das überhaupt nicht, weil ich denke, dass Klassendefinitionen nur als Referenz dienen und leicht zu lesen sein sollten. Es ist jedoch viel weniger schwierig, Vorlagen in der Klasse zu definieren als außerhalb. Und nicht alle Vorlagendeklarationen sind gleich komplex. Diese Methode macht die Vorlage auch zu einer echten Vorlage.

  2. Definieren Sie die Vorlage im selben Header, jedoch außerhalb der Klasse. Dies ist meistens mein bevorzugter Weg. Es hält Ihre Klassendefinition aufgeräumt, die Vorlage bleibt eine echte Vorlage. Es erfordert jedoch eine vollständige Benennung der Vorlagen, was schwierig sein kann. Außerdem steht Ihr Code allen zur Verfügung. Wenn Sie jedoch möchten, dass Ihr Code inline ist, ist dies der einzige Weg. Sie können dies auch erreichen, indem Sie am Ende Ihrer Klassendefinitionen eine INL-Datei erstellen.

  3. Fügen Sie die Datei header.h und Implementation.CPP in Ihre main.CPP ein. Ich denke, so wird es gemacht. Sie müssen keine Vorinstanziierungen vorbereiten, es verhält sich wie eine echte Vorlage. Das Problem, das ich damit habe, ist, dass es nicht natürlich ist. Normalerweise schließen wir keine Quelldateien ein und erwarten diese auch nicht. Ich denke, da Sie die Quelldatei aufgenommen haben, können die Vorlagenfunktionen eingebunden werden.

  4. Diese letzte Methode, wie sie veröffentlicht wurde, definiert die Vorlagen in einer Quelldatei, genau wie Nummer 3; Anstatt die Quelldatei einzuschließen, instanziieren wir die Vorlagen vorab auf diejenigen, die wir benötigen. Ich habe kein Problem mit dieser Methode und es ist manchmal nützlich. Wir haben einen großen Code, der nicht von Inline profitieren kann. Fügen Sie ihn einfach in eine CPP-Datei ein. Und wenn wir gemeinsame Instanziierungen kennen und sie vordefinieren können. Dies erspart uns, 5, 10 Mal im Grunde dasselbe zu schreiben. Diese Methode hat den Vorteil, dass unser Code geschützt bleibt. Ich empfehle jedoch nicht, winzige, regelmäßig verwendete Funktionen in CPP-Dateien einzufügen. Dies verringert die Leistung Ihrer Bibliothek.

Beachten Sie, dass mir die Folgen einer aufgeblähten obj-Datei nicht bekannt sind.

Cássio Renan
quelle
3

Ja, dies ist die Standardmethode für die explizite Instanziierung von Spezialisierungen . Wie Sie angegeben haben, können Sie diese Vorlage nicht mit anderen Typen instanziieren.

Bearbeiten: basierend auf Kommentar korrigiert.

Lou Franco
quelle
In Bezug auf die Terminologie wählerisch zu sein, ist eine "explizite Instanziierung".
Richard Corden
2

Nehmen wir ein Beispiel, aus irgendeinem Grund möchten Sie eine Vorlagenklasse haben:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Wenn Sie diesen Code mit Visual Studio kompilieren, funktioniert er sofort. gcc erzeugt einen Linkerfehler (wenn dieselbe Header-Datei aus mehreren CPP-Dateien verwendet wird):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Es ist möglich, die Implementierung in eine CPP-Datei zu verschieben, aber dann müssen Sie die Klasse wie folgt deklarieren:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

Und dann sieht .cpp so aus:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Ohne zwei letzte Zeilen in der Header-Datei funktioniert gcc einwandfrei, aber Visual Studio erzeugt einen Fehler:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

Die Syntax der Vorlagenklasse ist optional, wenn Sie die Funktion über den DLL-Export verfügbar machen möchten. Dies gilt jedoch nur für die Windows-Plattform. Daher könnte test_template.h folgendermaßen aussehen:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

mit .cpp-Datei aus dem vorherigen Beispiel.

Dies bereitet dem Linker jedoch mehr Kopfschmerzen. Es wird daher empfohlen, das vorherige Beispiel zu verwenden, wenn Sie die DLL-Funktion nicht exportieren.

TarmoPikaro
quelle
1

Zeit für ein Update! Erstellen Sie eine Inline-Datei (.inl oder wahrscheinlich eine andere) und kopieren Sie einfach alle Ihre Definitionen darin. Stellen Sie sicher, dass Sie die Vorlage über jeder Funktion hinzufügen ( template <typename T, ...>). Anstatt die Header-Datei in die Inline-Datei aufzunehmen, machen Sie jetzt das Gegenteil. Fügen Sie die Inline-Datei nach der Deklaration Ihrer Klasse ein ( #include "file.inl").

Ich weiß nicht wirklich, warum das niemand erwähnt hat. Ich sehe keine unmittelbaren Nachteile.

Didii
quelle
25
Der unmittelbare Nachteil ist, dass es im Grunde dasselbe ist, wie nur die Vorlagenfunktionen direkt im Header zu definieren. Sobald Sie #include "file.inl", wird der Präprozessor den Inhalt von file.inldirekt in den Header einfügen . Aus welchem ​​Grund auch immer Sie die Implementierung im Header vermeiden wollten, diese Lösung löst dieses Problem nicht.
Cody Gray
5
- und bedeutet, dass Sie sich technisch unnötig mit der Aufgabe belasten, alle ausführlichen, umwerfenden Boilerplates zu schreiben, die für Out-of-Line- templateDefinitionen erforderlich sind . Ich verstehe, warum die Leute es tun wollen - um die größtmögliche Parität mit Nicht-Template-Deklarationen / -Definitionen zu erreichen, um die Schnittstellendeklaration sauber zu halten usw. -, aber es lohnt sich nicht immer. Es geht darum, die Kompromisse auf beiden Seiten zu bewerten und die am wenigsten schlechten auszuwählen . ... bis es namespace classzu einer Sache wird: O [ bitte sei eine Sache ]
underscore_d
2
@ Andrew Es scheint in den Rohren des Komitees stecken geblieben zu sein, obwohl ich glaube, ich habe jemanden gesehen, der sagte, das sei nicht beabsichtigt. Ich wünschte, es hätte es in C ++ 17 geschafft. Vielleicht nächstes Jahrzehnt.
underscore_d
@CodyGray: Technisch gesehen ist dies für den Compiler tatsächlich dasselbe und verringert daher nicht die Kompilierungszeit. Dennoch denke ich, dass dies erwähnenswert ist und in einer Reihe von Projekten praktiziert wurde, die ich gesehen habe. Wenn Sie diesen Weg gehen, können Sie Interface von der Definition trennen. Dies ist eine gute Vorgehensweise. In diesem Fall hilft es nicht bei der ABI-Kompatibilität oder ähnlichem, aber es erleichtert das Lesen und Verstehen der Schnittstelle.
Kiloalphaindia
0

An dem Beispiel, das Sie gegeben haben, ist nichts auszusetzen. Aber ich muss sagen, ich glaube, es ist nicht effizient, Funktionsdefinitionen in einer CPP-Datei zu speichern. Ich verstehe nur die Notwendigkeit, die Deklaration und Definition der Funktion zu trennen.

In Verbindung mit der expliziten Klasseninstanziierung kann Ihnen die Boost Concept Check Library (BCCL) dabei helfen, Vorlagenfunktionscode in CPP-Dateien zu generieren.

Benoît
quelle
8
Was ist daran ineffizient?
Cody Gray
0

Keiner der oben genannten Punkte hat für mich funktioniert. Hier ist, wie Sie ihn gelöst haben. Meine Klasse hat nur eine Methode als Vorlage.

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

Dadurch werden Linkerfehler vermieden und TemporaryFunction muss überhaupt nicht aufgerufen werden

KronuZ
quelle