Czy dobrą praktyką jest poleganie na przesyłaniu nagłówków w sposób tranzytowy?

37

Usuwam dołączenia w projekcie C ++, nad którym pracuję, i wciąż zastanawiam się, czy powinienem jawnie dołączyć wszystkie nagłówki użyte bezpośrednio w określonym pliku, czy też powinienem zawierać tylko absolutne minimum.

Oto przykład Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Załóżmy, że deklaracja przekazania dla RenderObjectnie jest opcją.)

Teraz wiem, że to RenderObject.hppobejmuje Texture.hpp- wiem to, ponieważ każdy RenderObjectma Textureczłonka. Mimo to wyraźnie zawierać Texture.hppw Entity.hpp, bo nie jestem pewien, czy to dobry pomysł, aby polegać na nim są zawarte w RenderObject.hpp.

Więc: czy to dobra praktyka czy nie?

futlib
źródło
19
Gdzie są w tym strażnicy uwzględnienia? Po prostu zapomniałeś je przypadkowo, mam nadzieję?
Doc Brown
3
Jednym z problemów, który występuje, gdy nie dołączasz wszystkich używanych plików, jest to, że czasami kolejność dołączania plików staje się ważna. To jest naprawdę denerwujące tylko w jednym przypadku, w którym to się zdarza, ale czasami śnieżki, a ty naprawdę marzysz, że osoba, która napisała taki kod, byłaby maszerowana przed plutonem egzekucyjnym.
Dunk
Oto dlaczego #ifndef _RENDER_H #define _RENDER_H ... #endif.
sampathsris,
@Dunk Myślę, że źle zrozumiałeś problem. Z jedną z jego sugestii, które nie powinny się zdarzyć.
Kaczka Mooing
1
@DocBrown, zgadza #pragma oncesię, nie?
Pacerier

Odpowiedzi:

65

Zawsze należy dołączyć wszystkie nagłówki definiujące wszelkie obiekty użyte w pliku .cpp w tym pliku, niezależnie od tego, co wiesz o tym, co jest w tych plikach. Powinieneś mieć osłony we wszystkich plikach nagłówków, aby upewnić się, że wielokrotne dołączanie nagłówków nie ma znaczenia.

Powody:

  • To wyjaśnia programistom, którzy dokładnie czytają źródło, czego dokładnie wymaga dany plik źródłowy. Tutaj ktoś, kto patrzy na kilka pierwszych wierszy pliku, może zobaczyć, że masz do czynienia z Textureobiektami w tym pliku.
  • Pozwala to uniknąć problemów polegających na tym, że refaktoryzowane nagłówki powodują problemy z kompilacją, gdy same nie wymagają już konkretnych nagłówków. Załóżmy na przykład, że zdajesz sobie sprawę, że RenderObject.hpptak naprawdę nie potrzebuje Texture.hppsiebie.

Następstwem jest to, że nigdy nie należy umieszczać nagłówka w innym nagłówku, chyba że jest to wyraźnie potrzebne w tym pliku.

Gort the Robot
źródło
10
Zgadzam się z następstwem - pod warunkiem, że ZAWSZE powinien zawierać drugi nagłówek, jeśli tego potrzebuje!
Andrew
1
Nie podoba mi się praktyka bezpośredniego włączania nagłówków dla wszystkich poszczególnych klas. Popieram kumulatywne nagłówki. To znaczy, myślę, że plik wysokiego poziomu powinien odwoływać się do „modułu”, którego używa, ale nie musi bezpośrednio zawierać wszystkich poszczególnych części.
edA-qa mort-ora-y
8
Prowadzi to do dużych, monolitycznych nagłówków, które każdy plik zawiera, nawet jeśli potrzebują tylko niewielkich fragmentów tego, co się w nim znajduje, co prowadzi do długich czasów kompilacji i utrudnia refaktoryzację.
Gort the Robot
6
Google stworzyło narzędzie, które pomaga dokładnie egzekwować tę poradę, zwane „ włącz to, czego używasz” .
Matthew G.
3
Podstawowym problemem związanym z czasem kompilacji z dużymi monolitycznymi nagłówkami nie jest czas kompilacji samego kodu nagłówka, ale konieczność kompilacji każdego pliku CPP w aplikacji przy każdej zmianie nagłówka. Wstępnie skompilowane nagłówki nie pomagają w tym.
Gort the Robot
23

Ogólna ogólna zasada brzmi: dołącz to, czego używasz. Jeśli używasz obiektu bezpośrednio, dołącz bezpośrednio jego plik nagłówka. Jeśli używasz obiektu A, który używa B, ale sam nie używasz B, dodaj tylko Ah

Ponadto, gdy zajmujemy się tym tematem, powinieneś dołączać inne pliki nagłówkowe do pliku nagłówkowego tylko wtedy, gdy faktycznie potrzebujesz go w nagłówku. Jeśli potrzebujesz go tylko w .cpp, umieść go tylko tam: jest to różnica między zależnością publiczną i prywatną i uniemożliwi użytkownikom twojej klasy przeciąganie nagłówków, których tak naprawdę nie potrzebują.

Nir Friedman
źródło
10

Zastanawiam się, czy powinienem jawnie dołączyć wszystkie nagłówki użyte bezpośrednio w określonym pliku

Tak.

Nigdy nie wiadomo, kiedy te inne nagłówki mogą się zmienić. Na całym świecie sensowne jest umieszczanie w każdej jednostce tłumaczeniowej nagłówków, o których wiesz, że ta jednostka potrzebuje.

Posiadamy osłony nagłówków, aby zapewnić, że podwójne włączenie nie będzie szkodliwe.

Lekkość Wyścigi z Moniką
źródło
3

Opinie na ten temat różnią się, ale jestem zdania, że ​​każdy plik (czy to plik źródłowy c / cpp, czy plik nagłówka h / hpp) powinien mieć możliwość samodzielnej kompilacji lub analizy.

W związku z tym wszystkie pliki powinny # zawierać wszystkie potrzebne pliki nagłówkowe - nie należy zakładać, że jeden plik nagłówkowy został już wcześniej dołączony.

To prawdziwy ból, jeśli musisz dodać plik nagłówka i stwierdzić, że używa on elementu zdefiniowanego gdzie indziej, bez bezpośredniego dołączania go ... więc musisz znaleźć (i ewentualnie skończyć z niewłaściwym!)

Z drugiej strony (co do zasady) nie ma znaczenia, jeśli # dołączasz plik, którego nie potrzebujesz ...


Jako osobisty styl układam pliki #include w kolejności alfabetycznej, z podziałem na system i aplikację - pomaga to wzmocnić komunikat „samodzielny i w pełni spójny”.

Andrzej
źródło
Uwaga na temat kolejności dołączeń: czasami kolejność jest ważna, na przykład przy dołączaniu nagłówków X11. Może to wynikać z projektu (który w tym przypadku można uznać za zły projekt), czasem z powodu niefortunnych problemów z niekompatybilnością.
hyde
Uwaga na temat dołączania niepotrzebnych nagłówków, ma to znaczenie dla czasów kompilacji, najpierw bezpośrednio (zwłaszcza jeśli jest to C ++ z dużym szablonem), ale szczególnie, gdy dołącza nagłówki tego samego lub projektu zależności, w którym plik dołączania również się zmienia, i uruchomi rekompilację wszystko w tym (jeśli masz działające zależności, jeśli nie masz, musisz cały czas robić czystą kompilację ...).
hyde
2

Zależy to od tego, czy to przejście przechodnie jest z konieczności (np. Klasa bazowa), czy ze względu na szczegóły implementacji (członek prywatny).

Aby to wyjaśnić, dołączanie przechodnie jest konieczne, gdy można je usunąć dopiero po pierwszej zmianie interfejsów zadeklarowanych w nagłówku pośrednim. Ponieważ jest to już przełomowa zmiana, każdy plik .cpp, który go używa, musi zostać sprawdzony.

Przykład: Ah jest uwzględnione przez Bh, który jest używany przez C.cpp. Jeśli Bh użył Ah dla pewnych szczegółów implementacyjnych, C.cpp nie powinien zakładać, że Bh będzie nadal to robić. Ale jeśli Bh używa Ah dla klasy podstawowej, C.cpp może założyć, że Bh będzie nadal zawierać odpowiednie nagłówki dla swoich klas podstawowych.

Widzisz tutaj faktyczną zaletę NIE powielania inkluzji nagłówka. Powiedz, że klasa podstawowa używana przez Bh tak naprawdę nie należała do Ah i została przekształcona w samą Bh. Bh jest teraz samodzielnym nagłówkiem. Jeśli C.cpp zawiera nadmiarowo Ah, teraz zawiera niepotrzebny nagłówek.

MSalters
źródło
2

Może być inny przypadek: masz Ah, Bh i twoje C.cpp, Bh obejmuje Ah

więc w C.cpp możesz pisać

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Więc jeśli nie napiszesz tutaj #include „Ah”, co może się zdarzyć? w twoim C.cpp używane są zarówno A, jak i B (np. klasa). Później zmieniłeś kod C.cpp, usunąłeś rzeczy związane z B, ale pozostawiając Bh włączone.

Jeśli uwzględnisz zarówno Ah, jak i Bh, a teraz w tym momencie narzędzia wykrywające niepotrzebne dołączenia mogą pomóc ci wskazać, że uwzględnienie Bh nie jest już potrzebne. Jeśli podasz Bh tylko tak jak powyżej, wtedy ciężko jest narzędziom / ludziom wykryć niepotrzebne dołączenia po zmianie kodu.

król błędów
źródło
1

Przyjmuję podobne nieco inne podejście niż proponowane odpowiedzi.

W nagłówkach zawsze uwzględniaj tylko minimum, tylko to, co jest potrzebne, aby kompilacja przebiegła pomyślnie. W miarę możliwości używaj deklaracji przesyłania dalej.

W plikach źródłowych nie ma znaczenia, ile zawierasz. Moje preferencje wciąż muszą zawierać minimum, aby je spełnić.

W przypadku małych projektów, w tym nagłówków tu i tam, nie będzie to miało znaczenia. Ale w przypadku średnich i dużych projektów może to stanowić problem. Nawet jeśli do kompilacji używany jest najnowszy sprzęt, różnica może być zauważalna. Powodem jest to, że kompilator nadal musi otworzyć dołączony nagłówek i parsować go. Tak więc, aby zoptymalizować kompilację, zastosuj powyższą technikę (uwzględnij minimum i użyj deklaracji do przodu).

Chociaż jest to trochę nieaktualne, projekt oprogramowania w dużej skali C ++ (autor: John Lakos) wyjaśnia to wszystko szczegółowo.

BЈовић
źródło
1
Nie zgadzaj się z tą strategią ... jeśli umieścisz plik nagłówkowy w pliku źródłowym, musisz wyśledzić wszystkie jego zależności. Lepiej jest dołączyć bezpośrednio, niż spróbować udokumentować listę!
Andrew,
@Andrew ma narzędzia i skrypty do sprawdzania, co i ile razy jest w zestawie.
BЈовић
1
Zauważyłem optymalizację niektórych najnowszych kompilatorów, aby sobie z tym poradzić. Rozpoznają typowe oświadczenie straży i przetwarzają je. Następnie, # włączając to ponownie, mogą całkowicie zoptymalizować ładowanie pliku. Jednak zalecenie dotyczące deklaracji forward jest bardzo rozsądne, aby zmniejszyć liczbę uwzględnień. Gdy zaczniesz używać deklaracji przesyłania dalej, staje się to równowagą między czasem działania kompilatora (poprawionym dzięki deklaracjom przesyłania dalej) i przyjaznością dla użytkownika (poprawionym dzięki dodatkowym dodatkowym #include), który stanowi równowagę dla każdej firmy inaczej.
Cort Ammon,
1
@CortAmmon Typowy nagłówek zawiera zabezpieczenia, ale kompilator wciąż musi go otworzyć, a to jest powolna operacja
Bovo
4
@ BЈовић: W rzeczywistości nie. Wszystko, co muszą zrobić, to rozpoznać, że plik ma „typowe” osłony nagłówków i oflagować je, aby otwierało się tylko raz. Na przykład Gcc ma dokumentację dotyczącą tego, kiedy i gdzie stosuje tę optymalizację: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon
-4

Dobrą praktyką jest nie martwić się o strategię nagłówka, o ile się kompiluje.

Sekcja nagłówka twojego kodu jest tylko blokiem linii, na który nikt nie powinien nawet patrzeć, dopóki nie pojawi się łatwy do rozwiązania błąd kompilacji. Rozumiem pragnienie „poprawnego” stylu, ale tak naprawdę żadnego z tych sposobów nie można opisać jako poprawnego. Dołączenie nagłówka dla każdej klasy może powodować irytujące błędy kompilacji oparte na rozkazie, ale te błędy kompilacji odzwierciedlają również problemy, które może rozwiązać staranne kodowanie (choć prawdopodobnie nie warto tego naprawiać).

I tak, będą mieć te problemy zamówień opartych na raz zaczną się do friendziemi.

Możesz pomyśleć o problemie w dwóch przypadkach.


Przypadek 1: Masz niewielką liczbę klas oddziałujących ze sobą, powiedzmy mniej niż tuzin. Regularnie dodajesz te nagłówki, usuwasz je i w inny sposób modyfikujesz, w sposób, który może wpływać na ich wzajemne zależności. Jest to przypadek, który sugeruje twój przykład kodu.

Zestaw nagłówków jest na tyle mały, że rozwiązywanie problemów, które się pojawiają, nie jest skomplikowane. Wszelkie trudne problemy można rozwiązać, przepisując jeden lub dwa nagłówki. Martwienie się o strategię nagłówka polega na rozwiązywaniu problemów, które nie istnieją.


Przypadek 2: Masz dziesiątki klas. Niektóre klasy stanowią kręgosłup twojego programu, a przepisanie ich nagłówków zmusiłoby cię do przepisania / rekompilacji dużej części twojej bazy kodu. Inne klasy używają tego kręgosłupa do osiągania różnych celów. Jest to typowe ustawienie biznesowe. Nagłówki są rozmieszczone w katalogach i nie można realistycznie zapamiętać nazw wszystkiego.

Rozwiązanie: W tym momencie musisz pomyśleć o swoich klasach w logicznych grupach i zebrać te grupy w nagłówki, które powstrzymają cię od ciągłego #includepowtarzania. To nie tylko ułatwia życie, ale jest także niezbędnym krokiem do korzystania ze wstępnie skompilowanych nagłówków .

Skończyć #includeing zajęcia nie są potrzebne, ale kogo to obchodzi ?

W takim przypadku Twój kod wyglądałby jak ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
Pytanie C.
źródło
13
Musiałem to -1, ponieważ szczerze wierzę, że każde zdanie w formie „Dobra praktyka to nie martwić się o swoją strategię, dopóki się kompiluje”, prowadzi ludzi do złej oceny. Przekonałem się, że podejście bardzo szybko prowadzi do nieczytelności, a nieczytelność jest PRAWIE tak zła, jak „nie działa”. Znalazłem również wiele głównych bibliotek, które nie zgadzają się z wynikami z obu przypadków, które opisujesz. Jako przykład, Boost DOES wykonuje nagłówki „kolekcje”, które polecasz w przypadku 2, ale robią też dużą różnicę, zapewniając nagłówki klasa po klasie, kiedy ich potrzebujesz.
Cort Ammon,
3
Osobiście byłem świadkiem: „nie martw się, jeśli się skompiluje”, zamień się w „nasza aplikacja kompiluje się po 30 minutach, kiedy dodajesz wartość do wyliczenia, jak do diabła to naprawimy !?”
Gort the Robot
W swojej odpowiedzi podniosłem kwestię czasu kompilacji. W rzeczywistości moja odpowiedź jest jedną z dwóch (żadna z nich nie uzyskała dobrego wyniku), która mi odpowiada. Ale tak naprawdę jest to styczne do pytania OP; to jest „Czy powinienem nazywać wielbłąda moje nazwy zmiennych?” wpisz pytanie. Zdaję sobie sprawę, że moja odpowiedź jest niepopularna, ale nie zawsze jest najlepsza praktyka we wszystkim i jest to jeden z takich przypadków.
Pytanie C,
Zgadzam się z numerem 2. Co do wcześniejszych pomysłów - mam nadzieję na automatyzację, która zaktualizowałaby lokalny blok nagłówka - do tego czasu zalecam pełną listę.
chux - Przywróć Monikę
Podejście „włącz wszystko i zlew kuchenny” może początkowo zaoszczędzić trochę czasu - Twoje pliki nagłówkowe mogą nawet wyglądać na mniejsze (ponieważ większość rzeczy jest zawarta pośrednio z… gdzieś). Aż do momentu, gdy jakakolwiek zmiana w dowolnym miejscu spowoduje ponad 30-minutową rekompilację projektu. A twoje autouzupełnianie IDE-smart wyświetla setki nieistotnych sugestii. I przypadkowo mieszasz dwie zbyt podobnie nazwane klasy lub funkcje statyczne. I dodajesz nową strukturę, ale kompilacja kończy się niepowodzeniem, ponieważ gdzieś kolizja przestrzeni nazw z całkowicie niezwiązaną klasą ...
CharonX