Często znajduję się w sytuacji, w której napotykam wiele błędów kompilacji / linkera w projekcie C ++ z powodu złych decyzji projektowych (dokonanych przez kogoś innego :)), które prowadzą do cyklicznych zależności między klasami C ++ w różnych plikach nagłówkowych (może się zdarzyć również w tym samym pliku) . Ale na szczęście (?) To nie zdarza się wystarczająco często, abym pamiętał rozwiązanie tego problemu, kiedy następnym razem to się powtórzy.
Dlatego w celu łatwego przywołania w przyszłości zamierzam opublikować reprezentatywny problem wraz z rozwiązaniem. Lepsze rozwiązania są oczywiście mile widziane.
A.h
class B; class A { int _val; B *_b; public: A(int val) :_val(val) { } void SetB(B *b) { _b = b; _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B' } void Print() { cout<<"Type:A val="<<_val<<endl; } };
B.h
#include "A.h" class B { double _val; A* _a; public: B(double val) :_val(val) { } void SetA(A *a) { _a = a; _a->Print(); } void Print() { cout<<"Type:B val="<<_val<<endl; } };
main.cpp
#include "B.h" #include <iostream> int main(int argc, char* argv[]) { A a(10); B b(3.14); a.Print(); a.SetB(&b); b.Print(); b.SetA(&a); return 0; }
Odpowiedzi:
Sposób myślenia na ten temat to „myśleć jak kompilator”.
Wyobraź sobie, że piszesz kompilator. I widzisz taki kod.
Podczas kompilowania pliku .cc (pamiętaj, że .cc, a nie .h jest jednostką kompilacji), musisz przydzielić miejsce dla obiektu
A
. A więc ile miejsca? Wystarczy do przechowywaniaB
! Jaki jest wtedy rozmiarB
? Wystarczająco do przechowywaniaA
! UpsWyraźnie okrągłe odniesienie, które musisz złamać.
Możesz go złamać, pozwalając kompilatorowi zamiast tego zarezerwować tyle miejsca, ile wie na temat początkowych - na przykład wskaźniki i referencje zawsze będą miały 32 lub 64 bity (w zależności od architektury), więc jeśli zastąpisz (jedno) przez wskaźnik lub odniesienie, byłoby świetnie. Powiedzmy, że zastępujemy
A
:Teraz jest lepiej. Nieco.
main()
wciąż mówi:#include
, dla wszystkich zakresów i celów (jeśli wyjmiesz preprocesor), po prostu skopiuj plik do .cc . Tak naprawdę .cc wygląda następująco:Możesz zobaczyć, dlaczego kompilator nie może sobie z tym poradzić - nie ma pojęcia co
B
jest - nigdy wcześniej nie widział tego symbolu.Powiedzmy więc kompilatorowi
B
. Jest to znane jako deklaracja forward i jest omówione w dalszej części tej odpowiedzi .To działa . To nie jest świetne . Ale w tym momencie powinieneś zrozumieć problem z okrągłym odniesieniem i to, co zrobiliśmy, aby go „naprawić”, chociaż naprawa jest zła.
Przyczyną tego, że ta poprawka jest zła, jest to, że następna osoba
#include "A.h"
musi zadeklarować,B
zanim będzie mogła z niej skorzystać i otrzyma straszny#include
błąd. Przejdźmy więc do deklaracji samego Ah .A w Bh , w tym momencie możesz po prostu
#include "A.h"
bezpośrednio.HTH.
źródło
Można uniknąć błędów kompilacji, usuwając definicje metod z plików nagłówkowych i pozwalając, aby klasy zawierały tylko deklaracje metod i deklaracje / definicje zmiennych. Definicje metod powinny być umieszczone w pliku .cpp (tak jak mówi wytyczna najlepszych praktyk).
Wadą następującego rozwiązania jest (zakładając, że umieściłeś metody w pliku nagłówkowym, aby je wstawić), że metody nie są już uwzględniane przez kompilator, a próba użycia słowa kluczowego wbudowanego powoduje błędy linkera.
źródło
Spóźniam się z odpowiedzią na to pytanie, ale do tej pory nie ma jednej rozsądnej odpowiedzi, mimo że jestem popularnym pytaniem z bardzo pozytywnymi odpowiedziami ...
Najlepsza praktyka: nagłówki deklaracji forward
Jak zilustrowano w
<iosfwd>
nagłówku biblioteki standardowej , właściwym sposobem dostarczania deklaracji przesyłania dalej dla innych jest posiadanie nagłówka deklaracji przesyłania dalej . Na przykład:a.fwd.h:
ah:
b.fwd.h:
bh:
Opiekunowie
A
iB
bibliotek powinni ponosić odpowiedzialność za synchronizację nagłówków deklaracji przekazywania z nagłówkami i plikami implementacyjnymi, więc - na przykład - jeśli pojawi się opiekun „B” i przepisuje kod, aby ...b.fwd.h:
bh:
... następnie rekompilacja kodu dla „A” zostanie uruchomiona przez zmiany w dołączonym
b.fwd.h
i powinna zakończyć się czysto.Słaba, ale powszechna praktyka: forward deklaruje rzeczy w innych bibliotekach
Powiedz - zamiast używać nagłówka deklaracji przesyłania, jak wyjaśniono powyżej - kod w
a.h
luba.cc
zamiast tego deklarujeclass B;
się:a.h
luba.cc
zawierałb.h
później:B
(tj. powyższa zmiana w B złamała A i innych klientów nadużywających deklaracji forward, zamiast działać w sposób przejrzysty).b.h
- możliwe, jeśli A tylko przechowuje / omija Bs przez wskaźnik i / lub odniesienie)#include
analizie i zmienionych znacznikach czasu pliku nie odbudują sięA
(i jego zależnego kodu) po zmianie na B, powodując błędy w czasie łącza lub w czasie wykonywania. Jeśli B jest dystrybuowane jako biblioteka DLL załadowana w środowisku wykonawczym, kod w „A” może nie znaleźć różnych zniekształconych symboli w środowisku wykonawczym, które mogą, ale nie muszą być obsługiwane wystarczająco dobrze, aby uruchomić uporządkowane zamykanie lub akceptowalnie zmniejszoną funkcjonalność.Jeśli kod A ma specjalizacje / „cechy” szablonów dla starych
B
, nie będą obowiązywać.źródło
a.fwd.h
wa.h
, aby zapewnić, że pobyt w synchronizacji. W przypadku użycia tych klas brakuje kodu przykładowego.a.h
ib.h
oba będą musiały zostać uwzględnione, ponieważ nie będą działać w izolacji: `` //main.cpp #include "ah" #include "bh" int main () {...} `` Lub jeden z nich musi być w pełni uwzględniony w drugim, tak jak w pytaniu otwierającym. Gdzieb.h
obejmujea.h
imain.cpp
obejmujeb.h
main.cpp
, ale miło, że udokumentowałeś, co powinien zawierać Twój komentarz. Na zdrowie<iosfwd>
to klasyczny przykład: może istnieć kilka obiektów strumieniowych, do których można odwoływać się z wielu miejsc, i<iostream>
jest wiele do uwzględnienia.#include
s).Rzeczy do zapamiętania:
class A
ma obiektclass B
jako członek lub odwrotnie.Przeczytaj FAQ:
źródło
Kiedyś rozwiązałem ten problem, przenosząc wszystkie wstawki po definicji klasy i umieszczając pozostałe
#include
dla tuż przed wstawieniami w pliku nagłówkowym. W ten sposób należy się upewnić, że wszystkie definicje + wstawki są ustawione przed parsowaniem wstawek.W ten sposób można nadal mieć kilka wstawek w obu (lub wielu) plikach nagłówka. Ale trzeba mieć strażników .
Lubię to
... i robiąc to samo w
B.h
źródło
B.h
pierwszy?Kiedyś napisałem o tym post: Rozwiązywanie zależności cyklicznych w c ++
Podstawową techniką jest rozdzielenie klas za pomocą interfejsów. Więc w twoim przypadku:
źródło
virtual
ma wpływ na wydajność środowiska wykonawczego.Oto rozwiązanie dla szablonów: Jak obsługiwać zależności cykliczne za pomocą szablonów
Kluczem do rozwiązania tego problemu jest zadeklarowanie obu klas przed podaniem definicji (implementacji). Nie można podzielić deklaracji i definicji na osobne pliki, ale można je uporządkować tak, jakby były w osobnych plikach.
źródło
Prosty przykład przedstawiony na Wikipedii zadziałał dla mnie. (pełny opis można przeczytać na stronie http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )
Plik „a.h”:
Plik „b.h”:
Plik 'main.cpp':
źródło
Niestety we wszystkich poprzednich odpowiedziach brakuje niektórych szczegółów. Właściwe rozwiązanie jest nieco kłopotliwe, ale jest to jedyny sposób, aby zrobić to poprawnie. I łatwo się skaluje, obsługuje również bardziej złożone zależności.
Oto jak możesz to zrobić, dokładnie zachowując wszystkie szczegóły i użyteczność:
A
iB
mogą uwzględniać Ah i Bh w dowolnej kolejnościUtwórz dwa pliki, A_def.h, B_def.h. Będą one zawierać tylko
A
„s iB
” s definicję:A potem Ah i Bh będą zawierały to:
Pamiętaj, że A_def.h i B_def.h są „prywatnymi” nagłówkami, użytkownikami
A
iB
nie powinny ich używać. Nagłówek publiczny to Ah i Bhźródło
A
„SB
” s definicja jest dostępna, deklaracja nie wystarczy do przodu).W niektórych przypadkach możliwe jest zdefiniowanie metody lub konstruktora klasy B w pliku nagłówkowym klasy A w celu rozwiązania zależności cyklicznych obejmujących definicje. W ten sposób można uniknąć konieczności umieszczania definicji w
.cc
plikach, na przykład jeśli chcesz zaimplementować bibliotekę tylko z nagłówkami.źródło
Niestety nie mogę skomentować odpowiedzi od geza.
Nie mówi tylko „przedkładaj deklaracje w osobnym nagłówku”. Mówi, że musisz rozlać nagłówki definicji klas i wbudowane definicje funkcji do różnych plików nagłówkowych, aby umożliwić „odroczone zależności”.
Ale jego ilustracja nie jest naprawdę dobra. Ponieważ obie klasy (A i B) potrzebują tylko niepełnego typu (pola / parametry wskaźnika).
Aby to lepiej zrozumieć, wyobraź sobie, że klasa A ma pole typu B, a nie B *. Ponadto klasy A i B chcą zdefiniować funkcję wbudowaną z parametrami innego typu:
Ten prosty kod nie działałby:
Powoduje to następujący kod:
Ten kod nie kompiluje się, ponieważ B :: Do potrzebuje pełnego typu A, który zostanie zdefiniowany później.
Aby upewnić się, że się kompiluje, kod źródłowy powinien wyglądać następująco:
Jest to dokładnie możliwe w przypadku tych dwóch plików nagłówkowych dla każdej klasy, która musi zdefiniować funkcje wbudowane. Jedynym problemem jest to, że klasy kołowe nie mogą po prostu zawierać „nagłówka publicznego”.
Aby rozwiązać ten problem, chciałbym zaproponować rozszerzenie preprocesora:
#pragma process_pending_includes
Niniejsza dyrektywa powinna odroczyć przetwarzanie bieżącego pliku i zakończyć wszystkie oczekujące dołączenia.
źródło