Dlaczego szablony mogą być implementowane tylko w pliku nagłówkowym?


1775

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


13
Chociaż prawdą jest, że umieszczenie wszystkich definicji funkcji szablonu w pliku nagłówkowym jest prawdopodobnie najwygodniejszym sposobem ich użycia, nadal nie jest jasne, co „inline” robi w tym cytacie. Nie trzeba do tego używać funkcji wbudowanych. „Inline” nie ma z tym absolutnie nic wspólnego.
ANT

7
Książka jest nieaktualna.
gerardw

1
Szablon nie przypomina funkcji, którą można wkompilować w kod bajtowy. To tylko wzorzec do generowania takiej funkcji. Jeśli umieścisz szablon we własnym pliku * .cpp, nie będziesz musiał nic kompilować. Co więcej, wyraźne instannienie nie jest tak naprawdę szablonem, ale punktem wyjścia do utworzenia funkcji z szablonu, która kończy się w pliku * .obj.
dgrat

5
Czy jestem jedyną osobą, która uważa, że ​​koncepcja szablonu została w C ++ sparaliżowana z tego powodu? ...
DragonGamer

Odpowiedzi:


1556

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:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Podczas czytania tej linii kompilator utworzy nową klasę (nazwijmy ją FooInt), która jest równoważna z następującą:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

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

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

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

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Jeśli moje wyjaśnienie nie jest wystarczająco jasne, możesz zajrzeć do C ++ Super-FAQ na ten temat .


96
W rzeczywistości jawna instancja musi znajdować się w pliku .cpp, który ma dostęp do definicji wszystkich funkcji członkowskich Foo, a nie w nagłówku.
Mankarse

11
„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 one dostępne” Ale dlaczego implementacja jest w plik .cpp jest niedostępny dla kompilatora? Kompilator może również uzyskać dostęp do informacji .cpp, jak inaczej zamieniłby je w pliki .obj? EDYCJA: odpowiedź na to pytanie znajduje się w linku podanym w tej odpowiedzi ...
xcrypt

31
Nie sądzę, żeby to wyjaśniało pytanie, że kluczowa rzecz jest oczywiście związana z kompilacją UNIT, o której nie wspomniano w tym poście
zinking

6
@Gabson: struktury i klasy są równoważne, z tym wyjątkiem, że domyślny modyfikator dostępu do klas jest „prywatny”, podczas gdy jest publiczny dla struktur. Istnieje kilka innych drobnych różnic, których można się nauczyć, patrząc na to pytanie .
Luc Touraille,

3
Na samym początku tej odpowiedzi dodałem zdanie, aby wyjaśnić, że pytanie opiera się na fałszywej przesłance. Jeśli ktoś zapyta „Dlaczego X jest prawdą?” gdy w rzeczywistości X nie jest prawdą, powinniśmy szybko odrzucić to założenie.
Aaron McDaid,

250

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.

template class vector<int>;

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.


51
Ugh. Dobra odpowiedź, ale nie ma naprawdę czystego rozwiązania. Wyszczególnienie wszystkich możliwych typów szablonu wydaje się nie pasować do tego, jaki powinien być szablon.
Jiminion,

6
Może to być dobre w wielu przypadkach, ale ogólnie psuje cel szablonu, który ma na celu umożliwienie korzystania z dowolnej klasy typebez ręcznego wyświetlania ich.
Tomáš Zato - Przywróć Monikę

7
vectornie 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.
UncleZeiv

Używane po zdefiniowaniu szablonu, „i zdefiniowano wszystkie funkcje składowe”. Dzięki !
Vitt Volt

1
Wydaje mi się, że coś mi brakuje… Umieszczam jawną instancję dla dwóch typów w .cpppliku klasy, a do tych dwóch instancji odwołuje się inny .cppplik, i nadal pojawia się błąd linkowania, że ​​członków nie znaleziono.
wiosłowa

250

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:

  • foo.h
    • deklaruje interfejs class MyClass<T>
  • foo.cpp
    • określa wdrożenie class MyClass<T>
  • bar.cpp
    • wykorzystuje 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ści T. 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 identyczne class MyClass_int, class MyClass_floatitp, 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, z Tczym 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ć szablon MyClass<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 szablonu MyClass<T>(tylko jego interfejs w foo.h ), więc nie może go utworzyć.

Jeśli używa samego foo.cppMyClass<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:

  • baz.cpp
    • deklaruje, implementuje class BazPrivatei używaMyClass<BazPrivate>

Nie ma możliwości, aby to działało, chyba że my też

  1. Muszę rekompilować foo.cpp za każdym razem, gdy zmieniamy jakikolwiek inny plik w programie , na wypadek gdyby dodawał nową nową instancjęMyClass<T>
  2. Wymagaj, aby baz.cpp zawierał (ewentualnie przez nagłówek zawiera) pełny szablon 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).


50
podkreślony cytat szablon jest dosłownie szablonem; szablon klasy nie jest klasą, to przepis na utworzenie nowej klasy dla każdego napotkanego T
v.oddou 25.04.16

Chciałbym wiedzieć, czy możliwe jest wykonanie jawnych instancji z innego miejsca niż nagłówek lub plik źródłowy klasy? Na przykład czy robisz to w main.cpp?
gromit190,

1
@Birger Powinieneś być w stanie to zrobić z dowolnego pliku, który ma dostęp do pełnej implementacji szablonu (ponieważ jest w tym samym pliku lub zawiera nagłówek).
Ben

11
@ajeh To nie retoryka. Pytanie brzmi: „dlaczego musisz zaimplementować szablony w nagłówku?”, Więc wyjaśniłem techniczne wybory dokonywane przez język C ++, które prowadzą do tego wymagania. Zanim napisałem swoją odpowiedź, inni już przedstawili obejścia, które nie są pełnymi rozwiązaniami, ponieważ nie może być pełnego rozwiązania. Czułem, że odpowiedzi te zostaną uzupełnione pełniejszą dyskusją na temat „dlaczego” kąta pytania.
Ben

1
wyobraźcie sobie, że w ten sposób ludzie ... jeśli nie korzystaliście z szablonów (do skutecznego kodowania tego, czego potrzebowaliście), i tak oferowalibyście tylko kilka wersji tej klasy. więc masz 3 opcje. 1). nie używaj szablonów. (podobnie jak wszystkie inne klasy / funkcje, nikt nie dba o to, aby inni nie mogli zmienić typów) 2). użyj szablonów i udokumentuj, jakich typów mogą używać. 3). daj im całą implementację (źródło) premii 4). dajcie im całe źródło, na wypadek, gdyby chcieli zrobić szablon z innej klasy;)
Puddle

81

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, zdefiniowana a.cppi użyta w b.cpp. Podczas a.cppkompilacji niekoniecznie wiadomo, że nadchodząca kompilacja b.cppbę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?).


1
„eksport” jest standardem, ale jest trudny do wdrożenia, więc większość zespołów kompilatorów jeszcze tego nie zrobiła.
vava

5
eksport nie eliminuje potrzeby ujawniania źródła, ani nie zmniejsza zależności kompilacji, podczas gdy wymaga ogromnego wysiłku ze strony konstruktorów kompilatorów. Sam Herb Sutter poprosił konstruktorów kompilatorów o „zapomnienie o” eksporcie. Ponieważ potrzebny czas będzie lepiej spędzić gdzie indziej ...
Pieter

2
Więc nie sądzę, że eksport nie jest jeszcze „zaimplementowany”. Prawdopodobnie nigdy nie zrobi tego nikt inny niż EDG po tym, jak inni zobaczyli, ile czasu to zajęło i jak niewiele zostało zdobyte
Pieter

3
Jeśli cię to interesuje, artykuł nazywa się „Dlaczego nie stać nas na eksport”, jest wymieniony na jego blogu ( gotw.ca/publications ), ale nie ma tam pliku pdf (choć szybkie google powinno go włączyć)
Pieter

1
Ok, dzięki za dobry przykład i wyjaśnienie. Oto moje pytanie: dlaczego kompilator nie może ustalić, gdzie wywoływany jest szablon, i najpierw skompilować te pliki przed skompilowaniem pliku definicji? Mogę sobie wyobrazić, że można to zrobić w prostym przypadku ... Czy odpowiedź, że współzależności dość szybko zakłócą porządek?
Vlad

62

Właściwie przed C ++ 11 standard zdefiniował exportsł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ąć exportfunkcję szablonów w C ++ 11.


6
... i kilka lat później w końcu zrozumiałem, co exportwł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.
DevSolar,

4
@DevSolar: ten artykuł jest polityczny, powtarzalny i źle napisany. to nie jest zwykła proza ​​na poziomie standardowym. Niezmiernie długi i nudny, mówiąc w zasadzie 3 razy te same rzeczy na dziesiątkach stron. Ale teraz jestem informowany, że eksport nie jest eksportem. To dobry wywiad!
v.oddou

1
@ v.oddou: Dobry programista i dobry pisarz techniczny to dwa osobne zestawy umiejętności. Niektórzy potrafią robić obie rzeczy, wiele nie. ;-)
DevSolar 25.04.16

@ v.oddou Artykuł jest nie tylko źle napisany, to dezinformacja. Jest to także spin do rzeczywistości: tak naprawdę bardzo mocne argumenty za eksportem są mieszane w taki sposób, aby brzmiały, jakby były przeciwko eksportowi: „odkrywanie licznych dziur w standardzie ODR w obecności eksportu. Przed eksportem kompilator nie musiał diagnozować naruszeń ODR. Teraz jest to konieczne, ponieważ musisz łączyć wewnętrzne struktury danych z różnych jednostek tłumaczeniowych i nie możesz ich łączyć, jeśli faktycznie reprezentują różne rzeczy, więc musisz to sprawdzić ”.
ciekawy,

należy teraz dodać jednostkę tłumaczącą, w której się znajdowało, kiedy to się stało ” Duh. Kiedy jesteś zmuszony używać niepoprawnych argumentów, nie masz żadnych argumentów. Oczywiście w swoich błędach wspominasz o nazwach plików, o co chodzi? To, że ktoś zakochuje się w tym BS, jest oszałamiające. „ Nawet eksperci tacy jak James Kanze mają trudności z zaakceptowaniem tego, że eksport naprawdę taki jest. ” CO? !!!!
ciekawy,

34

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.


Dlaczego nie mogę zaimplementować ich w pliku .cpp ze słowem kluczowym „inline”?
MainID

2
Możesz i nawet nie musisz umieszczać „w linii”. Ale będziesz mógł ich użyć tylko w tym pliku CPP i nigdzie indziej.
vava

10
Jest to prawie najdokładniejsza odpowiedź, z wyjątkiem tego, że „oznacza to, że te kompilatory nie pozwolą na zdefiniowanie ich w plikach innych niż nagłówek, takich jak pliki .cpp”, jest oczywiście fałszywe.
Wyścigi lekkości na orbicie

28

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 .cpppliku, 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 exportsłowem kluczowym była funkcja, która miała być używana do oddzielnej kompilacji. Ta exportfunkcja jest przestarzała, C++11a AFAIK zaimplementował ją tylko jeden kompilator. Nie powinieneś z tego korzystać export. Oddzielna kompilacja nie jest możliwe C++albo C++11ale może w C++17razie 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.


15

Oznacza to, że najbardziej przenośnym sposobem definiowania implementacji metod klas szablonów jest zdefiniowanie ich w definicji klas szablonów.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

15

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:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantivedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantivedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

main.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

W ten sposób należy ponownie skompilować tylko instancje szablonów, a nie wszystkich użytkowników szablonów (i zależności).


1
Podoba mi się to podejście z wyjątkiem MyInstantiatedTemplate.hpliku i MyInstantiatedTemplatetypu dodanego . Jest trochę czystszy, jeśli go nie używasz, imho.
Sprawdź

To trwa najlepiej z dwóch światów. Chciałbym, żeby ta odpowiedź została oceniona wyżej! Zobacz także powyższy link, aby uzyskać nieco czystszą implementację tego samego pomysłu.
Wormer,

8

Aby dodać tutaj coś godnego uwagi. Można zdefiniować metody klasy szablonowej w pliku implementacji, gdy nie są to szablony funkcyjne.


myQueue.hpp:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}

2
Dla prawdziwego mężczyzny? Jeśli to prawda, odpowiedź powinna być sprawdzona jako poprawna. Dlaczego ktoś potrzebuje tych wszystkich hackujących rzeczy voodo, jeśli można po prostu zdefiniować metody nie będące szablonami w .cpp?
Michael IV

Cóż, to nie działa. Przynajmniej na MSVC 2019, uzyskanie nierozwiązanego zewnętrznego symbolu dla funkcji członka klasy szablonu.
Michael IV

Nie mam MSVC 2019 do przetestowania. Jest to dozwolone przez standard C ++. Teraz MSVC jest znane z tego, że nie zawsze przestrzega zasad. Jeśli jeszcze tego nie zrobiłeś, wypróbuj Ustawienia projektu -> C / C ++ -> Język -> Tryb zgodności -> Tak (permissive-).
Nikos

1
Ten dokładny przykład działa, ale nie można dzwonić isEmptyz żadnej innej jednostki tłumaczącej poza myQueue.cpp...
MM

7

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.


2
Ta odpowiedź powinna być jeszcze bardziej zmieniona. „ Niezależnie ” odkryłem to samo podejście i szukałem kogoś, kto już z niego skorzystałby, ponieważ jestem ciekawy, czy jest to oficjalny wzór i czy ma swoją nazwę. Moje podejście polega na wdrożeniu class XBasewszędzie tam, gdzie muszę zaimplementować template class X, umieszczając części zależne od typu Xi całą resztę XBase.
Fabio A.,

6

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.


5
„pliki nagłówkowe NIE są kompilowane” - to naprawdę dziwny sposób na opisanie tego. Pliki nagłówkowe mogą być częścią jednostki tłumaczeniowej, podobnie jak plik „c / cpp”.
Flexo

2
W rzeczywistości jest to prawie przeciwieństwo prawdy, że pliki nagłówkowe są bardzo często kompilowane wiele razy, podczas gdy plik źródłowy jest zwykle kompilowany raz.
xaxxon,

6

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 modyfikujetypename Tuż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ć Tmó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 Tget zostało zastąpione podczas kroku kompilacji, a nie kroku łączenia, więc jeśli spróbuję skompilować szablon bez Tzastą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ć co Tjest

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


4

Sposób oddzielnej implementacji jest następujący.

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

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.


2
s / inner_foo / foo / g i dołącz foo.tpp na końcu foo.h. Jeden plik mniej.

1

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.


-2

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:

template <class T>
T min(T const& one, T const& theOther);

Oraz w Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

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.


-7

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.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Teraz w twoim pliku .template definiujesz swoje funkcje tak, jak normalnie.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }

2
Większość ludzi zdefiniowałaby plik nagłówkowy jako coś, co propaguje definicje do plików źródłowych. Być może zdecydowałeś się użyć rozszerzenia „.template”, ale napisałeś plik nagłówka.
Tommy
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.