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.
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(); }
źródło
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
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.cpp
inotmain.cpp
niejawnie 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 nm
widzimy, żeW
dochodowe 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 sekcji0000000000000017
: rozmiar wygenerowanego dla nich koduWidzimy to nieco wyraźniej dzięki:
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
_ZN10MyTemplateIiE1fEi
jest zniekształconą nazwą,MyTemplate<int>::f(int)>
którejc++filt
zdecydowano 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 template
hpp 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:
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 template
do 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
extern
do swoich plików CPP, o czym programiści prawdopodobnie zapomną.Każde z tych rozwiązań
nm
zawiera 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.o
kompilacjęMyTemplate<int>
zgodnie z życzeniem, podczas gdynotmain.o
imain.o
nie, ponieważU
oznacza 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:
następnie możesz wypróbować jedną z następujących czynności:
mytemplate.hpp
: definicja szablonumytemplate_interface.hpp
: deklaracja szablonu pasująca tylko do definicji zmytemplate_interface.hpp
, brak definicjimytemplate.cpp
: dołączmytemplate.hpp
i utwórz jawne instancjemain.cpp
i wszędzie indziej w bazie kodu: dołączmytemplate_interface.hpp
, niemytemplate.hpp
mytemplate.hpp
: definicja szablonumytemplate_implementation.hpp
: obejmujemytemplate.hpp
i dodajeextern
do każdej klasy, której wystąpienie zostanie utworzonemytemplate.cpp
: dołączmytemplate.hpp
i utwórz jawne instancjemain.cpp
i wszędzie indziej w bazie kodu: dołączmytemplate_implementation.hpp
, niemytemplate.hpp
A może nawet lepiej dla wielu nagłówków: utwórz folder
intf
/impl
w swoimincludes/
folderze i używajmytemplate.hpp
jako nazwy zawsze.mytemplate_interface.hpp
Podejś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.o
automatycznie 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 -C
na końcowym pliku wykonywalnym i grep z nazwą szablonu:co bezpośrednio wskazuje na fakt, że
Dog
była to jedna z instancji:0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]() 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog) 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
źródło
To zależy od modelu kompilatora - najwyraźniej istnieje model Borlanda i model CFront. A potem zależy to również od twojego zamiaru - jeśli piszesz bibliotekę, możesz (jak wspomniano powyżej) jawnie utworzyć instancję specjalizacji, które chcesz.
Strona GNU c ++ omawia modele tutaj https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .
źródło