Zazwyczaj deklarując klasę C ++, najlepszą praktyką jest umieszczanie tylko deklaracji w pliku nagłówkowym i implementacji w pliku źródłowym. Wydaje się jednak, że ten model projektowy nie działa w przypadku klas szablonów.
Podczas wyszukiwania online wydaje się, że są 2 opinie na temat najlepszego sposobu zarządzania klasami szablonów:
1. Cała deklaracja i wdrożenie w nagłówku.
Jest to dość proste, ale prowadzi do tego, co moim zdaniem jest trudne do utrzymania i edycji plików kodu, gdy szablon staje się duży.
2. Napisz implementację w szablonie dołączonym na końcu plikiem (.tpp).
Wydaje mi się to lepszym rozwiązaniem, ale nie wydaje się być szeroko stosowane. Czy istnieje powód, dla którego takie podejście jest gorsze?
Wiem, że wiele razy styl kodu jest podyktowany osobistymi preferencjami lub starszym stylem. Zaczynam nowy projekt (przenoszę stary projekt C do C ++) i jestem stosunkowo nowy w projektowaniu OO i od samego początku chciałbym stosować się do najlepszych praktyk.
źródło
Odpowiedzi:
Pisząc szablonową klasę C ++, zazwyczaj masz trzy opcje:
(1) Umieść deklarację i definicję w nagłówku.
lub
Zawodowiec:
Kon:
Foo
jako członek, musisz ją uwzględnićfoo.h
. Oznacza to, że zmiana implementacjiFoo::f
propagacji odbywa się zarówno przez pliki nagłówkowe, jak i źródłowe.Przyjrzyjmy się bliżej wpływowi przebudowy: w przypadku nieszablonowanych klas C ++ deklaracje umieszczasz w .h, a definicje metod w .cpp. W ten sposób, gdy implementacja metody zostanie zmieniona, tylko jeden plik .cpp musi zostać ponownie skompilowany. Jest inaczej w przypadku klas szablonów, jeśli .h zawiera cały kod. Spójrz na następujący przykład:
Tutaj jedynym zastosowaniem
Foo::f
jest wnętrzebar.cpp
. Jeśli jednak zmienić realizacjiFoo::f
, zarównobar.cpp
iqux.cpp
potrzeby rekompilacji. ImplementacjaFoo::f
życia w obu plikach, nawet jeśli żadna częśćQux
bezpośrednio z nich nie korzystaFoo::f
. W przypadku dużych projektów może to wkrótce stać się problemem.(2) Umieść deklarację w .h, a definicję w .tpp i dołącz do .h.
Zawodowiec:
Kon:
To rozwiązanie dzieli deklarację i definicję metody na dwa osobne pliki, podobnie jak .h / .cpp. Jednak w tym podejściu występuje ten sam problem z odbudową, co (1) , ponieważ nagłówek zawiera bezpośrednio definicje metod.
(3) Umieść deklarację w .h, a definicję w .tpp, ale nie dołączaj .tpp do .h.
Zawodowiec:
Kon:
Foo
członka do klasyBar
, musisz dołączyć gofoo.h
do nagłówka. Jeśli wywołujeszFoo::f
plik .cpp, musisz tam również dołączyćfoo.tpp
.Takie podejście zmniejsza wpływ przebudowy, ponieważ tylko pliki .cpp, które naprawdę korzystają,
Foo::f
muszą zostać ponownie skompilowane. Ma to jednak swoją cenę: wszystkie te pliki muszą zostać uwzględnionefoo.tpp
. Weź przykład z góry i zastosuj nowe podejście:Jak widać, jedyną różnicą jest dodatkowe włączenie
foo.tpp
wbar.cpp
. Jest to niewygodne, a dodanie drugiej klasy dla klasy w zależności od tego, czy wywołujesz metody, wydaje się bardzo brzydkie. Zmniejszasz jednak wpływ przebudowy: musisz tylkobar.cpp
ponownie skompilować, jeśli zmienisz implementacjęFoo::f
. Plikqux.cpp
nie wymaga ponownej kompilacji.Podsumowanie:
Jeśli implementujesz bibliotekę, zwykle nie musisz przejmować się skutkami odbudowy. Użytkownicy Twojej biblioteki pobierają wersję i korzystają z niej, a implementacja biblioteki nie zmienia się w codziennej pracy użytkownika. W takich przypadkach biblioteka może zastosować podejście (1) lub (2) i to tylko kwestia gustu, który wybierzesz.
Jeśli jednak pracujesz nad aplikacją lub pracujesz nad biblioteką wewnętrzną swojej firmy, kod często się zmienia. Musisz więc dbać o wpływ odbudowy. Wybór podejścia (3) może być dobrą opcją, jeśli zachęcisz programistów do zaakceptowania dodatkowego uwzględnienia.
źródło
Podobnie do
.tpp
pomysłu (którego nigdy nie widziałem), umieszczamy większość wbudowanych funkcji w-inl.hpp
pliku znajdującym się na końcu zwykłego.hpp
pliku.Jak wskazują inne, dzięki temu interfejs jest czytelny, przenosząc bałagan implementacji wbudowanych (takich jak szablony) w innym pliku. Zezwalamy na wstawianie niektórych interfejsów, ale staramy się ograniczać je do małych, zwykle jednowierszowych funkcji.
źródło
Jedna profesjonalna moneta drugiego wariantu polega na tym, że nagłówki wyglądają na bardziej uporządkowane.
Wadą może być to, że masz wbudowane sprawdzanie błędów IDE, a powiązania debuggera są zepsute.
źródło
Zdecydowanie wolę podejście polegające na umieszczeniu implementacji w osobnym pliku i posiadaniu tylko dokumentacji i deklaracji w pliku nagłówkowym.
Być może powodem, dla którego nie widziałeś tego podejścia w praktyce, jest to, że nie szukałeś właściwych miejsc ;-)
Lub - być może dlatego, że zajmuje to trochę dodatkowego wysiłku w celu opracowania oprogramowania. Ale w przypadku biblioteki klasowej ten wysiłek jest WOLNY, IMHO, i zwraca się w znacznie łatwiejszej w użyciu / czytaniu bibliotece.
Weźmy tę bibliotekę na przykład: https://github.com/SophistSolutions/Stroika/
Cała biblioteka jest napisana z takim podejściem i jeśli przejrzysz kod, zobaczysz, jak dobrze działa.
Pliki nagłówkowe mają długość plików implementacyjnych, ale są wypełnione jedynie deklaracjami i dokumentacją.
Porównaj czytelność Stroiki z twoją ulubioną implementacją std c ++ (gcc lub libc ++ lub msvc). Wszystkie wykorzystują wbudowane podejście do implementacji w nagłówku i chociaż są wyjątkowo dobrze napisane, IMHO, nie są implementacjami czytelnymi.
źródło