Przechowywanie definicji funkcji szablonu C ++ w pliku .CPP

526

Mam kod szablonu, który wolałbym przechowywać w pliku CPP zamiast wbudowanego w nagłówku. Wiem, że można to zrobić, o ile wiesz, jakie typy szablonów będą używane. Na przykład:

plik .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

plik .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Zwróć uwagę na ostatnie dwa wiersze - funkcja szablonu foo :: do jest używana tylko z ints i std :: strings, więc te definicje oznaczają, że aplikacja się połączy.

Moje pytanie brzmi - czy to paskudny hack, czy będzie to działać z innymi kompilatorami / linkerami? Obecnie używam tego kodu tylko z VS2008, ale będę chciał przenieść się do innych środowisk.

Obrabować
źródło
22
Nie miałem pojęcia, że ​​to możliwe - ciekawa sztuczka! To pomogłoby niektórym ostatnim zadaniom znacznie to wiedzieć - na zdrowie!
xan
69
Rzeczą, która mnie tępi, jest użycie dojako identyfikatora: p
Quentin
zrobiłem somerhing podobnie z gcc, ale nadal badam
Nick
16
To nie jest „hack”, to deklinacja do przodu. Ma to miejsce w standardzie języka; więc tak, jest to dozwolone w każdym standardowym kompilatorze zgodnym.
Ahmet Ipkin
1
Co jeśli masz dziesiątki metod? Czy możesz to zrobić template class foo<int>;template class foo<std::string>;na końcu pliku .cpp?
Ignorant

Odpowiedzi:

231

Opisany problem można rozwiązać, definiując szablon w nagłówku lub stosując podejście opisane powyżej.

Polecam przeczytanie następujących punktów z C ++ FAQ Lite :

Zagłębiają się w wiele szczegółów na temat tych (i innych) problemów z szablonami.

Aaron N. Tubbs
źródło
39
Aby uzupełnić odpowiedź, odnośnik do linku pozytywnie odpowiada na pytanie, tzn. Można zrobić to, co zasugerował Rob i mieć kod, który będzie przenośny.
ivotron
160
Czy możesz po prostu zamieścić odpowiednie części w samej odpowiedzi? Dlaczego takie odwołania są nawet dozwolone w SO. Nie mam pojęcia, czego szukać w tym linku, ponieważ od tego czasu został on mocno zmieniony.
Ident
124

Dla innych na tej stronie, którzy zastanawiają się, jaka jest poprawna składnia (podobnie jak ja) w przypadku jawnej specjalizacji szablonów (lub przynajmniej w VS2008), jej następująca ...

W twoim pliku .h ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

I w twoim pliku .cpp

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
sid przestrzeni nazw
źródło
15
Czy masz na myśli „dla wyraźnej specjalizacji szablonu CLASS”? Czy w takim przypadku będzie to obejmować każdą funkcję, jaką ma klasa wzorcowa?
Arthur
@Arthur wydaje się nie, mam kilka metod szablonów pozostających w nagłówku i większość innych metod w cpp, działa dobrze. Bardzo fajne rozwiązanie.
user1633272,
W przypadku pytającego mają szablon funkcji, a nie szablon klasy.
user253751
23

Ten kod jest dobrze sformułowany. Trzeba tylko zwrócić uwagę, że definicja szablonu jest widoczna w momencie tworzenia wystąpienia. Cytując standard, § 14.7.2.4:

Definicja nieeksportowanego szablonu funkcji, nieeksportowanego szablonu funkcji składowej lub nieeksportowanej funkcji składowej lub elementu danych statycznych szablonu klasy musi być obecna w każdej jednostce tłumaczeniowej, w której jest wyraźnie utworzona.

Konrad Rudolph
źródło
2
Co oznacza brak eksportu ?
Dan Nissenbaum
1
@ Dan Widoczny tylko wewnątrz jednostki kompilacyjnej, a nie poza nią. Jeśli połączysz ze sobą wiele jednostek kompilacji, wyeksportowane symbole mogą być używane na nich (i muszą mieć jedną, lub przynajmniej, w przypadku szablonów, spójne definicje, w przeciwnym razie napotkasz UB).
Konrad Rudolph
Dzięki. Myślałem, że wszystkie funkcje są (domyślnie) widoczne poza jednostką kompilacji. Jeśli mam dwie jednostki kompilacji a.cpp(definiujące funkcję a() {}) i b.cpp(definiujące funkcję b() { a() }), to zostanie to pomyślnie połączone. Jeśli mam rację, to powyższy cytat wydaje się nie mieć zastosowania do typowego przypadku ... czy gdzieś się mylę?
Dan Nissenbaum
@ Dan Trywialny kontrprzykład: inlinefunkcje
Konrad Rudolph
1
@Dan Funkcje szablonów są niejawne inline. Powodem jest to, że bez znormalizowanego ABI w C ++ trudno / nie można zdefiniować efektu, który w przeciwnym razie miałby.
Konrad Rudolph
15

Powinno to działać dobrze wszędzie tam, gdzie obsługiwane są szablony. Jawna instancja szablonu jest częścią standardu C ++.

księżycowy cień
źródło
13

Twój przykład jest poprawny, ale niezbyt przenośny. Istnieje również nieco czystsza składnia, której można użyć (jak wskazał @ namespace-sid).

Załóżmy, że klasa szablonowa jest częścią biblioteki, która ma być udostępniana. Czy należy kompilować inne wersje klasy szablonowej? Czy opiekun biblioteki powinien przewidywać wszystkie możliwe zastosowania klasy w szablonie?

Alternatywne podejście to niewielka odmiana tego, co masz: dodaj trzeci plik, który jest plikiem implementacji / tworzenia szablonu.

plik foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

plik foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

plik foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Jedynym zastrzeżeniem jest to, że musisz powiedzieć kompilatorowi, aby skompilował, foo-impl.cppzamiast foo.cppkompilowania tego drugiego.

Oczywiście możesz mieć wiele implementacji w trzecim pliku lub mieć wiele plików implementacji dla każdego typu, którego chcesz użyć.

Umożliwia to znacznie większą elastyczność podczas udostępniania klasy wzorcowej do innych zastosowań.

Ta konfiguracja skraca również czas kompilacji ponownie używanych klas, ponieważ nie rekompilujesz tego samego pliku nagłówka w każdej jednostce tłumaczeniowej.

Cameron Tacklind
źródło
co to kupuje Nadal musisz edytować foo-impl.cpp, aby dodać nową specjalizację.
MK.
Oddzielenie szczegółów implementacji (aka definicji w foo.cpp), z których wersje są faktycznie kompilowane (in foo-impl.cpp) i deklaracji (in foo.h). Nie podoba mi się, że większość szablonów C ++ jest całkowicie zdefiniowanych w plikach nagłówkowych. Jest to sprzeczne ze standardem par C / C ++ c[pp]/hdla każdej klasy / przestrzeni nazw / jakiejkolwiek używanej grupy. Wydaje się, że ludzie nadal używają monolitycznych plików nagłówkowych tylko dlatego, że ta alternatywa nie jest powszechnie używana ani znana.
Cameron Tacklind
1
@MK. Najpierw umieszczałem wyraźne instancje szablonów na końcu definicji w pliku źródłowym, dopóki nie potrzebowałem dalszych instancji w innym miejscu (np. Testy jednostkowe z użyciem makiety jako typu szablonowego). Ta separacja pozwala mi dodawać więcej instancji na zewnątrz. Co więcej, nadal działa, gdy zachowuję oryginał jako h/cppparę, chociaż musiałem otoczyć oryginalną listę instancji w osłonie dołączania, ale nadal mogłem skompilować foo.cppjak zwykle. Nadal jestem całkiem nowy w C ++ i chciałbym wiedzieć, czy to mieszane użycie ma jakieś dodatkowe zastrzeżenie.
Thirdwater
3
Myślę, że lepiej jest oddzielić foo.cppi foo-impl.cpp. Nie #include "foo.cpp"w foo-impl.cpppliku; zamiast tego dodaj deklarację, extern template class foo<int>;aby foo.cppuniemożliwić kompilatorowi tworzenie instancji szablonu podczas kompilacji foo.cpp. Upewnij się, że system kompilacji buduje oba .cpppliki i przekazuje oba pliki obiektowe do konsolidatora. Ma to wiele zalet: a) jest jasne, foo.cppże nie ma instancji; b) zmiany w foo.cpp nie wymagają ponownej kompilacji foo-impl.cpp.
Shmuel Levine
3
Jest to bardzo dobre podejście do problemu definicji szablonów, które najlepiej wykorzystuje oba światy - implementację nagłówka i tworzenie instancji dla często używanych typów. Jedyną zmianą, jakiej dokonałbym w tej konfiguracji, jest zmiana nazwy foo.cppna foo_impl.hi foo-impl.cppna just foo.cpp. Dodałbym również typedefs dla instancji od foo.cppdo foo.h, podobnie using foo_int = foo<int>;. Sztuką jest zapewnienie użytkownikom dwóch interfejsów nagłówka do wyboru. Kiedy użytkownik potrzebuje wcześniej zdefiniowanej instancji, włącza foo.h, gdy użytkownik potrzebuje czegoś nie w porządku, to obejmuje foo_impl.h.
Wormer,
5

To zdecydowanie nie jest nieprzyjemny hack, ale pamiętaj, że będziesz musiał to zrobić (wyraźna specjalizacja szablonu) dla każdej klasy / typu, którego chcesz użyć z danym szablonem. W przypadku WIELU typów żądających utworzenia szablonu może być WIELE plików w pliku .cpp. Aby rozwiązać ten problem, możesz mieć TemplateClassInst.cpp w każdym używanym projekcie, aby mieć większą kontrolę nad typami, które będą tworzone. Oczywiście to rozwiązanie nie będzie idealne (inaczej srebrna kula), ponieważ może to doprowadzić do złamania ODR :).

Czerwony XIII
źródło
Czy jesteś pewien, że to złamie ODR? Jeśli linie instancji w TemplateClassInst.cpp odnoszą się do identycznego pliku źródłowego (zawierającego definicje funkcji szablonu), czy nie jest to gwarantowane, że nie narusza ODR, ponieważ wszystkie definicje są identyczne (nawet jeśli się powtarzają)?
Dan Nissenbaum
Proszę, co to jest ODR?
nieusuwalny
4

W najnowszym standardzie istnieje słowo kluczowe ( export), które pomogłoby złagodzić ten problem, ale nie jest ono zaimplementowane w żadnym znanym mi kompilatorze innym niż Comeau.

Zobacz najczęściej zadawane pytania na ten temat.

Ben Collins
źródło
2
AFAIK, eksport jest martwy, ponieważ napotykają coraz to nowe problemy, za każdym razem, gdy rozwiązują ostatnie, co sprawia, że ​​ogólne rozwiązanie jest coraz bardziej skomplikowane. A słowo kluczowe „eksport” i tak nie pozwoli ci „eksportować” z CPP (w każdym razie nadal z H. Suttera). Mówię więc: nie wstrzymuj oddechu ...
paercebal
2
Aby wdrożyć eksport, kompilator nadal wymaga pełnej definicji szablonu. Wszystko, co zyskujesz, to mieć w pewnym sensie skompilowaną formę. Ale tak naprawdę nie ma sensu.
Zan Lynx,
2
... i odszedł od standardu z powodu nadmiernej komplikacji dla minimalnego wzmocnienia.
DevSolar
4

Jest to standardowy sposób definiowania funkcji szablonów. Sądzę, że czytam trzy metody definiowania szablonów. Lub prawdopodobnie 4. Każdy z zalet i wad.

  1. Zdefiniuj w definicji klasy. W ogóle mi się to nie podoba, ponieważ uważam, że definicje klas mają charakter wyłącznie informacyjny i powinny być łatwe do odczytania. Jednak znacznie trudniej jest zdefiniować szablony w klasie niż na zewnątrz. I nie wszystkie deklaracje szablonów są na tym samym poziomie złożoności. Ta metoda sprawia również, że szablon jest prawdziwym szablonem.

  2. Zdefiniuj szablon w tym samym nagłówku, ale poza klasą. Jest to mój preferowany sposób przez większość czasu. Utrzymuje porządek w definicji klasy, szablon pozostaje prawdziwym szablonem. Wymaga to jednak pełnego nazewnictwa szablonów, co może być trudne. Ponadto twój kod jest dostępny dla wszystkich. Ale jeśli potrzebujesz wbudowanego kodu, jest to jedyny sposób. Możesz to również osiągnąć, tworząc plik .INL na końcu definicji klas.

  3. Uwzględnij nagłówek.h oraz implementację.CPP w pliku main.CPP. Myślę, że tak to się robi. Nie będziesz musiał przygotowywać żadnych wcześniejszych instancji, zachowa się jak prawdziwy szablon. Problem, który mam z tym, to, że nie jest to naturalne. Zwykle nie dołączamy plików źródłowych i nie oczekujemy ich. Sądzę, że odkąd załączyłeś plik źródłowy, funkcje szablonu można wstawiać.

  4. Ta ostatnia metoda, która była opublikowana, polega na definiowaniu szablonów w pliku źródłowym, podobnie jak numer 3; ale zamiast dołączać plik źródłowy, tworzymy szablony według potrzebnych nam szablonów. Nie mam problemu z tą metodą i czasem się przydaje. Mamy jeden duży kod, nie można czerpać korzyści z wstawiania go, więc po prostu umieść go w pliku CPP. A jeśli znamy typowe instancje i możemy je wstępnie zdefiniować. To oszczędza nam pisania w zasadzie 5, 10 razy tego samego. Ta metoda ma tę zaletę, że nasz kod jest zastrzeżony. Ale nie polecam umieszczania małych, regularnie używanych funkcji w plikach CPP. Zmniejszy to wydajność Twojej biblioteki.

Uwaga: nie jestem świadomy konsekwencji rozdętego pliku obj.

Cássio Renan
źródło
3

Tak, to standardowy sposób specjalizacji jawnej instancji . Jak już wspomniano, nie można utworzyć tego szablonu z innymi typami.

Edycja: poprawione na podstawie komentarza.

Lou Franco
źródło
Bycie wybrednym pod względem terminologicznym to „wyraźna instancja”.
Richard Corden,
2

Weźmy jeden przykład, powiedzmy z jakiegoś powodu, że chcesz mieć klasę szablonów:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Jeśli skompilujesz ten kod za pomocą Visual Studio - działa on od razu po rozpakowaniu. gcc wyświetli błąd linkera (jeśli ten sam plik nagłówkowy jest używany z wielu plików .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Możliwe jest przeniesienie implementacji do pliku .cpp, ale wtedy musisz zadeklarować klasę w ten sposób -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

A potem .cpp będzie wyglądać tak:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Bez dwóch ostatnich wierszy w pliku nagłówkowym - gcc będzie działało dobrze, ale Visual Studio wyświetli błąd:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

składnia klasy szablonów jest opcjonalna, jeśli chcesz udostępnić funkcję poprzez eksport .dll, ale dotyczy to tylko platformy Windows - test_template.h mógłby wyglądać następująco:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

z plikiem .cpp z poprzedniego przykładu.

To jednak powoduje więcej bólu głowy dla linkera, więc zaleca się użycie poprzedniego przykładu, jeśli nie eksportujesz funkcji .dll.

TarmoPikaro
źródło
1

Czas na aktualizację! Utwórz plik wbudowany (.inl lub prawdopodobnie dowolny inny) i po prostu skopiuj w nim wszystkie swoje definicje. Pamiętaj, aby dodać szablon nad każdą funkcją ( template <typename T, ...>). Teraz zamiast dołączać plik nagłówkowy do pliku wbudowanego, robi się odwrotnie. Dołącz plik śródliniowy po deklaracji swojej klasy ( #include "file.inl").

Naprawdę nie wiem, dlaczego nikt o tym nie wspominał. Nie widzę żadnych bezpośrednich wad.

Didii
źródło
25
Bezpośrednią wadą jest to, że jest to zasadniczo to samo, co zdefiniowanie funkcji szablonu bezpośrednio w nagłówku. Gdy to zrobisz #include "file.inl", preprocesor wklei zawartość file.inlbezpośrednio do nagłówka. Bez względu na powód, dla którego chcesz uniknąć implementacji w nagłówku, to rozwiązanie nie rozwiązuje tego problemu.
Cody Gray
5
- i oznacza, że ​​technicznie niepotrzebnie obciążasz się pisaniem wszystkich pełnych wigoru myśli, które są potrzebne poza templatedefinicjami. Rozumiem, dlaczego ludzie chcą to zrobić - aby osiągnąć jak największą parzystość z deklaracjami / definicjami niebędącymi szablonami, aby utrzymać porządek w deklaracji interfejsu itp. - ale nie zawsze jest to warte kłopotu. Chodzi o ocenę kompromisów po obu stronach i wybranie najmniej złego . ... dopóki nie stanie namespace classsię rzeczą: O [ proszę bądź rzeczą ]
podkreślenie_d
2
@Andrew Wygląda na to, że utknął w fajkach Komitetu, chociaż myślę, że widziałem kogoś, kto mówił, że nie było to zamierzone. Chciałbym, żeby udało się to zrobić w C ++ 17. Może w następnej dekadzie.
underscore_d
@CodyGray: Technicznie rzecz biorąc, jest to w istocie to samo dla kompilatora, a zatem nie skraca czasu kompilacji. Nadal uważam, że warto o tym wspomnieć i ćwiczyć w wielu projektach, które widziałem. Zejście tą drogą pomaga oddzielić interfejs od definicji, co jest dobrą praktyką. W tym przypadku nie pomaga w kompatybilności z ABI itp., Ale ułatwia czytanie i rozumienie interfejsu.
kiloalphaindia
0

W podanym przykładzie nie ma nic złego. Ale muszę powiedzieć, że uważam, że przechowywanie definicji funkcji w pliku CPP nie jest efektywne. Rozumiem tylko potrzebę oddzielenia deklaracji i definicji funkcji.

W połączeniu z jawnym tworzeniem instancji klasy biblioteka sprawdzania koncepcji doładowania (BCCL) może pomóc w generowaniu kodu funkcji szablonu w plikach CPP.

Benoit
źródło
8
Co jest w tym nieefektywnego?
Cody Gray
0

Żadne z powyższych nie działało dla mnie, więc oto jak to rozwiązałeś, moja klasa ma tylko 1 szablon.

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

pozwala to uniknąć błędów linkera i nie trzeba w ogóle wywoływać funkcji tymczasowej

KronuZ
źródło