Zagwarantowanie, że nagłówki są wyraźnie zawarte w pliku CPP

9

Myślę, że ogólnie dobrą praktyką jest #includestosowanie nagłówka dla wszystkich typów używanych w pliku CPP, niezależnie od tego, co jest już zawarte w pliku HPP. Więc mogę na przykład #include <string>zarówno w HPP, jak i CPP, mimo że nadal mógłbym kompilować, jeśli pominąłem go w CPP. W ten sposób nie muszę się martwić, czy mój HPP użył deklaracji forward, czy nie.

Czy są jakieś narzędzia, które mogą wymusić ten #includestyl kodowania? Czy powinienem egzekwować ten styl kodowania?

Ponieważ preprocesor / kompilator nie dba o to, czy #includepochodzi od HPP, czy CPP, nie otrzymuję żadnej informacji zwrotnej, jeśli zapomnę zastosować ten styl.

M. Dudley
źródło

Odpowiedzi:

7

Jest to jeden z tych rodzajów „raczej” niż „powinien” standardów kodowania. Powodem jest to, że musiałbyś napisać parser C ++, aby go wymusić.

Bardzo powszechną zasadą dla plików nagłówkowych jest to, że muszą one stać same. Plik nagłówkowy nie może wymagać, aby niektóre inne pliki nagłówkowe były # zawarte przed dołączeniem danego nagłówka. Jest to wymaganie do przetestowania. Biorąc pod uwagę jakiś losowy nagłówek foo.hh, następujące powinny się skompilować i uruchomić:

#include "foo.hh"
int main () {
   return 0;
}

Ta reguła ma wpływ na użycie innych klas w niektórych nagłówkach. Czasami tych konsekwencji można uniknąć, zgłaszając dalej te inne klasy. Nie jest to możliwe w przypadku wielu standardowych klas bibliotek. Nie ma możliwości przekazania dalej deklaracji wystąpienia szablonu, takiego jak std::stringlub std::vector<SomeType>. Musisz do #includetych nagłówków STL w nagłówku, nawet jeśli jedynym zastosowaniem tego typu jest argument funkcji.

Innym problemem są rzeczy, które przypadkowo wciągasz. Przykład: rozważ następujące kwestie:

plik foo.cc:

#include "foo.hh"
#include "bar.hh"

void Foo::Foo () : bar() { /* body elided */ }

void Foo::do_something (int item) {
   ...
   bar.add_item (item);
   ...
}

Oto barelement Foodanych klasy, który jest typu Bar. Zrobiłeś to dobrze i masz #included bar.hh, chociaż musiałoby to być uwzględnione w nagłówku definiującym klasę Foo. Nie uwzględniono jednak rzeczy używanych przez Bar::Bar()i Bar::add_item(int). Istnieje wiele przypadków, w których te połączenia mogą powodować dodatkowe odniesienia zewnętrzne.

Jeśli analizujesz foo.oza pomocą takiego narzędzia, jak nm, okaże się, że funkcje foo.ccwywołują wszelkiego rodzaju rzeczy, dla których nie zrobiłeś odpowiedniego #include. Czy powinieneś zatem dodać #includedyrektywy dotyczące tych przypadkowych odniesień zewnętrznych foo.cc? Odpowiedź absolutnie nie jest. Problem polega na tym, że bardzo trudno jest odróżnić funkcje wywoływane przypadkowo od funkcji wywoływanych bezpośrednio.

David Hammen
źródło
2

Czy powinienem egzekwować ten styl kodowania?

Prawdopodobnie nie. Moja reguła jest następująca: dołączenie pliku nagłówkowego nie może zależeć od kolejności.

Możesz to dość łatwo zweryfikować za pomocą prostej reguły, że pierwszym plikiem zawartym w xc jest xh

Kevin Cline
źródło
1
Czy możesz to rozwinąć? Nie rozumiem, jak to naprawdę zweryfikowałoby niezależność zamówień.
detly
1
tak naprawdę nie gwarantuje niezależności zamówienia - po prostu zapewnia, że #include "x.h"będzie działał bez konieczności uprzedniego #include. To wystarczy, jeśli nie będziesz nadużywać #define.
kevin cline
1
Rozumiem. Nadal więc dobry pomysł.
detly
2

Jeśli musisz egzekwować regułę, że określone pliki nagłówkowe muszą stać samodzielnie, możesz skorzystać z narzędzi, które już masz. Utwórz podstawowy plik makefile, który kompiluje każdy plik nagłówka osobno, ale nie generuje pliku obiektowego. Będziesz mógł określić, w którym trybie chcesz skompilować plik nagłówkowy (tryb C lub C ++) i sprawdzić, czy może on działać samodzielnie. Można przyjąć rozsądne założenie, że dane wyjściowe nie zawierają żadnych wyników fałszywie dodatnich, wszystkie wymagane zależności są deklarowane, a dane wyjściowe są dokładne.

Jeśli używasz IDE, nadal możesz to zrobić bez makefile (w zależności od IDE). Po prostu utwórz dodatkowy projekt, dodaj pliki nagłówkowe, które chcesz zweryfikować, i zmień ustawienia, aby skompilować je jako plik C lub C ++. Na przykład w MSVC zmieniłbyś ustawienie „Typ elementu” w „Właściwości konfiguracji-> Ogólne”.

Kapitan Obvlious
źródło
1

Nie sądzę, aby takie narzędzie istniało, ale byłbym szczęśliwy, gdyby inna odpowiedź mnie obaliła.

Problem z pisaniem takiego narzędzia polega na tym, że bardzo łatwo zgłasza on fałszywy wynik, dlatego szacuję przewagę netto takiego narzędzia na bliską zeru.

Jedynym sposobem, w jaki takie narzędzie mogłoby działać, jest zresetowanie tabeli symboli tylko do zawartości przetwarzanego pliku nagłówka, ale wtedy pojawia się problem polegający na tym, że nagłówki tworzące zewnętrzny interfejs API biblioteki delegują rzeczywiste deklaracje do wewnętrzne nagłówki.
Na przykład <string>w implementacji GCC libc ++ nic nie deklaruje, ale po prostu zawiera kilka wewnętrznych nagłówków, które zawierają rzeczywiste deklaracje. Gdyby narzędzie zresetowało tabelę symboli do tego, co zostało zadeklarowane przez <string>siebie, byłoby to niczym.
Możesz mieć narzędzie do rozróżnienia między #include ""i #include <>, ale to nie pomaga, jeśli biblioteka zewnętrzna wykorzystuje #include ""swoje wewnętrzne nagłówki do API.

Bart van Ingen Schenau
źródło
1

nie #Pragma onceosiąga tego? Możesz dołączyć coś tyle razy, ile chcesz, albo bezpośrednio, albo poprzez łańcuchowe dołączenia, i tak długo, jak jest #Pragma onceobok każdego z nich, nagłówek jest dołączany tylko raz.

Jeśli chodzi o wymuszanie go, być może mógłbyś stworzyć system kompilacji, który po prostu zawiera każdy nagłówek sam z jakąś sztuczną funkcją główną, aby upewnić się, że się kompiluje. #ifdefzawiera najlepsze wyniki dla tej metody testowania.

Ericson2314
źródło
1

Zawsze dołączaj pliki nagłówkowe do pliku CPP. To nie tylko znacznie skraca czas kompilacji, ale także oszczędza kłopotów, jeśli zdecydujesz się na prekompilowane nagłówki. Z mojego doświadczenia wynika, że ​​nawet robienie kłopotów z deklaracjami do przodu jest warte praktyki. Łam zasadę tylko wtedy, gdy jest to konieczne.

Gvozden
źródło
Czy możesz wyjaśnić, jak to „znacząco skróci czas kompilacji”?
Mawg mówi o przywróceniu Moniki
1
Zapewnia to, że każda jednostka kompilacji (plik CPP) pobiera tylko minimum plików dołączanych podczas kompilacji. W przeciwnym razie, jeśli umieścisz włączniki w plikach H, szybko powstają fałszywe łańcuchy zależności i w końcu wyciągniesz wszystkie dołączenia z każdą kompilacją.
Gvozden
Z włączonymi strażnikami, mogę zapytać „znacząco”, ale przypuszczam, że dostęp do dysku (w celu odkrycia tych trudnych problemów) jest „powolny”, więc odpuszczę. Dzięki za wyjaśnienie
Mawg mówi o przywróceniu Moniki
0

Powiedziałbym, że ta konwencja ma zarówno zalety, jak i wady. Z jednej strony dobrze jest wiedzieć dokładnie, co zawiera Twój plik .cpp. Z drugiej strony lista dołączeń może łatwo wzrosnąć do absurdalnych rozmiarów.

Jednym ze sposobów zachęcania do tej konwencji jest nie umieszczanie niczego we własnych nagłówkach, ale tylko w plikach .cpp. Wówczas żaden plik .cpp korzystający z nagłówka nie zostanie skompilowany, chyba że wyraźnie podasz wszystkie inne nagłówki, od których zależy.

Prawdopodobnie jest tu jakiś rozsądny kompromis. Na przykład możesz zdecydować, że możesz umieszczać standardowe nagłówki bibliotek we własnych nagłówkach, ale nie więcej.

Dima
źródło
3
Jeśli odejdziesz obejmuje nagłówki, wtedy kolejność dołączania zaczyna mieć znaczenie ... i zdecydowanie chcę tego uniknąć.
M. Dudley
1
-1: Tworzy koszmar zależności. Ulepszenie xh może wymagać zmiany KAŻDEGO pliku .cpp, który zawiera xh
kevin cline
1
@ M.Dudley, tak jak powiedziałem, są zalety i wady.
Dima,