Explizite Vorlageninstanziierung - wann wird sie verwendet?


92

Nach einigen Wochen versuche ich, mein Wissen über Vorlagen mit dem Buch Vorlagen - Der vollständige Leitfaden von David Vandevoorde und Nicolai M. Josuttis zu erweitern und zu erweitern. Was ich derzeit zu verstehen versuche, ist die explizite Instanziierung von Vorlagen .

Ich habe eigentlich kein Problem mit dem Mechanismus als solchem, aber ich kann mir keine Situation vorstellen, in der ich diese Funktion verwenden möchte oder möchte. Wenn mir das jemand erklären kann, bin ich mehr als dankbar.

Antworten:


66

Direkt von https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation kopiert :

Sie können die explizite Instanziierung verwenden, um eine Instanziierung einer Klasse oder Funktion mit Vorlagen zu erstellen, ohne sie tatsächlich in Ihrem Code zu verwenden. Da dies nützlich ist, wenn Sie Bibliotheksdateien (.lib) erstellen, die Vorlagen für die Verteilung verwenden, werden unbegründete Vorlagendefinitionen nicht in Objektdateien (.obj) abgelegt.

(Zum Beispiel enthält libstdc ++ die explizite Instanziierung von std::basic_string<char,char_traits<char>,allocator<char> >(was ist std::string), sodass jedes Mal, wenn Sie Funktionen von verwenden std::string, derselbe Funktionscode nicht in Objekte kopiert werden muss. Der Compiler muss diese nur auf libstdc ++ verweisen (verknüpfen).)


7
Ja, die MSVC-CRT-Bibliotheken verfügen über explizite Instanziierungen für alle Stream-, Gebietsschema- und Zeichenfolgenklassen, die auf char und wchar_t spezialisiert sind. Die resultierende .lib ist über 5 Megabyte groß.
Hans Passant

4
Woher weiß der Compiler, dass die Vorlage an anderer Stelle explizit instanziiert wurde? Wird es nicht einfach die Klassendefinition generieren, weil sie verfügbar ist?

@STing: Wenn die Vorlage instanziiert wird, werden diese Funktionen in die Symboltabelle eingetragen.
Kennytm

@Kenny: Du meinst, wenn es bereits in derselben TU instanziiert ist? Ich würde annehmen, dass jeder Compiler klug genug ist, um dieselbe Spezialisierung nicht mehr als einmal in derselben TU zu instanziieren. Ich dachte, der Vorteil der expliziten Instanziierung (in Bezug auf Build- / Link-Zeiten) besteht darin, dass eine Spezialisierung, die in einer TU (explizit) instanziiert wird, in den anderen TUs, in denen sie verwendet wird, nicht instanziiert wird. Nein?

4
@Kenny: Ich kenne die GCC-Option, um implizite Instanziierung zu verhindern, aber dies ist kein Standard. Soweit ich weiß, hat VC ++ keine solche Option. Explizite inst. wird immer als Verbesserung der Kompilierungs- / Verknüpfungszeiten angepriesen (auch von Bjarne), aber damit es diesem Zweck dient, muss der Compiler irgendwie wissen, dass er Vorlagen nicht implizit instanziiert (z. B. über das GCC-Flag), oder er darf das nicht erhalten Vorlagendefinition, nur eine Deklaration. Klingt das richtig? Ich versuche nur zu verstehen, warum man explizite Instanziierung verwenden würde (außer um die konkreten Typen einzuschränken).

83

Wenn Sie eine Vorlagenklasse definieren, die nur für einige explizite Typen verwendet werden soll.

Fügen Sie die Vorlagendeklaration wie eine normale Klasse in die Header-Datei ein.

Fügen Sie die Vorlagendefinition wie eine normale Klasse in eine Quelldatei ein.

Instanziieren Sie dann am Ende der Quelldatei explizit nur die Version, die verfügbar sein soll.

Dummes Beispiel:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

Quelle:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Main

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

1
Ist es richtig zu sagen , dass , wenn der Compiler die gesamte Template - Definition (einschließlich Funktionsdefinitionen) in einer bestimmten Übersetzungseinheit hat, es wird eine Spezialisierung der Vorlage instanziiert bei Bedarf (unabhängig davon , ob diese Spezialisierung wurde explizit in einer anderen TU instanziiert)? Das heißt, um die Vorteile der expliziten Instanziierung für die Kompilierung / Verknüpfungszeit zu nutzen, muss nur die Vorlagendeklaration eingefügt werden, damit der Compiler sie nicht instanziieren kann.

1
@ user123456: Wahrscheinlich compilerabhängig. Aber mehr als wahrscheinlich in den meisten Situationen wahr.
Martin York

1
Gibt es eine Möglichkeit, den Compiler dazu zu bringen, diese explizit instanziierte Version für die von Ihnen vorgegebenen Typen zu verwenden? Wenn Sie jedoch versuchen, die Vorlage mit einem "seltsamen / unerwarteten" Typ zu instanziieren, muss sie "wie gewohnt" funktionieren, wo sie gerade funktioniert instanziiert die Vorlage nach Bedarf?
David Doria

2
Was wäre eine gute Prüfung, um sicherzustellen, dass die expliziten Instanziierungen tatsächlich verwendet werden? Das heißt, es funktioniert, aber ich bin nicht ganz davon überzeugt, dass nicht nur alle Vorlagen bei Bedarf instanziiert werden.
David Doria

7
Die meisten der obigen Kommentar-Chatter sind nicht mehr wahr, da c ++ 11: Eine explizite Instanziierungsdeklaration (eine externe Vorlage) implizite Instanziierungen verhindert: Der Code, der andernfalls eine implizite Instanziierung verursachen würde, muss die explizite Instanziierungsdefinition verwenden, die an einer anderen Stelle in der Programm (normalerweise in einer anderen Datei: Dies kann verwendet werden, um die Kompilierungszeiten zu verkürzen) en.cppreference.com/w/cpp/language/class_template
xaxxon

17

Durch explizite Instanziierung können Kompilierungszeiten und Objektgrößen reduziert werden

Dies sind die Hauptgewinne, die es bieten kann. Sie stammen aus den folgenden zwei Effekten, die in den folgenden Abschnitten ausführlich beschrieben werden:

  • Entfernen Sie Definitionen aus den Headern, um zu verhindern, dass Build-Tools Includer neu erstellen
  • Objektneudefinition

Entfernen Sie Definitionen aus den Headern

Durch explizite Instanziierung können Sie Definitionen in der CPP-Datei belassen.

Wenn sich die Definition im Header befindet und Sie sie ändern, kompiliert ein intelligentes Build-System alle Einschlüsse neu. Dies können Dutzende von Dateien sein, was die Kompilierung unerträglich langsam macht.

Das Einfügen von Definitionen in CPP-Dateien hat den Nachteil, dass externe Bibliotheken die Vorlage nicht mit ihren eigenen neuen Klassen wiederverwenden können. Die folgende Option enthält jedoch eine Problemumgehung.

Siehe konkrete Beispiele unten.

Objektneudefinition gewinnt: das Problem verstehen

Wenn Sie eine Vorlage in einer Header-Datei nur vollständig definieren, kompiliert jede einzelne Kompilierungseinheit, die diesen Header enthält, eine eigene implizite Kopie der Vorlage für jede andere verwendete Verwendung von Vorlagenargumenten.

Dies bedeutet viel nutzlose Festplattennutzung und Kompilierungszeit.

Hier ist ein konkretes Beispiel, in dem beide main.cppund aufgrund ihrer Verwendung in diesen Dateien notmain.cppimplizit definiert MyTemplate<int>werden.

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub stromaufwärts .

Kompilieren und Anzeigen von Symbolen mit nm:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

Ausgabe:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Aus sehen man nmwir, dass Wdies ein schwaches Symbol bedeutet, das GCC gewählt hat, weil dies eine Vorlagenfunktion ist. Schwaches Symbol bedeutet, dass der kompilierte implizit generierte Code für MyTemplate<int>beide Dateien kompiliert wurde.

Der Grund, warum es bei der Verknüpfung mit mehreren Definitionen nicht explodiert, ist, dass der Linker mehrere schwache Definitionen akzeptiert und nur eine davon auswählt, um sie in die endgültige ausführbare Datei einzufügen.

Die Zahlen in der Ausgabe bedeuten:

  • 0000000000000000: Adresse innerhalb des Abschnitts. Diese Null liegt daran, dass Vorlagen automatisch in einen eigenen Abschnitt eingefügt werden
  • 0000000000000017: Größe des für sie generierten Codes

Wir können dies etwas deutlicher sehen mit:

objdump -S main.o | c++filt

was endet in:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

und _ZN10MyTemplateIiE1fEiist der verstümmelte Name, von MyTemplate<int>::f(int)>dem c++filtentschieden wurde, nicht zu entwirren.

Wir sehen also, dass für jede einzelne Methodeninstanziierung ein separater Abschnitt generiert wird und dass jeder von ihnen natürlich Platz in den Objektdateien beansprucht.

Lösungen für das Problem der Objektneudefinition

Dieses Problem kann durch explizite Instanziierung vermieden werden.

  • Verschieben Sie die Definition in die CPP-Datei, lassen Sie nur die Deklaration in HPP, dh ändern Sie das ursprüngliche Beispiel wie folgt:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;

    Nachteil: Externe Projekte können Ihre Vorlage nicht mit eigenen Typen verwenden. Außerdem müssen Sie alle Typen explizit instanziieren. Aber vielleicht ist dies ein Vorteil, da Programmierer es dann nicht vergessen werden.

  • Behalten Sie die Definition auf hpp bei und fügen Sie extern templatejeden Einschluss hinzu, siehe auch: Verwenden einer externen Vorlage (C ++ 11), dh ändern Sie das ursprüngliche Beispiel wie folgt :

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }

    Nachteil: Alle Einschließer müssen das externzu ihren CPP-Dateien hinzufügen , was Programmierer wahrscheinlich vergessen werden.

  • Behalten Sie die Definition auf hpp bei und fügen Sie extern templatehpp für Typen hinzu, die explizit instanziiert werden sollen:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }

    Nachteil: Sie zwingen externe Projekte, ihre eigene explizite Instanziierung durchzuführen.

Mit einer dieser Lösungen nmenthält jetzt:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

so sehen wir haben haben nur mytemplate.oeine zusammenstellung von MyTemplate<int>wie gewünscht, während notmain.ound main.onicht weil Ubedeutet undefiniert.

Entfernen Sie Definitionen aus den enthaltenen Headern, machen Sie aber auch Vorlagen für eine externe API verfügbar

Schließlich gibt es noch einen Anwendungsfall, den Sie berücksichtigen sollten, wenn Sie beides möchten:

  • Beschleunigen Sie die Kompilierung Ihres Projekts
  • Stellen Sie Header als externe Bibliotheks-API bereit, damit andere sie verwenden können

Um dies zu lösen, können Sie eine der folgenden Aktionen ausführen:

    • mytemplate.hpp: Vorlagendefinition
    • mytemplate_interface.hpp: Vorlagendeklaration, die nur mit den Definitionen von übereinstimmt mytemplate_interface.hpp, keine Definitionen
    • mytemplate.cpp: mytemplate.hppexplizite Instantiierungen einschließen und vornehmen
    • main.cppund überall sonst in der Codebasis: einschließen mytemplate_interface.hpp, nichtmytemplate.hpp
    • mytemplate.hpp: Vorlagendefinition
    • mytemplate_implementation.hpp: schließt jede Klasse ein mytemplate.hppund fügt externsie hinzu , die instanziiert wird
    • mytemplate.cpp: mytemplate.hppexplizite Instantiierungen einschließen und vornehmen
    • main.cppund überall sonst in der Codebasis: einschließen mytemplate_implementation.hpp, nichtmytemplate.hpp

Oder noch besser für mehrere Header: Erstellen Sie einen intf/ implOrdner in Ihrem includes/Ordner und verwenden Sie ihn immer mytemplate.hppals Namen.

Der mytemplate_interface.hppAnsatz sieht folgendermaßen aus:

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

Kompilieren und ausführen:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

Ausgabe:

2

Getestet in Ubuntu 18.04.

C ++ 20 Module

https://en.cppreference.com/w/cpp/language/modules

Ich denke, diese Funktion bietet das beste Setup für die Zukunft, sobald sie verfügbar ist, aber ich habe sie noch nicht überprüft, da sie auf meinem GCC 9.2.1 noch nicht verfügbar ist.

Sie müssen noch eine explizite Instanziierung durchführen, um die Geschwindigkeit zu erhöhen / die Festplatte zu speichern, aber zumindest haben wir eine vernünftige Lösung für "Entfernen von Definitionen aus enthaltenen Headern, aber auch Anzeigen einer externen API für Vorlagen", bei der das Kopieren nicht etwa 100 Mal erforderlich ist.

Die erwartete Verwendung (ohne die explizite Begründung, nicht sicher, wie die genaue Syntax aussehen wird, siehe: Wie wird die explizite Instanziierung von Vorlagen mit C ++ 20-Modulen verwendet? )

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

und dann die unter https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/ erwähnte Zusammenstellung.

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

Daraus sehen wir, dass clang die Template-Schnittstelle + Implementierung in die Magie extrahieren kann helloworld.pcm, die eine LLVM-Zwischendarstellung der Quelle enthalten muss: Wie werden Vorlagen im C ++ - Modulsystem behandelt? Dies ermöglicht weiterhin die Vorlage von Vorlagenspezifikationen.

So analysieren Sie Ihren Build schnell, um festzustellen, ob die Vorlageninstanziierung viel bringt

Sie haben also ein komplexes Projekt und möchten entscheiden, ob die Instanziierung von Vorlagen erhebliche Vorteile bringt, ohne den vollständigen Refactor auszuführen?

Die folgende Analyse kann Ihnen dabei helfen, die vielversprechendsten Objekte zu bestimmen oder zumindest auszuwählen, die beim Experimentieren zuerst umgestaltet werden sollen, indem Sie einige Ideen ausleihen: Meine C ++ - Objektdatei ist zu groß

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

Der Traum: ein Template-Compiler-Cache

Ich denke, die ultimative Lösung wäre, wenn wir bauen könnten mit:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

und myfile.owürde dann zuvor zuvor kompilierte Vorlagen automatisch für mehrere Dateien wiederverwenden.

Dies würde 0 zusätzlichen Aufwand für die Programmierer bedeuten, abgesehen davon, dass diese zusätzliche CLI-Option an Ihr Build-System übergeben wird.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.