Jawna instancja szablonu - kiedy jest używana?


102

Po kilku tygodniach przerwy próbuję poszerzyć i poszerzyć swoją wiedzę na temat szablonów dzięki książce Templates - The Complete Guide Davida Vandevoorde'a i Nicolai M. Josuttis, a to, co próbuję teraz zrozumieć, to jawne tworzenie instancji szablonów .

Właściwie nie mam problemu z samym mechanizmem, ale nie wyobrażam sobie sytuacji, w której chciałbym lub chciałbym skorzystać z tej funkcji. Jeśli ktokolwiek może mi to wyjaśnić, będę wdzięczny.

Odpowiedzi:


68

Bezpośrednio skopiowane z https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

Możesz użyć jawnej instancji, aby utworzyć instancję klasy lub funkcji na podstawie szablonu bez faktycznego używania jej w kodzie. Ponieważ jest to przydatne podczas tworzenia plików bibliotek (.lib), które używają szablonów do dystrybucji, niezainstalowane definicje szablonów nie są umieszczane w plikach obiektów (.obj).

(Na przykład libstdc ++ zawiera jawną instancję std::basic_string<char,char_traits<char>,allocator<char> >(która jest std::string), więc za każdym razem, gdy używasz funkcji programu std::string, ten sam kod funkcji nie musi być kopiowany do obiektów. Kompilator musi tylko odnosić się (łączyć) do libstdc ++.)


8
Tak, biblioteki MSVC CRT mają jawne wystąpienia dla wszystkich klas stream, locale i string, wyspecjalizowane dla char i wchar_t. Wynikowy plik .lib ma ponad 5 megabajtów.
Hans Passant

4
Skąd kompilator wie, że szablon został jawnie utworzony w innym miejscu? Czy nie wygeneruje po prostu definicji klasy, ponieważ jest dostępna?

@STing: Jeśli utworzona zostanie instancja szablonu, w tablicy symboli pojawi się wpis tych funkcji.
kennytm

@Kenny: Masz na myśli, czy jest już utworzony w tej samej jednostce tłumaczeniowej? Zakładam, że każdy kompilator jest na tyle inteligentny, aby nie tworzyć instancji tej samej specjalizacji więcej niż raz w tej samej jednostce tłumaczeniowej. Pomyślałem, że korzyść z jawnego tworzenia instancji (w odniesieniu do czasów budowania / łączenia) polega na tym, że jeśli specjalizacja jest (jawnie) tworzona w jednej jednostce tłumaczeniowej, nie zostanie ona utworzona w innych jednostkach tłumaczeniowych, w których jest używana. Nie?

4
@Kenny: Wiem o opcji GCC, która zapobiega niejawnej instancji, ale to nie jest standard. O ile wiem, VC ++ nie ma takiej opcji. Jawne inst. jest zawsze reklamowany jako poprawiający czas kompilacji / linkowania (nawet przez Bjarne'a), ale aby służył temu celowi, kompilator musi w jakiś sposób wiedzieć, że nie może niejawnie tworzyć instancji szablonów (np. poprzez flagę GCC) lub nie może otrzymać definicja szablonu, tylko deklaracja. Czy to brzmi poprawnie? Próbuję tylko zrozumieć, dlaczego ktoś miałby używać jawnych instancji (innych niż ograniczenie konkretnych typów).

89

Jeśli zdefiniujesz klasę szablonu, z którą chcesz pracować tylko dla kilku jawnych typów.

Umieść deklarację szablonu w pliku nagłówkowym, tak jak w normalnej klasie.

Umieść definicję szablonu w pliku źródłowym, tak jak w normalnej klasie.

Następnie na końcu pliku źródłowego jawnie utwórz wystąpienie tylko wersji, która ma być dostępna.

Głupi przykład:

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

Źródło:

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

Główny

#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
Czy słuszne jest stwierdzenie, że jeśli kompilator ma całą definicję szablonu (w tym definicje funkcji) w danej jednostce tłumaczeniowej, to w razie potrzeby utworzy instancję specjalizacji szablonu (niezależnie od tego, czy ta specjalizacja została jawnie utworzona w innej jednostce tłumaczeniowej )? To znaczy, aby czerpać korzyści z jawnego tworzenia instancji w czasie kompilacji / łączenia, należy dołączyć tylko deklarację szablonu, aby kompilator nie mógł jej utworzyć?

1
@ user123456: Prawdopodobnie zależne od kompilatora. Ale bardziej niż prawdopodobne w większości sytuacji.
Martin York

1
czy istnieje sposób, aby kompilator używał tej jawnie utworzonej wersji dla typów, które wstępnie określisz, ale jeśli spróbujesz utworzyć instancję szablonu z „dziwnym / nieoczekiwanym” typem, spraw, aby działał „normalnie”, gdzie po prostu tworzy instancję szablonu w razie potrzeby?
David Doria,

2
co byłoby dobrym sprawdzeniem / testem, aby upewnić się, że jawne instancje są rzeczywiście używane? To znaczy działa, ale nie jestem w pełni przekonany, że nie jest to tylko tworzenie instancji wszystkich szablonów na żądanie.
David Doria

7
Większość powyższych komentarzy nie jest już prawdą, ponieważ c ++ 11: Jawna deklaracja instancji (szablon extern) zapobiega niejawnym instancjom: kod, który w przeciwnym razie spowodowałby niejawną instancję, musi używać jawnej definicji instancji dostarczonej gdzie indziej w program (zazwyczaj w innym pliku: można to wykorzystać do skrócenia czasu kompilacji) en.cppreference.com/w/cpp/language/class_template
xaxxon

30

Jawne tworzenie instancji pozwala skrócić czas kompilacji i rozmiary obiektów

To są główne korzyści, jakie może przynieść. Pochodzą one z następujących dwóch efektów opisanych szczegółowo w poniższych sekcjach:

  • usuń definicje z nagłówków, aby uniemożliwić narzędziom kompilacji przebudowę dołączeń (oszczędza czas)
  • redefinicja obiektu (oszczędność czasu i rozmiaru)

Usuń definicje z nagłówków

Jawna instancja umożliwia pozostawienie definicji w pliku .cpp.

Gdy definicja znajduje się w nagłówku i zmodyfikujesz ją, inteligentny system kompilacji przekompilowałby wszystkie załączniki, które mogą składać się z dziesiątek plików, prawdopodobnie powodując nieznośnie powolną przyrostową rekompilację po zmianie pojedynczego pliku.

Umieszczanie definicji w plikach .cpp ma tę wadę, że biblioteki zewnętrzne nie mogą ponownie używać szablonu z własnymi nowymi klasami, ale „Usuń definicje z dołączonych nagłówków, ale także wyświetl szablony przez zewnętrzny interfejs API” poniżej pokazuje obejście tego problemu.

Zobacz konkretne przykłady poniżej.

Korzyści z redefinicji obiektu: zrozumienie problemu

Jeśli po prostu całkowicie zdefiniujesz szablon w pliku nagłówkowym, każda pojedyncza jednostka kompilacji, która zawiera ten nagłówek, będzie kompilować własną niejawną kopię szablonu dla każdego innego użycia argumentu szablonu.

Oznacza to dużo bezużytecznego wykorzystania dysku i czasu kompilacji.

Oto konkretny przykład, w którym zarówno main.cppi notmain.cppniejawnie definiują MyTemplate<int>ze względu na użycie w tych plikach.

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 upstream .

Kompiluj i przeglądaj symbole za pomocą 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

Wynik:

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

Od man nmwidzimy, że Wdochodowe słaby symbol, który wybrał GCC, ponieważ jest to funkcja szablonu.

Powodem, dla którego nie wysadza się w czasie łączenia z wieloma definicjami jest to, że linker akceptuje wiele słabych definicji i po prostu wybiera jedną z nich do umieszczenia w ostatecznym pliku wykonywalnym, a wszystkie są takie same w naszym przypadku, więc wszystko jest w porządku.

Liczby w danych wyjściowych oznaczają:

  • 0000000000000000: adres w sekcji. To zero wynika z tego, że szablony są automatycznie umieszczane we własnej sekcji
  • 0000000000000017: rozmiar wygenerowanego dla nich kodu

Widzimy to nieco wyraźniej dzięki:

objdump -S main.o | c++filt

która kończy się:

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

i _ZN10MyTemplateIiE1fEijest zniekształconą nazwą, MyTemplate<int>::f(int)>której c++filtzdecydowano się nie rozplątywać.

Widzimy więc, że dla każdej instancji metody generowana jest osobna sekcja i że każda z nich zajmuje oczywiście miejsce w plikach obiektowych.

Rozwiązania problemu redefinicji obiektu

Tego problemu można uniknąć, używając jawnej instancji i:

  • zachowaj definicję na hpp i dodaj extern templatehpp dla typów, które mają być jawnie tworzone.

    Jak wyjaśniono w: użycie szablonu extern (C ++ 11) extern template zapobiega tworzeniu wystąpienia całkowicie zdefiniowanego szablonu przez jednostki kompilacyjne, z wyjątkiem naszej jawnej instancji. W ten sposób tylko nasza jawna instancja zostanie zdefiniowana w końcowych obiektach:

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

    Wady:

    • definicja pozostaje w nagłówku, co powoduje, że pojedyncza zmiana pliku rekompiluje się do tego nagłówka prawdopodobnie wolno
    • jeśli jesteś biblioteką tylko z nagłówkiem, zmuszasz zewnętrzne projekty do wykonania własnych, jawnych instancji. Jeśli nie jesteś biblioteką tylko z nagłówkami, to rozwiązanie jest prawdopodobnie najlepsze.
    • jeśli typ szablonu jest zdefiniowany w twoim własnym projekcie, a nie wbudowanym podobnym int, wydaje się że jesteś zmuszony dodać do niego dołączenie do nagłówka, deklaracja do przodu nie wystarczy: extern szablon i niekompletne typy Zwiększa to zależności nagłówków trochę.
  • przenosząc definicję do pliku cpp, zostaw tylko deklarację na hpp, czyli zmodyfikuj oryginalny przykład na:

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

    Wada: projekty zewnętrzne nie mogą używać Twojego szablonu z własnymi typami. Ponadto jesteś zmuszony jawnie tworzyć instancje wszystkich typów. Ale może to jest plus, ponieważ wtedy programiści nie zapomną.

  • zachowaj definicję na hpp i dodaj extern templatedo każdego elementu włączającego:

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

    Wada: wszystkie włączniki muszą dodać rozszerzenie externdo swoich plików CPP, o czym programiści prawdopodobnie zapomną.

Każde z tych rozwiązań nmzawiera teraz:

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

więc widzimy, że ma tylko mytemplate.okompilację MyTemplate<int>zgodnie z życzeniem, podczas gdy notmain.oi main.onie, ponieważ Uoznacza niezdefiniowany.

Usuń definicje z dołączonych nagłówków, ale także ujawnij szablony zewnętrzny interfejs API w bibliotece tylko z nagłówkami

Jeśli twoja biblioteka nie jest tylko nagłówkiem, plik extern template metoda zadziała, ponieważ użycie projektów będzie po prostu linkować do twojego pliku obiektowego, który będzie zawierał obiekt jawnej instancji szablonu.

Jednak w przypadku bibliotek obsługujących tylko nagłówki, jeśli chcesz:

  • przyspiesz kompilację swojego projektu
  • udostępniać nagłówki jako zewnętrzny interfejs API biblioteki, aby inni mogli z niego korzystać

następnie możesz wypróbować jedną z następujących czynności:

    • mytemplate.hpp: definicja szablonu
    • mytemplate_interface.hpp: deklaracja szablonu pasująca tylko do definicji z mytemplate_interface.hpp, brak definicji
    • mytemplate.cpp: dołącz mytemplate.hppi utwórz jawne instancje
    • main.cppi wszędzie indziej w bazie kodu: dołącz mytemplate_interface.hpp, niemytemplate.hpp
    • mytemplate.hpp: definicja szablonu
    • mytemplate_implementation.hpp: obejmuje mytemplate.hppi dodaje externdo każdej klasy, której wystąpienie zostanie utworzone
    • mytemplate.cpp: dołącz mytemplate.hppi utwórz jawne instancje
    • main.cppi wszędzie indziej w bazie kodu: dołącz mytemplate_implementation.hpp, niemytemplate.hpp

A może nawet lepiej dla wielu nagłówków: utwórz folder intf/ implw swoim includes/folderze i używaj mytemplate.hppjako nazwy zawsze.

mytemplate_interface.hppPodejście wygląda następująco:

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

Skompiluj i uruchom:

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

Wynik:

2

Testowane w Ubuntu 18.04.

C ++ 20 modułów

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

Myślę, że ta funkcja zapewni najlepszą konfigurację w przyszłości, gdy stanie się dostępna, ale jeszcze jej nie sprawdzałem, ponieważ nie jest jeszcze dostępna w moim GCC 9.2.1.

Nadal będziesz musiał wykonać jawną instancję, aby uzyskać przyspieszenie / zapisanie dysku, ale przynajmniej będziemy mieć rozsądne rozwiązanie „Usuń definicje z dołączonych nagłówków, ale także udostępnisz szablony zewnętrzne API”, które nie wymaga kopiowania około 100 razy.

Oczekiwane użycie (bez jawnej insantacji, nie jestem pewien, jaka będzie dokładna składnia, zobacz: Jak używać jawnej instancji szablonu z modułami C ++ 20? )

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

a następnie kompilacja wspomniana na https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

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

Z tego widzimy, że clang może wyodrębnić interfejs szablonu + implementację do magii helloworld.pcm, która musi zawierać pewną pośrednią reprezentację źródła LLVM: Jak obsługiwane są szablony w systemie modułów C ++?co nadal pozwala na specyfikację szablonu.

Jak szybko przeanalizować swoją kompilację, aby sprawdzić, czy wiele zyskałaby na instancji szablonu

Masz więc złożony projekt i chcesz zdecydować, czy tworzenie instancji szablonu przyniesie znaczące korzyści bez faktycznego wykonywania pełnego refaktora?

Poniższa analiza może pomóc Ci zdecydować lub przynajmniej wybrać najbardziej obiecujące obiekty do refaktoryzacji w pierwszej kolejności podczas eksperymentowania, zapożyczając kilka pomysłów z: Mój plik obiektowy C ++ jest za duży

# 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

Marzenie: pamięć podręczna kompilatora szablonów

Myślę, że najlepszym rozwiązaniem byłoby budowanie z:

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

a następnie myfile.oautomatycznie ponownie używał wcześniej skompilowanych szablonów w różnych plikach.

Oznaczałoby to 0 dodatkowego wysiłku dla programistów poza przekazaniem tej dodatkowej opcji CLI do twojego systemu kompilacji.

Dodatkowa premia w postaci jawnej instancji szablonu: pomoc w wyświetlaniu instancji szablonów przez środowiska IDE

Zauważyłem, że niektóre środowiska IDE, takie jak Eclipse, nie potrafią rozpoznać „listy wszystkich używanych wystąpień szablonów”.

Na przykład, jeśli jesteś w kodzie opartym na szablonie i chcesz znaleźć możliwe wartości szablonu, musisz znaleźć konstruktor, który używa jednego po drugim i wydedukować możliwe typy jeden po drugim.

Ale w Eclipse 2020-03 mogę łatwo wyświetlić listę szablonów z jawną instancją, wykonując wyszukiwanie Znajdź wszystkie zastosowania (Ctrl + Alt + G) na nazwie klasy, co wskazuje mi np. Z:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

do:

template class AnimalTemplate<Dog>;

Oto demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Inną techniką guerrila, której możesz użyć poza IDE, byłoby uruchomienie nm -Cna końcowym pliku wykonywalnym i grep z nazwą szablonu:

nm -C main.out | grep AnimalTemplate

co bezpośrednio wskazuje na fakt, że Dogbyła to jedna z instancji:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.