Otrzymuję błędy podczas próby skompilowania klasy szablonu C ++, która jest podzielona między a .hpp
i .cpp
plik:
$ 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;
}
ld
jest 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 .hpp
pliku - nie powinienem musiał, prawda?
Czy jedynym rozsądnym rozwiązaniem jest przeniesienie wszystkiego z .cpp
pliku do .hpp
pliku 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.cpp
na stack.hpp
i skończyć z tym.
Odpowiedzi:
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.
źródło
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:
Wszystkie metody stosu inne niż szablonowe zostaną utworzone, a krok łączenia będzie działał poprawnie.
źródło
template class stack<int>;
.Możesz to zrobić w ten sposób
Zostało to omówione w Daniweb
Również w FAQ, ale przy użyciu słowa kluczowego eksportu w C ++.
źródło
include
tworzeniecpp
pliku 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ćhpp
lub inne rozszerzenie (np.tpp
), aby bardzo jasno wyjaśnić, co się dzieje, usunąć nieporozumienia związane zmakefile
celowaniem w rzeczywistecpp
pliki itp..cpp
pliku jest okropnym pomysłem?cpp
(lubcc
, lubc
, 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.#include
ing jest rzeczywistycpp
plik szybko wypełnić ekran z wieloma błędami definicji, i słusznie. w tym przypadku, ponieważ nie jest to powód do#include
niego,cpp
był tylko zły wybór rozszerzenia..cpp
rozszerzenia do takiego użytku jest błędem . Ale użycie innego słowa.tpp
jest całkowicie w porządku, które służyłoby temu samemu celowi, ale używałoby innego rozszerzenia dla łatwiejszego / szybszego zrozumienia?cpp
/cc
/ etc należy unikać, ale jest to dobry pomysł, aby użyć czegoś innego niżhpp
- na przykładtpp
,tcc
itd - tak można ponownie wykorzystać resztę pliku i wskazać, że wtpp
pliku, mimo że działa jak nagłówek, przechowuje zewnętrzną implementację deklaracji szablonu w odpowiednim plikuhpp
. 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 rozszerzenieNie, to niemożliwe. Nie bez
export
sł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.
źródło
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.
źródło
Tylko jeśli
#include "stack.cpp
pod koniecstack.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.źródło
cpp
(cc
lub 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żywamtpp
do tego, co dłoń może również oznaczaćt
emp
późno imp
lementation (Out-of-line definicje). Więcej na ten temat opisałem tutaj: stackoverflow.com/questions/1724036/…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".
źródło
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).
źródło
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:
źródło
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.
źródło
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
stack.cpp
main.cpp
Wynik:
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.
źródło
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.
źródło
Inną możliwością jest zrobienie czegoś takiego:
Nie podoba mi się ta propozycja ze względu na styl, ale może ci odpowiadać.
źródło
cpp
celu uniknięcia pomyłki z rzeczywistymi plikami źródłowymi. Typowe sugestie obejmujątpp
itcc
.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
źródło
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:
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ć
źródło
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
źródło