Cytat ze standardowej biblioteki C ++: samouczek i podręcznik :
Jedynym obecnie przenośnym sposobem korzystania z szablonów jest ich implementacja w plikach nagłówkowych za pomocą funkcji wbudowanych.
Dlaczego to?
(Wyjaśnienie: pliki nagłówkowe nie są jedynym przenośnym rozwiązaniem. Ale są najwygodniejszym przenośnym rozwiązaniem).
Odpowiedzi:
Zastrzeżenie: Jest to nie trzeba go wstawiać do wdrożenia w pliku nagłówkowym, zobaczyć alternatywne rozwiązanie na końcu tej odpowiedzi.
W każdym razie przyczyną niepowodzenia kodu jest to, że podczas tworzenia szablonu kompilator tworzy nową klasę z podanym argumentem szablonu. Na przykład:
Podczas czytania tej linii kompilator utworzy nową klasę (nazwijmy ją
FooInt
), która jest równoważna z następującą:W związku z tym kompilator musi mieć dostęp do implementacji metod, aby utworzyć ich instancję z argumentem szablonu (w tym przypadku
int
). Gdyby te implementacje nie były w nagłówku, nie byłyby dostępne, a zatem kompilator nie byłby w stanie utworzyć szablonu.Typowym rozwiązaniem tego jest zapisanie deklaracji szablonu w pliku nagłówka, a następnie zaimplementowanie klasy w pliku implementacji (na przykład .tpp) i dołączenie tego pliku implementacji na końcu nagłówka.
Foo.h
Foo.tpp
W ten sposób implementacja jest nadal oddzielona od deklaracji, ale jest dostępna dla kompilatora.
Alternatywne rozwiązanie
Innym rozwiązaniem jest oddzielenie implementacji i jawne tworzenie instancji wszystkich potrzebnych instancji szablonów:
Foo.h
Foo.cpp
Jeśli moje wyjaśnienie nie jest wystarczająco jasne, możesz zajrzeć do C ++ Super-FAQ na ten temat .
źródło
Mnóstwo poprawnych odpowiedzi tutaj, ale chciałem dodać to (dla kompletności):
Jeśli na dole pliku cpp implementacji wykonasz jawną instancję wszystkich typów, z którymi będzie używany szablon, linker będzie mógł je znaleźć jak zwykle.
Edycja: dodanie przykładu jawnej instancji szablonu. Używane po zdefiniowaniu szablonu i zdefiniowaniu wszystkich funkcji składowych.
Spowoduje to utworzenie instancji (a tym samym udostępnienie linkerowi) klasy i wszystkich jej funkcji składowych (tylko). Podobna składnia działa dla funkcji szablonów, więc jeśli masz przeciążenia operatora nie będące członkami, być może będziesz musiał zrobić to samo dla nich.
Powyższy przykład jest dość bezużyteczny, ponieważ wektor jest w pełni zdefiniowany w nagłówkach, z wyjątkiem sytuacji, gdy używany jest wspólny plik dołączania (prekompilowany nagłówek?)
extern template class vector<int>
, Aby nie tworzył instancji we wszystkich innych (1000?) Plikach, które używają wektora.źródło
type
bez ręcznego wyświetlania ich.vector
nie jest dobrym przykładem, ponieważ kontener z natury jest ukierunkowany na „wszystkie” typy. Ale często zdarza się, że tworzysz szablony przeznaczone tylko dla określonego zestawu typów, na przykład typów numerycznych: int8_t, int16_t, int32_t, uint8_t, uint16_t itp. W takim przypadku nadal warto używać szablonu , ale jednoznaczne utworzenie ich dla całego zestawu typów jest również możliwe i moim zdaniem zalecane..cpp
pliku klasy, a do tych dwóch instancji odwołuje się inny.cpp
plik, i nadal pojawia się błąd linkowania, że członków nie znaleziono.Wynika to z wymogu oddzielnej kompilacji i dlatego, że szablony są polimorfizmem w stylu instancji.
Wyjaśnijmy trochę bliżej betonu. Powiedz, że mam następujące pliki:
class MyClass<T>
class MyClass<T>
MyClass<int>
Oddzielna kompilacja oznacza, że powinienem być w stanie skompilować foo.cpp niezależnie od bar.cpp . Kompilator wykonuje całą ciężką pracę analizy, optymalizacji i generowania kodu w każdej jednostce kompilacyjnej całkowicie niezależnie; nie musimy przeprowadzać analizy całego programu. Tylko łącznik musi obsłużyć cały program na raz, a zadanie łącznika jest znacznie łatwiejsze.
bar.cpp nawet nie musi istnieć, kiedy kompiluję foo.cpp , ale nadal powinienem móc połączyć foo.o miałem już razem z bar.o dopiero co stworzyłem, bez konieczności ponownej kompilacji foo .cpp . foo.cpp można nawet skompilować w bibliotekę dynamiczną, rozpowszechniać gdzie indziej bez foo.cpp i połączyć z kodem, który piszą wiele lat po tym, jak napisałem foo.cpp .
„Polimorfizm w stylu tworzenia instancji” oznacza, że szablon
MyClass<T>
nie jest tak naprawdę ogólną klasą, którą można skompilować do kodu, który może działać dla dowolnej wartościT
. To by dodać napowietrznych takich jak boks, potrzebując przekazać wskaźników funkcji do podzielników i konstruktorów itp Intencją Szablony C ++ jest uniknięcie konieczności pisania niemal identyczneclass MyClass_int
,class MyClass_float
itp, ale nadal będzie mógł skończyć ze skompilowanego kodu, który jest przeważnie tak, jakbyśmy już napisany każdej wersji oddzielnie. Szablon jest więc dosłownie szablonem; szablon klasy nie jest klasą, to przepis na tworzenie nowej klasy dla każdego, zT
czym się spotykamy. Szablon nie może zostać skompilowany do kodu, można jedynie skompilować wynik tworzenia instancji szablonu.Kiedy kompiluje się foo.cpp , kompilator nie widzi bar.cpp, aby wiedzieć, że
MyClass<int>
jest potrzebny. Może zobaczyć szablonMyClass<T>
, ale nie może wysłać do niego kodu (to szablon, a nie klasa). A kiedy kompilacja bar.cpp , kompilator może zobaczyć, że musi utworzyćMyClass<int>
, ale nie widzi szablonuMyClass<T>
(tylko jego interfejs w foo.h ), więc nie może go utworzyć.Jeśli używa samego foo.cpp
MyClass<int>
, kod tego zostanie wygenerowany podczas kompilacji foo.cpp , więc kiedy bar.o jest połączony z foo.o , można je podłączyć i zadziała. Możemy wykorzystać ten fakt, aby umożliwić skończony zestaw instancji szablonów do zaimplementowania w pliku .cpp poprzez napisanie jednego szablonu. Ale bar.cpp nie ma możliwości użycia szablonu jako szablonu i utworzenia go na dowolnych typach; może korzystać tylko z wcześniej istniejących wersji klasy szablonowej, które zapewnił autor foo.cpp .Możesz pomyśleć, że kompilując szablon, kompilator powinien „wygenerować wszystkie wersje”, przy czym te, które nigdy nie są używane, są odfiltrowywane podczas łączenia. Oprócz ogromnych kosztów ogólnych i ekstremalnych trudności takie podejście musiałoby się spotkać, ponieważ funkcje „modyfikatora typu”, takie jak wskaźniki i tablice, pozwalają nawet tylko wbudowanym typom wywoływać nieskończoną liczbę typów, co dzieje się, gdy teraz rozszerzam swój program poprzez dodanie:
class BazPrivate
i używaMyClass<BazPrivate>
Nie ma możliwości, aby to działało, chyba że my też
MyClass<T>
MyClass<T>
, aby kompilator mógł generowaćMyClass<BazPrivate>
podczas kompilacji baz.cpp .Nikt nie lubi (1), ponieważ systemy kompilacji analizy całych programów zajmują wieczność do kompilacji, a także dlatego, że nie można rozpowszechniać skompilowanych bibliotek bez kodu źródłowego. Zamiast tego mamy (2).
źródło
Szablony muszą być tworzone wystąpienia przez kompilator, zanim faktycznie je do kompilacji kodu wynikowego. Ta instancja może zostać osiągnięta tylko wtedy, gdy znane są argumenty szablonu. Teraz wyobraź sobie scenariusz, w którym funkcja szablonu jest zadeklarowana
a.h
, zdefiniowanaa.cpp
i użyta wb.cpp
. Podczasa.cpp
kompilacji niekoniecznie wiadomo, że nadchodząca kompilacjab.cpp
będzie wymagała wystąpienia szablonu, nie mówiąc już o tym, które konkretne wystąpienie byłoby. W przypadku większej liczby plików nagłówkowych i źródłowych sytuacja może się szybko skomplikować.Można argumentować, że kompilatory można uczynić mądrzejszymi, aby „patrzyły w przyszłość” na wszystkie zastosowania szablonu, ale jestem pewien, że nie byłoby trudno stworzyć rekurencyjnych lub w inny sposób skomplikowanych scenariuszy. AFAIK, kompilatory nie patrzą w przyszłość. Jak zauważył Anton, niektóre kompilatory obsługują jawne deklaracje eksportowe instancji szablonów, ale nie wszystkie kompilatory obsługują (jeszcze?).
źródło
Właściwie przed C ++ 11 standard zdefiniował
export
słowo kluczowe, które umożliwiłoby zadeklarowanie szablonów w pliku nagłówkowym i zaimplementowanie ich w innym miejscu.Żaden z popularnych kompilatorów nie zaimplementował tego słowa kluczowego. Jedyne, o czym wiem, to nakładka napisana przez Edison Design Group, która jest używana przez kompilator Comeau C ++. Wszystkie pozostałe wymagały pisania szablonów w plikach nagłówków, ponieważ kompilator potrzebuje definicji szablonu do poprawnego tworzenia instancji (jak już zauważyli inni).
W rezultacie komitet standardowy ISO C ++ postanowił usunąć
export
funkcję szablonów w C ++ 11.źródło
export
właściwie by nam dało , a co nie ... i teraz całym sercem zgadzam się z ludźmi EDG: Nie przyniosłoby nam tego, co większość ludzi (ja w '11 uwzględnione) myślę, że tak, a standard C ++ lepiej bez niego.Chociaż standardowe C ++ nie ma takich wymagań, niektóre kompilatory wymagają udostępnienia wszystkich szablonów funkcji i klas w każdej używanej jednostce tłumaczeniowej. W efekcie dla tych kompilatorów treści funkcji szablonów muszą zostać udostępnione w pliku nagłówkowym. Powtarzam: oznacza to, że te kompilatory nie pozwolą na ich zdefiniowanie w plikach innych niż nagłówek, takich jak pliki .cpp
Istnieje słowo kluczowe eksportu, które ma złagodzić ten problem, ale nie jest wcale bliskie przenośności.
źródło
Szablony muszą być używane w nagłówkach, ponieważ kompilator musi tworzyć instancje różnych wersji kodu, w zależności od parametrów podanych / wydedukowanych dla parametrów szablonu. Pamiętaj, że szablon nie reprezentuje kodu bezpośrednio, ale szablon dla kilku wersji tego kodu. Kiedy kompilujesz funkcję niebędącą szablonem w
.cpp
pliku, kompilujesz konkretną funkcję / klasę. Nie dotyczy to szablonów, które mogą być tworzone z różnymi typami, a mianowicie konkretny kod musi być emitowany podczas zastępowania parametrów szablonu typami konkretnymi.Ze
export
słowem kluczowym była funkcja, która miała być używana do oddzielnej kompilacji. Taexport
funkcja jest przestarzała,C++11
a AFAIK zaimplementował ją tylko jeden kompilator. Nie powinieneś z tego korzystaćexport
. Oddzielna kompilacja nie jest możliweC++
alboC++11
ale może wC++17
razie koncepcja uczynić ją, możemy mieć jakiś sposób oddzielnej kompilacji.Aby osiągnąć osobną kompilację, musi być możliwe osobne sprawdzanie treści szablonu. Wydaje się, że możliwe jest rozwiązanie za pomocą koncepcji. Spójrz na ten artykuł niedawno zaprezentowany na spotkaniu komitetu normalizacyjnego. Myślę, że nie jest to jedyny wymóg, ponieważ nadal musisz utworzyć instancję kodu dla kodu szablonu w kodzie użytkownika.
Osobny problem kompilacji szablonów Myślę, że jest to również problem związany z migracją do modułów, która jest obecnie w pracy.
źródło
Oznacza to, że najbardziej przenośnym sposobem definiowania implementacji metod klas szablonów jest zdefiniowanie ich w definicji klas szablonów.
źródło
Mimo że istnieje wiele dobrych wyjaśnień powyżej, brakuje mi praktycznego sposobu na rozdzielenie szablonów na nagłówek i treść.
Moim głównym zmartwieniem jest unikanie ponownej kompilacji wszystkich użytkowników szablonów, kiedy zmieniam ich definicję.
Posiadanie wszystkich instancji szablonów w treści szablonu nie jest dla mnie realnym rozwiązaniem, ponieważ autor szablonu może nie wiedzieć wszystkiego, jeśli jego użycie, a użytkownik szablonu może nie mieć prawa do jego modyfikacji.
Przyjąłem następujące podejście, które działa również dla starszych kompilatorów (gcc 4.3.4, aCC A.03.13).
Dla każdego użycia szablonu istnieje typedef we własnym pliku nagłówkowym (generowanym z modelu UML). Jego ciało zawiera instancję (która kończy się w bibliotece, do której prowadzi link na końcu).
Każdy użytkownik szablonu dołącza ten plik nagłówka i używa typedef.
Schematyczny przykład:
MyTemplate.h:
MyTemplate.cpp:
MyInstantivedTemplate.h:
MyInstantivedTemplate.cpp:
main.cpp:
W ten sposób należy ponownie skompilować tylko instancje szablonów, a nie wszystkich użytkowników szablonów (i zależności).
źródło
MyInstantiatedTemplate.h
pliku iMyInstantiatedTemplate
typu dodanego . Jest trochę czystszy, jeśli go nie używasz, imho.Aby dodać tutaj coś godnego uwagi. Można zdefiniować metody klasy szablonowej w pliku implementacji, gdy nie są to szablony funkcyjne.
myQueue.hpp:
myQueue.cpp:
źródło
isEmpty
z żadnej innej jednostki tłumaczącej pozamyQueue.cpp
...Jeśli problemem jest dodatkowy czas kompilacji i rozdęty rozmiar binarny powstały w wyniku kompilacji .h jako części wszystkich używających go modułów .cpp, w wielu przypadkach możesz zrobić, aby klasa szablonów spadła z nie-szablonowej klasy bazowej dla niezależne od typu części interfejsu, a ta klasa podstawowa może mieć swoją implementację w pliku .cpp.
źródło
class XBase
wszędzie tam, gdzie muszę zaimplementowaćtemplate class X
, umieszczając części zależne od typuX
i całą resztęXBase
.Jest to dokładnie poprawne, ponieważ kompilator musi wiedzieć, jaki jest typ alokacji. Tak więc klasy szablonów, funkcje, wyliczenia itp. Muszą zostać zaimplementowane również w pliku nagłówkowym, jeśli mają być upublicznione lub stanowić część biblioteki (statycznej lub dynamicznej), ponieważ pliki nagłówkowe NIE są kompilowane w przeciwieństwie do plików c / cpp, które są. Jeśli kompilator nie wie, typ nie może go skompilować. W .Net może, ponieważ wszystkie obiekty pochodzą z klasy Object. To nie jest .Net.
źródło
Kompilator wygeneruje kod dla każdej instancji szablonu, gdy użyjesz szablonu podczas kroku kompilacji. W procesie kompilacji i łączenia pliki .cpp są konwertowane na czysty obiekt lub kod maszynowy, który zawiera odniesienia lub niezdefiniowane symbole, ponieważ pliki .h zawarte w pliku main.cpp nie mają jeszcze implementacji YET. Są one gotowe do połączenia z innym plikiem obiektowym, który definiuje implementację twojego szablonu, dzięki czemu masz pełny plik wykonywalny a.out.
Ponieważ jednak szablony muszą być przetwarzane na etapie kompilacji w celu wygenerowania kodu dla każdej zdefiniowanej instancji szablonu, więc po prostu kompilacja szablonu osobnego od pliku nagłówkowego nie będzie działać, ponieważ zawsze idą w parze, z tego samego powodu. że każda instancja szablonu jest dosłownie nową klasą. W zwykłej klasie możesz rozdzielić .h i .cpp, ponieważ .h jest schematem tej klasy, a .cpp jest surową implementacją, więc wszelkie pliki implementacji można regularnie kompilować i łączyć, jednak przy użyciu szablonów .h jest schematem tego, jak klasa nie powinna wyglądać tak, jak powinien wyglądać obiekt, co oznacza, że szablon .cpp nie jest zwykłą zwykłą implementacją klasy, to po prostu schemat klasy, więc każda implementacja pliku szablonu .h może „
Dlatego szablony nigdy nie są kompilowane osobno i są kompilowane tylko tam, gdzie masz konkretną instancję w innym pliku źródłowym. Jednak konkretna instancja musi znać implementację pliku szablonu, ponieważ po prostu modyfikuje
typename T
użycie konkretnego typu w pliku .h nie wykona zadania, ponieważ to, co .cpp jest tam do połączenia, nie mogę go później znaleźć, ponieważ pamiętam, że szablony są abstrakcyjne i nie można ich skompilować, więc jestem zmuszony aby przekazać implementację w tej chwili, więc wiem, co skompilować i połączyć, a teraz, gdy mam implementację, zostaje ona dołączona do załączonego pliku źródłowego. Zasadniczo w chwili tworzenia szablonu muszę utworzyć zupełnie nową klasę i nie mogę tego zrobić, jeśli nie wiem, jak ta klasa powinna wyglądać, gdy używam typu, który udostępniam, chyba że powiadomię kompilator implementacja szablonu, więc teraz kompilator może zastąpićT
mój typ i stworzyć konkretną klasę, która jest gotowa do kompilacji i połączenia.Podsumowując, szablony są schematami tego, jak powinny wyglądać klasy, klasy są schematami tego, jak powinien wyglądać obiekt. Nie mogę kompilować szablonów oddzielnie od ich konkretnej instancji, ponieważ kompilator kompiluje tylko konkretne typy, innymi słowy szablony przynajmniej w C ++ są czystą abstrakcją językową. Musimy de-abstrakcyjne szablony, że tak powiem, i robimy to, dając im konkretny typ, z którym możemy sobie poradzić, aby nasza abstrakcja szablonu mogła przekształcić się w zwykły plik klasy, a z kolei można go skompilować normalnie. Oddzielenie szablonu .h pliku od szablonu .cpp nie ma znaczenia. Jest to nonsensowne, ponieważ oddzielenie .cpp i .h występuje tylko wtedy, gdy .cpp można skompilować osobno i połączyć indywidualnie z szablonami, ponieważ nie możemy ich osobno skompilować, ponieważ szablony są abstrakcją,
Znaczenie
typename T
get zostało zastąpione podczas kroku kompilacji, a nie kroku łączenia, więc jeśli spróbuję skompilować szablon bezT
zastąpienia go konkretnym typem wartości, który jest całkowicie bez znaczenia dla kompilatora, w wyniku czego nie można utworzyć kodu obiektowego, ponieważ nie wiedzieć coT
jestTechnicznie możliwe jest stworzenie pewnego rodzaju funkcji, która zapisze plik template.cpp i zmieni typy, gdy znajdzie je w innych źródłach, myślę, że standard ma słowo kluczowe
export
, które pozwoli ci umieścić szablony w osobnym plik cpp, ale nie tak wiele kompilatorów faktycznie to implementuje.Na marginesie, przy tworzeniu specjalizacji dla klasy szablonu, możesz oddzielić nagłówek od implementacji, ponieważ specjalizacja z definicji oznacza, że specjalizuję się w konkretnym typie, który można kompilować i łączyć indywidualnie.
źródło
Sposób oddzielnej implementacji jest następujący.
inner_foo ma deklaracje przekazywania. foo.tpp ma implementację i zawiera inner_foo.h; i foo.h będzie miał tylko jedną linię, zawierającą foo.tpp.
W czasie kompilacji zawartość foo.h jest kopiowana do foo.tpp, a następnie cały plik jest kopiowany do foo.h, po czym kompiluje się. W ten sposób nie ma ograniczeń, a nazewnictwo jest spójne w zamian za jeden dodatkowy plik.
Robię to, ponieważ statyczne analizatory kodu łamią się, gdy nie widzi deklaracji forward klasy w * .tpp. Jest to denerwujące podczas pisania kodu w dowolnym środowisku IDE lub przy użyciu YouCompleteMe lub innych.
źródło
Proponuję spojrzeć na tę stronę gcc, która omawia kompromisy między modelem „front” i „borland” dla instancji szablonów.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
Model „borland” odpowiada temu, co sugeruje autor, zapewniając pełną definicję szablonu i kompilując wiele razy.
Zawiera wyraźne zalecenia dotyczące ręcznego i automatycznego tworzenia szablonów. Na przykład opcji „-repo” można używać do zbierania szablonów, które należy utworzyć. Inną opcją jest wyłączenie automatycznych instancji szablonów za pomocą „-fno-implicit-templates”, aby wymusić ręczne tworzenie instancji szablonów.
Z mojego doświadczenia korzystam, że szablony biblioteki standardowej C ++ i Boost są tworzone dla każdej jednostki kompilacji (przy użyciu biblioteki szablonów). W przypadku dużych klas szablonów wykonuję ręczne tworzenie szablonów raz, dla potrzebnych typów.
Takie jest moje podejście, ponieważ zapewniam działający program, a nie bibliotekę szablonów do użytku w innych programach. Autor książki, Josuttis, dużo pracuje nad bibliotekami szablonów.
Gdybym naprawdę martwił się szybkością, przypuszczam, że zbadałbym, używając Prekompilowanych Nagłówków https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
który zyskuje wsparcie w wielu kompilatorach. Myślę jednak, że prekompilowane nagłówki byłyby trudne w przypadku plików nagłówków szablonów.
źródło
Innym powodem, dla którego dobrym pomysłem jest pisanie zarówno deklaracji, jak i definicji w plikach nagłówkowych, jest ich czytelność. Załóżmy, że istnieje taka funkcja szablonu w Utility.h:
Oraz w Utility.cpp:
Wymaga to, aby każda klasa T tutaj implementowała mniej niż operator (<). Zgłasza błąd kompilatora podczas porównywania dwóch instancji klasy, które nie zaimplementowały „<”.
Dlatego jeśli oddzielisz deklarację i definicję szablonu, nie będziesz w stanie odczytać pliku nagłówka, aby zobaczyć tajniki tego szablonu w celu korzystania z tego interfejsu API we własnych klasach, chociaż kompilator powie ci w tym przypadek, który operator musi zostać pominięty.
źródło
Możesz zdefiniować klasę szablonu w pliku .template zamiast w pliku .cpp. Ktokolwiek mówi, że możesz go zdefiniować tylko w pliku nagłówkowym, jest zły. Jest to coś, co działa aż do c ++ 98.
Nie zapomnij, aby Twój kompilator traktował plik .template jako plik c ++, aby zachować inteligencję.
Oto przykład tego dla dynamicznej klasy tablicowej.
Teraz w twoim pliku .template definiujesz swoje funkcje tak, jak normalnie.
źródło