Dzielenie klas C ++ z szablonami na pliki .hpp / .cpp - czy to możliwe?

97

Otrzymuję błędy podczas próby skompilowania klasy szablonu C ++, która jest podzielona między a .hppi .cppplik:

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

Oto mój kod:

stack.hpp :

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp :

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp :

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ldjest oczywiście poprawne: symboli nie ma stack.o.

Odpowiedź na to pytanie nie pomaga, ponieważ już robię, co mówi.
Ten może pomóc, ale nie chcę przenosić wszystkich metod do .hpppliku - nie powinienem musiał, prawda?

Czy jedynym rozsądnym rozwiązaniem jest przeniesienie wszystkiego z .cpppliku do .hpppliku i po prostu dołączenie wszystkiego zamiast dołączania jako samodzielnego pliku obiektowego? To wydaje się okropnie brzydkie! W takim przypadku mógłbym równie dobrze powrócić do poprzedniego stanu, zmienić nazwę stack.cppna stack.hppi skończyć z tym.

ucieczka
źródło
Istnieją dwa świetne obejścia, gdy chcesz naprawdę ukryć swój kod (w pliku binarnym) lub zachować go w czystości. Konieczne jest zmniejszenie ogólności, chociaż w pierwszej sytuacji. Wyjaśniono to tutaj: stackoverflow.com/questions/495021/…
Sheric
Wyraźne tworzenie instancji szablonu to sposób na skrócenie czasu kompilacji szablonów: stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

151

Nie jest możliwe zapisanie implementacji klasy szablonu w oddzielnym pliku cpp i skompilowanie. Wszystkie sposoby, aby to zrobić, jeśli ktoś twierdzi, są obejściami naśladującymi użycie oddzielnego pliku cpp, ale praktycznie jeśli zamierzasz napisać bibliotekę klas szablonów i rozprowadzić ją z plikami nagłówka i lib, aby ukryć implementację, jest to po prostu niemożliwe .

Aby wiedzieć, dlaczego, przyjrzyjmy się procesowi kompilacji. Pliki nagłówkowe nigdy nie są kompilowane. Są tylko wstępnie przetworzone. Wstępnie przetworzony kod jest następnie umieszczany w pliku cpp, który jest faktycznie kompilowany. Teraz, jeśli kompilator ma wygenerować odpowiedni układ pamięci dla obiektu, musi znać typ danych klasy szablonu.

W rzeczywistości należy rozumieć, że klasa szablonu nie jest w ogóle klasą, ale szablonem dla klasy, której deklaracja i definicja jest generowana przez kompilator w czasie kompilacji po uzyskaniu informacji o typie danych z argumentu. Dopóki nie można utworzyć układu pamięci, nie można wygenerować instrukcji dotyczących definicji metody. Pamiętaj, że pierwszym argumentem metody klasy jest operator „this”. Wszystkie metody klasowe są konwertowane na metody indywidualne z zniekształcaniem nazw i pierwszym parametrem jako obiektem, na którym działają. Argument „this” w rzeczywistości mówi o rozmiarze obiektu, w przypadku gdy klasa szablonu jest niedostępna dla kompilatora, chyba że użytkownik utworzy instancję obiektu z prawidłowym argumentem typu. W takim przypadku, jeśli umieścisz definicje metod w oddzielnym pliku cpp i spróbujesz go skompilować, sam plik obiektowy nie zostanie wygenerowany z informacjami o klasie. Kompilacja nie zakończy się niepowodzeniem, wygeneruje plik obiektowy, ale nie wygeneruje żadnego kodu dla klasy szablonu w pliku obiektowym. To jest powód, dla którego konsolidator nie może znaleźć symboli w plikach obiektowych i kompilacja kończy się niepowodzeniem.

Jaka jest teraz alternatywa dla ukrycia ważnych szczegółów implementacji? Jak wszyscy wiemy, głównym celem oddzielenia interfejsu od implementacji jest ukrycie szczegółów implementacji w postaci binarnej. Tutaj musisz oddzielić struktury danych i algorytmy. Twoje klasy szablonów muszą reprezentować tylko struktury danych, a nie algorytmy. Pozwala to na ukrycie cenniejszych szczegółów implementacji w oddzielnych bibliotekach klas, które nie są szablonami, a klasy wewnątrz będą działać na klasach szablonów lub po prostu używać ich do przechowywania danych. Klasa szablonu faktycznie zawierałaby mniej kodu do przypisywania, pobierania i ustawiania danych. Reszta pracy byłaby wykonana przez klasy algorytmów.

Mam nadzieję, że ta dyskusja byłaby pomocna.

Sharjith N.
źródło
2
„trzeba zrozumieć, że klasa szablonowa w ogóle nie jest klasą” - czy nie było na odwrót? Szablon klasy to szablon. „Klasa szablonu” jest czasami używana zamiast „tworzenia instancji szablonu” i byłaby to rzeczywista klasa.
Xupicor
Tylko w celach informacyjnych, nie jest poprawne stwierdzenie, że nie ma obejścia! Oddzielanie struktur danych od metod jest również złym pomysłem, ponieważ przeciwdziała mu hermetyzacja. Istnieje świetne obejście, którego możesz użyć w niektórych sytuacjach (uważam, że większość) tutaj: stackoverflow.com/questions/495021/…
Sheric
@Xupicor, masz rację. Z technicznego punktu widzenia „Szablon klasy” jest tym, co piszesz, aby można było utworzyć instancję „klasy szablonu” i odpowiadającego jej obiektu. Uważam jednak, że w terminologii ogólnej, używanie obu terminów zamiennie nie byłoby aż tak błędne, składnia definiowania samego „szablonu klasy” zaczyna się od słowa „szablon”, a nie „klasa”.
Sharjith N.
@Sheric, nie powiedziałem, że nie ma obejścia. W rzeczywistości wszystko, co jest dostępne, to tylko obejścia naśladujące separację interfejsu i implementację w przypadku klas szablonów. Żadne z tych obejść nie działa bez utworzenia wystąpienia określonej klasy szablonu. To w każdym razie rozwiązuje cały sens używania szablonów klas. Oddzielanie struktur danych od algorytmów to nie to samo, co oddzielanie struktur danych od metod. Klasy struktury danych mogą mieć bardzo dobre metody, takie jak konstruktory, metody pobierające i ustawiające.
Sharjith N.
Najbliższą rzeczą, jaką właśnie znalazłem, jest użycie pary plików .h / .hpp i #include „filename.hpp” na końcu pliku .h definiującego klasę szablonu. (pod nawiasem zamykającym dla definicji klasy ze średnikiem). To przynajmniej oddziela je strukturalnie pod względem plików i jest dozwolone, ponieważ na końcu kompilator kopiuje / wkleja kod .hpp do #include „filename.hpp”.
Artorias2718
90

Jest to możliwe, o ile wiesz, jakich instancji będziesz potrzebować.

Dodaj następujący kod na końcu stack.cpp i zadziała:

template class stack<int>;

Wszystkie metody stosu inne niż szablonowe zostaną utworzone, a krok łączenia będzie działał poprawnie.

Benoît
źródło
7
W praktyce większość ludzi używa do tego osobnego pliku cpp - czegoś w rodzaju stackinstantiations.cpp.
Nemanja Trifunovic
@NemanjaTrifunovic Czy możesz podać przykład, jak wyglądałby stackinstantiations.cpp?
qwerty9967
3
Właściwie istnieją inne rozwiązania: codeproject.com/Articles/48575/…
sleepsort
@ Benoît Wystąpił błąd: oczekiwano niekwalifikowanego-id przed ';' stos szablonów tokenów <int>; Wiesz dlaczego? Dzięki!
camino
3
Właściwie poprawna składnia to template class stack<int>;.
Paul Baltescu
8

Możesz to zrobić w ten sposób

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Zostało to omówione w Daniweb

Również w FAQ, ale przy użyciu słowa kluczowego eksportu w C ++.

Sadanand
źródło
5
includetworzenie cpppliku jest generalnie okropnym pomysłem. nawet jeśli masz ku temu ważny powód, plik - który tak naprawdę jest tylko gloryfikowanym nagłówkiem - powinien mieć hpplub inne rozszerzenie (np. tpp), aby bardzo jasno wyjaśnić, co się dzieje, usunąć nieporozumienia związane z makefilecelowaniem w rzeczywiste cpp pliki itp.
underscore_d
@underscore_d Czy możesz wyjaśnić, dlaczego dołączenie .cpppliku jest okropnym pomysłem?
Abbas
1
@Abbas, ponieważ rozszerzenie cpp(lub cc, lub c, lub cokolwiek innego) wskazuje, że plik jest częścią implementacji, że wynikowa jednostka tłumacząca (wyjście preprocesora) jest oddzielnie kompilowalna i że zawartość pliku jest kompilowana tylko raz. nie oznacza to, że plik jest częścią interfejsu wielokrotnego użytku, którą można dowolnie umieścić w dowolnym miejscu. #includeing jest rzeczywisty cpp plik szybko wypełnić ekran z wieloma błędami definicji, i słusznie. w tym przypadku, ponieważ nie jest to powód do #includeniego, cppbył tylko zły wybór rozszerzenia.
underscore_d
@underscore_d Więc w zasadzie używanie .cpprozszerzenia do takiego użytku jest błędem . Ale użycie innego słowa .tppjest całkowicie w porządku, które służyłoby temu samemu celowi, ale używałoby innego rozszerzenia dla łatwiejszego / szybszego zrozumienia?
Abbas
1
@Abbas Tak cpp/ cc/ etc należy unikać, ale jest to dobry pomysł, aby użyć czegoś innego niż hpp- na przykład tpp, tccitd - tak można ponownie wykorzystać resztę pliku i wskazać, że w tpppliku, mimo że działa jak nagłówek, przechowuje zewnętrzną implementację deklaracji szablonu w odpowiednim pliku hpp. Więc ten post zaczyna się od dobrego założenia - oddzielenie deklaracji i definicji do 2 różnych plików, co może być łatwiejsze do grok / grep lub czasami jest wymagane ze względu na zależności cykliczne IME - ale kończy się źle, sugerując, że drugi plik ma nieprawidłowe rozszerzenie
underscore_d
6

Nie, to niemożliwe. Nie bez exportsłowa kluczowego, które właściwie nie istnieje.

Najlepsze, co możesz zrobić, to umieścić implementacje funkcji w pliku „.tcc” lub „.tpp” i # dołączyć plik .tcc na końcu pliku .hpp. Jednak jest to tylko kosmetyczne; to wciąż to samo, co implementowanie wszystkiego w plikach nagłówkowych. To jest po prostu cena, jaką płacisz za korzystanie z szablonów.

Charles Salvia
źródło
3
Twoja odpowiedź jest nieprawidłowa. Możesz wygenerować kod z klasy szablonu w pliku cpp, mając pewność, jakich argumentów szablonu użyć. Zobacz moją odpowiedź, aby uzyskać więcej informacji.
Benoît
2
To prawda, ale wiąże się to z poważnym ograniczeniem polegającym na konieczności aktualizowania pliku .cpp i ponownej kompilacji za każdym razem, gdy wprowadzany jest nowy typ, który korzysta z szablonu, co prawdopodobnie nie jest tym, co miał na myśli OP.
Charles Salvia,
3

Uważam, że istnieją dwa główne powody, dla których warto próbować oddzielić kod oparty na szablonie do nagłówka i CPP:

Jeden jest za zwykłą elegancją. Wszyscy lubimy pisać kod, który jest łatwy do czytania, zarządzania i wielokrotnego użytku później.

Innym jest skrócenie czasu kompilacji.

Obecnie (jak zawsze) tworzę oprogramowanie do symulacji kodowania w połączeniu z OpenCL i lubimy zachować kod, aby można go było uruchomić przy użyciu typów float (cl_float) lub double (cl_double) w zależności od możliwości sprzętu. W tej chwili odbywa się to za pomocą #define REAL na początku kodu, ale nie jest to zbyt eleganckie. Zmiana żądanej precyzji wymaga ponownej kompilacji aplikacji. Ponieważ nie ma typów rzeczywistych w czasie wykonywania, na razie musimy z tym żyć. Na szczęście jądra OpenCL są kompilowane w czasie wykonywania, a prosty rozmiar (REAL) pozwala nam odpowiednio zmienić czas wykonywania kodu jądra.

Dużo większym problemem jest to, że chociaż aplikacja jest modułowa, przy opracowywaniu klas pomocniczych (takich jak te, które wstępnie obliczają stałe symulacji) również trzeba tworzyć szablony. Wszystkie te klasy pojawiają się przynajmniej raz na szczycie drzewa zależności klas, ponieważ ostateczna klasa szablonu Simulation będzie miała instancję jednej z tych klas fabrycznych, co oznacza, że ​​praktycznie za każdym razem, gdy wprowadzam niewielką zmianę w klasie fabrycznej, cały oprogramowanie musi zostać odbudowane. To bardzo irytujące, ale nie mogę znaleźć lepszego rozwiązania.

Meteorhead
źródło
2

Tylko jeśli #include "stack.cpppod koniec stack.hpp. Polecam to podejście tylko wtedy, gdy implementacja jest stosunkowo duża i jeśli zmienisz nazwę pliku .cpp na inne rozszerzenie, aby odróżnić go od zwykłego kodu.

lyricat
źródło
4
Jeśli to robisz, będziesz chciał dodać #ifndef STACK_CPP (i znajomych) do pliku stack.cpp.
Stephen Newell
Pokonaj mnie do tej sugestii. Ja też nie preferuję tego podejścia ze względu na styl.
Łukasz
2
Tak, w takim przypadku drugi plik zdecydowanie nie powinien mieć rozszerzenia cpp( cclub czegokolwiek), ponieważ jest to wyraźny kontrast w stosunku do jego prawdziwej roli. Zamiast tego należy mu nadać inne rozszerzenie, które wskazuje, że (A) jest to nagłówek i (B) nagłówek, który ma być umieszczony na dole innego nagłówka. Używam tppdo tego, co dłoń może również oznaczać tem ppóźno im plementation (Out-of-line definicje). Więcej na ten temat opisałem tutaj: stackoverflow.com/questions/1724036/…
underscore_d
2

Czasami jest możliwe, aby większość implementacji była ukryta w pliku cpp, jeśli można wyodrębnić typową funkcjonalność z wszystkich parametrów szablonu do klasy innej niż szablon (prawdopodobnie typ niebezpieczny). Wtedy nagłówek będzie zawierał przekierowania do tej klasy. Podobne podejście stosuje się podczas walki z problemem "wzdęcia szablonu".

Konstantin Tenzin
źródło
+1 - mimo że przez większość czasu nie działa zbyt dobrze (przynajmniej nie tak często, jak bym chciał)
peterchen
2

Jeśli wiesz, z jakimi typami będzie używany twój stos, możesz jawnie utworzyć ich instancję w pliku cpp i zachować tam cały odpowiedni kod.

Możliwe jest również wyeksportowanie ich do bibliotek DLL (!), Ale dość trudno jest uzyskać właściwą składnię (specyficzne dla MS kombinacje __declspec (dllexport) i słowo kluczowe export).

Użyliśmy tego w bibliotece matematycznej / geom, która zawierała szablony double / float, ale zawierała sporo kodu. (W tym czasie szukałem go w wyszukiwarce Google, ale dziś nie mam tego kodu).

Macke
źródło
2

Problem polega na tym, że szablon nie generuje rzeczywistej klasy, to tylko szablon mówiący kompilatorowi, jak wygenerować klasę. Musisz wygenerować konkretną klasę.

Łatwym i naturalnym sposobem jest umieszczenie metod w pliku nagłówkowym. Ale jest inny sposób.

Jeśli w pliku .cpp masz odniesienie do każdej instancji szablonu i wymaganej metody, kompilator wygeneruje je tam do wykorzystania w całym projekcie.

nowy stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}
Mark Okup
źródło
8
Nie potrzebujesz funkcji głupka: użyj opcji „template stack <int>;” Wymusza to wstawienie szablonu do bieżącej jednostki kompilacji. Bardzo przydatne, jeśli definiujesz szablon, ale potrzebujesz tylko kilku konkretnych implementacji w udostępnionej bibliotece.
Martin York
@Martin: w tym wszystkie funkcje członkowskie? To fantastycznie. Należy dodać tę sugestię do wątku „ukryte funkcje C ++”.
Mark Ransom
@LokiAstari Znalazłem artykuł na ten temat na wypadek, gdyby ktoś chciał dowiedzieć się więcej: cplusplus.com/forum/articles/14272
Andrew Larsson
1

Musisz mieć wszystko w pliku hpp. Problem polega na tym, że klasy nie są faktycznie tworzone, dopóki kompilator nie zobaczy, że są one potrzebne w jakimś INNYM pliku cpp - więc musi mieć cały kod dostępny do skompilowania klasy z szablonem w tym czasie.

Jedną z rzeczy, które zwykle robię, jest próba podzielenia moich szablonów na ogólną część bez szablonu (którą można podzielić między cpp / hpp) i część szablonu specyficzną dla typu, która dziedziczy klasę bez szablonu.

Aaron
źródło
1

Miejscem, w którym możesz chcieć to zrobić, jest utworzenie kombinacji biblioteki i nagłówka oraz ukrycie implementacji przed użytkownikiem. Dlatego sugerowanym podejściem jest użycie jawnej instancji, ponieważ wiesz, co ma dostarczyć Twoje oprogramowanie, i możesz ukryć implementacje.

Kilka przydatnych informacji jest tutaj: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Na przykład: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Wynik:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

Jednak nie do końca podoba mi się to podejście, ponieważ pozwala aplikacji strzelić sobie w stopę, przekazując niepoprawne typy danych do klasy z szablonem. Na przykład w funkcji main można przekazać inne typy, które mogą być niejawnie konwertowane na int, takie jak s.Push (1.2); i moim zdaniem jest to po prostu złe.

Sriram Murali
źródło
Konkretne pytanie dotyczące konkretnej instancji szablonu: stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
0

Ponieważ szablony są kompilowane w razie potrzeby, wymusza to ograniczenie dla projektów wieloplikowych: implementacja (definicja) klasy szablonu lub funkcji musi znajdować się w tym samym pliku, co jej deklaracja. Oznacza to, że nie możemy oddzielić interfejsu w osobnym pliku nagłówkowym i musimy uwzględnić zarówno interfejs, jak i implementację w każdym pliku, który używa szablonów.

ChadNC
źródło
0

Inną możliwością jest zrobienie czegoś takiego:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

Nie podoba mi się ta propozycja ze względu na styl, ale może ci odpowiadać.

Łukasz
źródło
1
Dołączony gloryfikowany drugi nagłówek powinien mieć przynajmniej rozszerzenie inne niż w cppcelu uniknięcia pomyłki z rzeczywistymi plikami źródłowymi. Typowe sugestie obejmują tppi tcc.
underscore_d
0

Słowo kluczowe „eksport” umożliwia oddzielenie implementacji szablonu od deklaracji szablonu. Zostało to wprowadzone w standardzie C ++ bez istniejącej implementacji. W odpowiednim czasie tylko kilka kompilatorów faktycznie go zaimplementowało. Przeczytaj szczegółowe informacje w artykule Inform IT dotyczącym eksportu

Shailesh Kumar
źródło
1
To jest prawie odpowiedź tylko na link, a ten link jest martwy.
underscore_d
0

1) Pamiętaj, że głównym powodem oddzielenia plików .h i .cpp jest ukrycie implementacji klasy jako osobno skompilowanego kodu Obj, który można połączyć z kodem użytkownika zawierającym .h klasy.

2) Klasy nie będące szablonami mają wszystkie zmienne konkretnie i szczegółowo zdefiniowane w plikach .h i .cpp. Kompilator będzie więc potrzebował informacji o wszystkich typach danych używanych w klasie przed kompilacją / tłumaczeniem  generowanie kodu obiektu / maszynowego Klasy szablonów nie mają informacji o konkretnym typie danych zanim użytkownik klasy utworzy instancję obiektu przekazującego wymagane dane rodzaj:

        TClass<int> myObj;

3) Dopiero po tej instancji osoba odpowiedzialna generuje określoną wersję klasy szablonu w celu dopasowania do przekazanych typów danych.

4) Dlatego .cpp NIE MOŻE być kompilowany osobno bez znajomości typu danych konkretnego użytkownika. Musi więc pozostać jako kod źródłowy w obrębie „.h”, dopóki użytkownik nie określi wymaganego typu danych, po czym można go wygenerować do określonego typu danych, a następnie skompilować

Aaron01
źródło
-3

Pracuję z Visual Studio 2010, jeśli chcesz podzielić swoje pliki na .h i .cpp, dołącz nagłówek cpp na końcu pliku .h

Ahmad
źródło