Czy dobrą praktyką jest umieszczanie definicji C ++ w plikach nagłówkowych?


196

Mój osobisty styl z C ++ zawsze umieszczał deklaracje klas w pliku dołączanym, a definicje w pliku .cpp, bardzo podobnie, jak określono w odpowiedzi Lokiego na pliki nagłówkowe C ++, Separacja kodów . Trzeba przyznać, że część tego, co podoba mi się w tym stylu, prawdopodobnie wiąże się z tymi wszystkimi latami, które spędziłem na kodowaniu Modula-2 i Ady, które mają podobny schemat z plikami specyfikacji i plikami body.

Mam współpracownika, znacznie bardziej znającego się na C ++ niż ja, który nalega, aby wszystkie deklaracje C ++ zawierały, tam gdzie to możliwe, definicje zawarte w pliku nagłówkowym. Nie twierdzi, że jest to poprawny styl alternatywny lub nawet nieco lepszy styl, ale raczej jest to nowy, powszechnie akceptowany styl, którego wszyscy używają w C ++.

Nie jestem już tak powściągliwy jak kiedyś, więc tak naprawdę nie mam ochoty wskoczyć na jego modę, dopóki nie zobaczę tam kilku innych ludzi. Więc jak często ten idiom jest naprawdę powszechny?

Żeby udzielić odpowiedzi na pytania: czy teraz jest to Droga , bardzo powszechna, nieco powszechna, niecodzienna czy szalona?


funkcje jednowierszowe (pobierające i ustawiające) w nagłówku są wspólne. Dłużej niż można by rzucić pytające drugie spojrzenie. Być może do pełnej definicji małej klasy, która jest używana tylko przez inną w tym samym nagłówku?
Martin Beckett,

do tej pory zawsze umieszczałem wszystkie moje definicje klas w nagłówkach. wyjątki stanowią tylko definicje klas pimpl. Deklaruję tylko te w nagłówkach.
Johannes Schaub - litb

3
Może on tak myśli, ponieważ w ten sposób Visual C ++ nalega na napisanie kodu. Po kliknięciu przycisku implementacja jest generowana w pliku nagłówkowym. Nie wiem jednak, dlaczego Microsoft zachęca do tego z powodów, które inni wyjaśnili poniżej.
WKS

4
@ WKS - Microsoft wolałby, żeby każdy programował w języku C #, aw języku C # nie ma rozróżnienia „nagłówek” vs. „ciało”, to tylko jeden plik. Będąc w świecie C ++ i C # od dłuższego czasu, sposób w C # jest znacznie łatwiejszy do opanowania.
Mark Lakata

1
@MarkLakata - To rzeczywiście jedna z rzeczy, na które wskazał. Ostatnio nie słyszałem od niego tego argumentu, ale IIRC argumentował, że Java i C # działają w ten sposób, a C # był w tym czasie zupełnie nowy, co sprawiło, że stał się trendem, że wszystkie języki wkrótce będą podążać
TED

Odpowiedzi:


209

Twój współpracownik się myli, powszechnym sposobem jest i zawsze było umieszczanie kodu w plikach .cpp (lub jakimkolwiek innym rozszerzeniu, które lubisz) i deklaracjach w nagłówkach.

Czasami wstawianie kodu do nagłówka ma pewne zalety, co może pozwolić na bardziej sprytne wstawianie przez kompilator. Ale jednocześnie może zniszczyć Twoje czasy kompilacji, ponieważ cały kod musi być przetwarzany za każdym razem, gdy jest uwzględniany przez kompilator.

Wreszcie, często denerwujące są okrągłe relacje między obiektami (czasem pożądane), gdy cały kod jest nagłówkiem.

Podsumowując, miałeś rację, on się myli.

EDYCJA: Myślałem o twoim pytaniu. Jest jeden przypadek, w którym to, co mówi, jest prawdą. szablony. Wiele nowszych „nowoczesnych” bibliotek, takich jak boost, intensywnie wykorzystuje szablony i często są one „tylko nagłówkami”. Należy to jednak zrobić tylko w przypadku szablonów, ponieważ jest to jedyny sposób, aby to zrobić.

EDYCJA: Niektórzy chcieliby trochę więcej wyjaśnień, oto kilka minusów pisania kodu „tylko nagłówek”:

Jeśli się rozejrzysz, zobaczysz, że sporo osób próbuje znaleźć sposób na skrócenie czasu kompilacji podczas pracy z boostem. Na przykład: Jak skrócić czas kompilacji za pomocą Boost Asio , który widzi kompilację 14s pojedynczego pliku 1K z dołączonym boostem. 14s może nie wydawać się „eksplodować”, ale z pewnością jest znacznie dłuższy niż typowy i może zsumować się dość szybko. W przypadku dużego projektu. Biblioteki tylko nagłówka wpływają na czasy kompilacji w dość wymierny sposób. Po prostu tolerujemy to, ponieważ wzmocnienie jest tak przydatne.

Ponadto istnieje wiele rzeczy, których nie można zrobić tylko w nagłówkach (nawet boost ma biblioteki, do których należy się połączyć w przypadku niektórych części, takich jak wątki, system plików itp.). Podstawowym przykładem jest to, że nie można mieć prostych obiektów globalnych w bibliotekach tylko nagłówkowych (chyba że skorzystasz z obrzydliwości, która jest singletonem), ponieważ napotkasz błędy definicji. UWAGA: Zmienne wbudowane w C ++ 17 sprawią, że ten konkretny przykład będzie wykonalny w przyszłości.

Na koniec, gdy używasz wzmocnienia jako przykładu kodu zawierającego tylko nagłówek, często pomija się ogromny szczegół.

Boost to biblioteka, a nie kod poziomu użytkownika. więc to nie zmienia się tak często. Jeśli w kodzie użytkownika umieścisz wszystko w nagłówkach, każda drobna zmiana spowoduje konieczność ponownej kompilacji całego projektu. To ogromna strata czasu (i nie dotyczy to bibliotek, które nie zmieniają się z kompilacji na kompilację). Kiedy dzielisz rzeczy między nagłówek / źródło, a jeszcze lepiej, użyj deklaracji przesyłania dalej, aby zmniejszyć liczbę dołączeń, możesz zaoszczędzić godziny ponownej kompilacji, jeśli zostaną dodane w ciągu jednego dnia.


16
Jestem prawie pewien, że właśnie stąd to bierze. Kiedy tylko to się pojawi, przywołuje szablony. Jego argumentem jest z grubsza, że ​​powinieneś robić cały kod w ten sposób, aby zachować spójność.
TED

14
to kiepski argument, który stawia, trzymajcie się swoich pistoletów :)
Evan Teran

11
Definicje szablonów mogą znajdować się w plikach CPP, jeśli obsługiwane jest słowo kluczowe „eksport”. To ciemny kąt C ++, który, zgodnie z moją najlepszą wiedzą, zwykle nie jest nawet implementowany przez większość kompilacji.
Andrei Taranchenko

2
Zobacz na dole tej odpowiedzi (góra jest nieco skomplikowana) na przykład: stackoverflow.com/questions/555330/…
Evan Teran

3
Zaczyna mieć znaczenie dla tej dyskusji w „Brawo, żadnych błędów linkera”.
Evan Teran

158

W dniu, w którym koderzy C ++ uzgodnią Drogę , jagnięta położy się z lwami, Palestyńczycy przyjmą Izraelczyków, a koty i psy będą mogły wyjść za mąż.

Oddzielenie plików .h i .cpp jest w tym momencie w większości arbitralne, co pozostało po optymalizacji kompilatora. Moim zdaniem deklaracje należą do nagłówka, a definicje do pliku implementacyjnego. Ale to tylko nawyk, nie religia.


140
„W dniu, w którym koderzy C ++ uzgodnią The Way ...” pozostanie tylko jeden koder C ++!
Brian Ensink

9
Myślałem, że już się zgadzają na drodze, deklaracje w .h i definicje w .cpp
hasen

6
Wszyscy jesteśmy niewidomi, a C ++ jest słoniem.
Roderick Taylor,

nawyk? więc co z użyciem .h do zdefiniowania zakresu? przez co to zostało zastąpione?
Hernán Eche

28

Kod w nagłówkach jest ogólnie złym pomysłem, ponieważ wymusza rekompilację wszystkich plików, które zawierają nagłówek, gdy zmieniasz rzeczywisty kod, a nie deklaracje. Spowolni również kompilację, ponieważ będziesz musiał przeanalizować kod w każdym pliku zawierającym nagłówek.

Powodem, dla którego kod w plikach nagłówkowych jest potrzebny, jest to na ogół potrzebne do poprawnego działania słowa kluczowego w wierszu i podczas korzystania z szablonów, które są tworzone w innych plikach CPP.


1
„wymusza rekompilację wszystkich plików, które zawierają nagłówek, gdy zmieniasz rzeczywisty kod, a nie deklaracje”. Myślę, że jest to najbardziej autentyczny powód; idzie również z tym, że deklaracje w nagłówkach zmieniają się rzadziej niż implementacja w plikach .c.
Ninad

20

To, co może informować współpracownika, to pogląd, że większość kodu C ++ powinna być szablonowana, aby umożliwić maksymalną użyteczność. A jeśli jest szablonowy, wszystko musi znajdować się w pliku nagłówkowym, aby kod klienta mógł go zobaczyć i utworzyć go. Jeśli jest wystarczająco dobry dla Boost i STL, jest dla nas wystarczająco dobry.

Nie zgadzam się z tym punktem widzenia, ale może być skąd pochodzi.


Myślę, że masz rację co do tego. Kiedy o tym rozmawiamy, zawsze używa przykładu szablonów, gdzie mniej więcej musisz to zrobić. Nie zgadzam się również z „koniecznością”, ale moje alternatywy są raczej zawiłe.
TED

1
@ted - w przypadku kodu szablonowego należy umieścić implementację w nagłówku. Słowo kluczowe „eksport” umożliwia kompilatorowi obsługę rozdzielania deklaracji i definicji szablonów, ale obsługa eksportu jest w większości nieistniejąca. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr

Nagłówka, tak, ale to nie musi być taki sam nagłówek. Zobacz odpowiedź nieznanego poniżej.
TED

To ma sens, ale nie mogę powiedzieć, że wcześniej spotkałem ten styl.
Michael Burr

14

Myślę, że twój współpracownik jest mądry i ty też masz rację.

Przydatne rzeczy, które znalazłem, umieszczając wszystko w nagłówkach, to:

  1. Nie trzeba pisać i synchronizować nagłówków i źródeł.

  2. Struktura jest prosta i brak zależności między kołami zmusza koder do stworzenia „lepszej” struktury.

  3. Przenośny, łatwy do osadzenia w nowym projekcie.

Zgadzam się z problemem czasu kompilacji, ale myślę, że powinniśmy zauważyć, że:

  1. Zmiana pliku źródłowego najprawdopodobniej zmieni pliki nagłówkowe, co prowadzi do ponownej kompilacji całego projektu.

  2. Szybkość kompilacji jest znacznie szybsza niż wcześniej. A jeśli masz projekt do zbudowania z długim czasem i wysoką częstotliwością, może to oznaczać, że Twój projekt ma wady. Rozdziel zadania na różne projekty, a moduł może uniknąć tego problemu.

Na koniec chcę tylko wspierać waszego współpracownika, tylko z mojego osobistego punktu widzenia.


3
+1. Nikt oprócz ciebie nie wpadł na pomysł, że tylko w projekcie nagłówka długie czasy kompilacji mogą wskazywać na zbyt duże zależności, co jest złym projektem. Słuszna uwaga! Ale czy te zależności można usunąć w takim stopniu, w którym czas kompilacji jest tak krótki?
TobiMcNamobi

@TobiMcNamobi: Podoba mi się pomysł „zwalniania”, aby uzyskać lepszą opinię na temat złych decyzji projektowych. Jednak w przypadku kompilacji tylko w nagłówku vs. oddzielnie, jeśli zdecydujemy się na ten pomysł, otrzymamy jedną jednostkę kompilacji i ogromne czasy kompilacji. Nawet jeśli projekt jest naprawdę świetny.
Jo So

Innymi słowy, oddzielenie interfejsu od implementacji jest w rzeczywistości częścią twojego projektu. W C wymagane jest wyrażenie decyzji dotyczących enkapsulacji poprzez rozdzielenie w nagłówku i implementację.
Jo So

1
Zaczynam się zastanawiać, czy w ogóle są jakieś wady polegające na porzuceniu nagłówków, tak jak robią to współczesne języki.
Jo So

12

Często umieszczam w pliku nagłówkowym trywialne funkcje składowe, aby umożliwić ich wstawianie. Ale żeby umieścić tam cały kod, żeby zachować spójność z szablonami? To zwykłe orzechy.

Pamiętaj: głupia konsekwencja to hobgoblin małych umysłów .


Tak, ja też to robię. Ogólna zasada, której używam, wydaje się być czymś w rodzaju „jeśli pasuje do jednego wiersza kodu, zostaw to w nagłówku”.
TED

Co się stanie, gdy biblioteka udostępni A<B>treść klasy szablonu w pliku CPP, a następnie użytkownik będzie chciał A<C>?
jww

@jww Nie podałem tego wprost, ale klasy szablonów powinny być w pełni zdefiniowane w nagłówkach, aby kompilator mógł utworzyć instancję z dowolnymi potrzebnymi typami. To wymóg techniczny, a nie wybór stylistyczny. Myślę, że problem w pierwotnym pytaniu polega na tym, że ktoś zdecydował, czy to jest dobre dla szablonów, ale także dla regularnych zajęć.
Mark Ransom,

7

Jak powiedział Tuomas, nagłówek powinien być minimalny. Aby zakończyć, rozwinę się nieco.

Osobiście korzystam z 4 typów plików w moich C++projektach:

  • Publiczny:
  • Przekazywanie nagłówka: w przypadku szablonów itp. Plik ten otrzymuje deklaracje przekazywania, które pojawią się w nagłówku.
  • Nagłówek: ten plik zawiera ewentualny nagłówek przekazywania i deklaruje wszystko, co chcę być publiczny (i definiuje klasy ...)
  • Prywatny:
  • Prywatny nagłówek: ten plik jest nagłówkiem zarezerwowanym do implementacji, zawiera nagłówek i deklaruje funkcje / struktury pomocnicze (na przykład dla Pimpl lub predykatów). Pomiń, jeśli nie jest to konieczne.
  • Plik źródłowy: zawiera prywatny nagłówek (lub nagłówek, jeśli nie ma prywatnego nagłówka) i definiuje wszystko (nie-szablon ...)

Ponadto łączę to z inną regułą: nie definiuj tego, co możesz przesłać dalej deklarować. Chociaż oczywiście jestem tam rozsądny (używanie Pimpl wszędzie jest dość kłopotliwe).

Oznacza to, że wolę deklarację przekazania zamiast #includedyrektywy w moich nagłówkach, ilekroć będę w stanie uciec od nich.

Na koniec stosuję również zasadę widoczności: ograniczam zakresy moich symboli tak bardzo, jak to możliwe, aby nie zanieczyszczały zewnętrznych zakresów.

Podsumowując:

// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;

// example.hpp
#include "project/example_fwd.hpp"

// Those can't really be skipped
#include <string>
#include <vector>

#include "project/pimpl.hpp"

// Those can be forward declared easily
#include "project/foo_fwd.hpp"

namespace project { class Bar; }

namespace project
{
  class MyClass
  {
  public:
    struct Color // Limiting scope of enum
    {
      enum type { Red, Orange, Green };
    };
    typedef Color::type Color_t;

  public:
    MyClass(); // because of pimpl, I need to define the constructor

  private:
    struct Impl;
    pimpl<Impl> mImpl; // I won't describe pimpl here :p
  };

  template <class T> class MyClassT: public MyClass {};
} // namespace project

// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"

template <class T> void check(MyClass<T> const& c) { }

// example.cpp
#include "example_impl.hpp"

// MyClass definition

Ratownik jest to, że większość czasu nagłówek przodu jest bezużyteczny: konieczne tylko w przypadku typedefalbo templatei tak jest nagłówek realizacja;)


6

Aby dodać więcej zabawy, możesz dodać .ipppliki, które zawierają implementację szablonu (która jest uwzględniona .hpp), podczas gdy .hppzawiera interfejs.

Ponieważ poza kodowanym szablonem (w zależności od projektu może to być większość lub mniejszość plików) istnieje normalny kod i tutaj lepiej jest oddzielić deklaracje i definicje. W razie potrzeby podaj także deklaracje forward - może to mieć wpływ na czas kompilacji.


Właśnie to zrobiłem z definicjami szablonów (chociaż nie jestem pewien, czy użyłem tego samego rozszerzenia ... minęło trochę czasu).
TED

5

Zasadniczo, pisząc nową klasę, umieszczam cały kod w klasie, więc nie muszę szukać innego pliku. Po tym, jak wszystko działa, rozkładam treść metod na plik cpp , pozostawiając prototypy w pliku hpp.


4

Jeśli ten nowy sposób jest naprawdę The Way , moglibyśmy podążać w innym kierunku w naszych projektach.

Ponieważ staramy się unikać wszystkich niepotrzebnych rzeczy w nagłówkach. Obejmuje to unikanie kaskady nagłówków. Kod w nagłówkach będzie prawdopodobnie wymagał dołączenia innego nagłówka, który będzie wymagał innego nagłówka i tak dalej. Jeśli jesteśmy zmuszeni do korzystania z szablonów, staramy się zbytnio nie zaśmiecać nagłówków elementami szablonu.

W razie potrzeby używamy również „nieprzezroczystego wskaźnika” .

Dzięki tym praktykom możemy wykonywać szybsze kompilacje niż większość naszych rówieśników. I tak ... zmiana kodu lub członków klasy nie spowoduje wielkich przebudów.


4

Osobiście robię to w moich plikach nagłówkowych:

// class-declaration

// inline-method-declarations

Nie lubię mieszać kodu metod z klasą, ponieważ trudno mi jest szybko znaleźć rzeczy.

Nie umieściłbym WSZYSTKICH metod w pliku nagłówkowym. Kompilator (normalnie) nie będzie w stanie wstawiać metod wirtualnych i (prawdopodobnie) będzie wstawiał tylko małe metody bez pętli (całkowicie zależy od kompilatora).

Robienie metod w klasie jest poprawne ... ale z czytelnego punktu widzenia nie podoba mi się to. Umieszczenie metod w nagłówku oznacza, że ​​jeśli to możliwe, zostaną wstawione.


2

IMHO, Zasługuje TYLKO, jeśli robi szablony i / lub metaprogramowanie. Jest już wiele powodów, dla których ograniczasz pliki nagłówkowe do samych deklaracji. Są po prostu… nagłówkami. Jeśli chcesz dołączyć kod, skompiluj go jako bibliotekę i połącz z nim.


2

Wyciągnąłem całą implementację z definicji klasy. Chcę mieć komentarze doxygen z definicji klasy.


1
Wiem, że jest późno, ale zwolennicy (lub sympatycy) chcą komentować, dlaczego? Wydaje mi się to rozsądnym stwierdzeniem. Używamy Doxygen i problem z pewnością się pojawił.
TED

2

Myślę, że absolutnie absurdalne jest umieszczenie WSZYSTKICH definicji funkcji w pliku nagłówkowym. Czemu? Ponieważ plik nagłówkowy jest używany jako interfejs PUBLICZNY dla twojej klasy. To zewnętrzna część „czarnej skrzynki”.

Kiedy musisz spojrzeć na klasę, aby dowiedzieć się, jak z niej korzystać, powinieneś spojrzeć na plik nagłówka. Plik nagłówkowy powinien zawierać listę tego, co może zrobić (skomentowany, aby opisać szczegóły korzystania z każdej funkcji) i powinien zawierać listę zmiennych składowych. NIE POWINIEN OBEJMOWAĆ JAK każda poszczególna funkcja jest zaimplementowana, ponieważ jest to ładunek niepotrzebnych informacji i tylko zaśmieca plik nagłówkowy.


1

Czy to naprawdę nie zależy od złożoności systemu i wewnętrznych konwencji?

Obecnie pracuję nad niezwykle złożonym symulatorem sieci neuronowej, a akceptowanym stylem, którego mam się spodziewać, jest:

Definicje klas w classname.h
Kod klasy w classnameCode.h
kod wykonywalny w classname.cpp

Dzieli to symulacje zbudowane przez użytkownika od klas bazowych opracowanych przez programistów i działa najlepiej w danej sytuacji.

Byłbym jednak zaskoczony, gdy ludzie robią to w aplikacji graficznej lub w jakiejkolwiek innej aplikacji, której celem nie jest zapewnienie użytkownikom bazy kodu.


1
Czym dokładnie jest różnica między „Kodem klasy” a „Kodem wykonywalnym”?
TED

Jak powiedziałem, jest to symulator neuronowy: użytkownik tworzy symulacje wykonywalne, które są zbudowane na dużej liczbie klas, które działają jak neurony itp. Więc nasz kod to po prostu klasy, które same nie mogą nic zrobić, a użytkownik tworzy kod wykonywalny dzięki czemu symulator robi różne rzeczy.
Ed James

Zasadniczo, czy nie można powiedzieć „sama nie jest w stanie nic zrobić” dla zdecydowanej większości (jeśli nie całości) większości programów? Czy mówisz, że „główny” kod wchodzi w procesor, ale nic więcej nie robi?
TED

W tej sytuacji jest trochę inaczej. Kod, który piszemy, jest w zasadzie biblioteką, a użytkownik buduje na nim swoje symulacje, które w rzeczywistości można uruchomić. Pomyśl o tym jak o openGL -> masz mnóstwo funkcji i obiektów, ale bez pliku cpp, który mógłby je uruchomić, są bezużyteczne.
Ed James

0

Kod szablonu powinien znajdować się tylko w nagłówkach. Poza tym wszystkie definicje z wyjątkiem wstawiania powinny być w .cpp. Najlepszym argumentem za tym byłyby implementacje standardowej biblioteki, które stosują tę samą regułę. Nie zgodziłbyś się, że deweloperzy std lib mieliby rację w tej sprawie.


Które stdlibs? libstdc++Wydaje się , że GCC (AFAICS) nie umieszcza prawie nic srci prawie wszystkiego include, niezależnie od tego, czy „musi” być w nagłówku. Więc nie sądzę, że jest to dokładne / przydatne odniesienie. W każdym razie, nie sądzę, aby stdlibs były wzorem dla kodu użytkownika: są one oczywiście napisane przez wysoko wykwalifikowanych programistów, ale należy ich używać , a nie czytać: usuwają dużą złożoność, o której większość programistów nie powinna myśleć , _Reserved __nameswszędzie są brzydkie, aby uniknąć konfliktów z użytkownikiem, komentarze i odstępy są poniżej tego, co radziłbym itp. Są wzorowe w wąskim zakresie.
underscore_d

0

Myślę, że twój współpracownik ma rację, dopóki nie wejdzie w proces pisania kodu wykonywalnego w nagłówku. Wydaje mi się, że właściwą równowagą jest podążanie ścieżką wskazaną przez GNAT Ada, gdzie plik .ads zapewnia całkowicie odpowiednią definicję interfejsu pakietu dla jego użytkowników i jego dzieci.

Nawiasem mówiąc, Ted, czy na tym forum zapoznałeś się z ostatnim pytaniem na temat powiązania Ady z biblioteką CLIPS, które napisałeś kilka lat temu i które nie jest już dostępne (odpowiednie strony internetowe są teraz zamknięte). Nawet jeśli zostanie wykonane w starej wersji Clips, to powiązanie może być dobrym przykładem na początek dla kogoś, kto chce korzystać z mechanizmu wnioskowania CLIPS w programie Ada 2012.


1
Lol. Dwa lata później jest to dziwny sposób na złapanie kogoś. Sprawdzę, czy nadal mam kopię, ale najprawdopodobniej nie. Zrobiłem to dla klasy AI, abym mógł napisać kod w Adzie, ale celowo stworzyłem ten projekt CC0 (zasadniczo nie posiadający prawa autorskiego) w nadziei, że ktoś bezwstydnie go weźmie i coś z tym zrobi.
TED
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.