Rozwiązuj błędy kompilacji wynikające z okrągłej zależności między klasami


353

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;
    }

23
Podczas pracy z Visual Studio flaga / showInclude bardzo pomaga w debugowaniu tego rodzaju problemów.
wyczyść

Odpowiedzi:


288

Sposób myślenia na ten temat to „myśleć jak kompilator”.

Wyobraź sobie, że piszesz kompilator. I widzisz taki kod.

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

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 przechowywania B! Jaki jest wtedy rozmiar B? Wystarczająco do przechowywaniaA ! Ups

Wyraź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:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

Teraz jest lepiej. Nieco. main()wciąż mówi:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#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:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

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 .

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

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ć, Bzanim będzie mogła z niej skorzystać i otrzyma straszny #includebłąd. Przejdźmy więc do deklaracji samego Ah .

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

A w Bh , w tym momencie możesz po prostu#include "A.h" bezpośrednio.

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

HTH.


20
„Mówienie kompilatorowi o B” znane jest jako przekazanie dalej przez B.
Peter Ajtai

8
O mój Boże! całkowicie pominął fakt, że odniesienia są znane pod względem zajmowanej przestrzeni. Wreszcie mogę teraz poprawnie zaprojektować!
kellogs,

47
Ale nadal nie możesz użyć żadnej funkcji na B (jak w pytaniu _b-> Printt ())
ranga 1

3
To jest problem, który mam. Jak wprowadzić funkcje z deklaracją przekazywania bez całkowitego przepisywania pliku nagłówka?
sydan


101

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.

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

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;
}

Dzięki. To z łatwością rozwiązało problem. Po prostu przeniosłem okólniki do plików .cpp.
Lenar Hoyt

3
Co jeśli masz metodę szablonów? Wtedy nie można tak naprawdę przenieść go do pliku CPP, chyba że ręcznie utworzysz szablony.
Malcolm

Zawsze dołączasz razem „Ah” i „Bh”. Dlaczego nie umieścisz „Ah” w „Bh”, a następnie umieścisz tylko „Bh” zarówno w „A.cpp”, jak i „B.cpp”?
Gusev Slava

28

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:

#pragma once
class A;

ah:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

bh:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

Opiekunowie AiB 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:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

bh:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

... następnie rekompilacja kodu dla „A” zostanie uruchomiona przez zmiany w dołączonym b.fwd.hi 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.hlub a.cczamiast tego deklaruje class B;się:

  • jeśli a.hlub a.cczawierał b.hpóźniej:
    • kompilacja A zakończy się z błędem, gdy dojdzie do sprzecznej deklaracji / definicji 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).
  • w przeciwnym razie (jeśli A ostatecznie nie uwzględnił b.h- możliwe, jeśli A tylko przechowuje / omija Bs przez wskaźnik i / lub odniesienie)
    • narzędzia do budowania oparte na #includeanalizie 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ć.


2
Jest to naprawdę czysty sposób obsługi deklaracji przesyłania dalej. Jedyną „wadą” byłyby dodatkowe pliki. Zakładam, że zawsze zawierają a.fwd.hw a.h, aby zapewnić, że pobyt w synchronizacji. W przypadku użycia tych klas brakuje kodu przykładowego. a.hi b.hoba 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. Gdzie b.hobejmuje a.hi main.cppobejmujeb.h
Farway

2
@ Farway Prawo pod każdym względem. Nie zawracałem sobie głowy pokazywaniem main.cpp, ale miło, że udokumentowałeś, co powinien zawierać Twój komentarz. Na zdrowie
Tony Delroy

1
Jedna z lepszych odpowiedzi, z ładnym szczegółowym wyjaśnieniem, dlaczego nie ma i nie działa z powodu zalet i wad ...
Francis Cugler

1
@RezaHajianpour: sensownie jest mieć nagłówek deklaracji przekazywania dla wszystkich klas, dla których chcesz przekazywać deklaracje, cykliczne lub nie. To powiedziawszy, będziesz ich potrzebować tylko wtedy, gdy: 1) włączenie rzeczywistej deklaracji jest (lub można oczekiwać, że stanie się później) kosztowne (np. Zawiera wiele nagłówków, których jednostka tłumacząca nie potrzebowałaby w innym przypadku), oraz 2) kod klienta jest prawdopodobnie będą w stanie korzystać ze wskaźników lub odniesień do obiektów. <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.
Tony Delroy

1
@RezaHajianpour: Myślę, że masz dobry pomysł, ale z twoim stwierdzeniem jest problem terminologiczny: „potrzebujemy tylko typu, który ma zostać zadeklarowany ” byłby odpowiedni. Deklarowany typ oznacza, że ​​wyświetlono deklarację forward; to zdefiniowane raz pełna definicja została analizowana (i które mogą potrzebować więcej #includes).
Tony Delroy

20

Rzeczy do zapamiętania:

  • To nie zadziała, jeśli class Ama obiektclass B jako członek lub odwrotnie.
  • Przekazywanie deklaracji jest już gotowe.
  • Kolejność deklaracji ma znaczenie (dlatego przenosisz definicje).
    • Jeśli obie klasy wywołują funkcje drugiej, musisz przenieść definicje.

Przeczytaj FAQ:


1
linki, które podałeś, już nie działają, czy znasz nowe, do których można się odwoływać?
Ramya Rao

11

Kiedyś rozwiązałem ten problem, przenosząc wszystkie wstawki po definicji klasy i umieszczając pozostałe #includedla 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

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

... i robiąc to samo w B.h


Dlaczego? Myślę, że to eleganckie rozwiązanie podstępnego problemu ... kiedy ktoś chce inline. Jeśli ktoś nie chce inline, nie powinien był pisać kodu tak, jak został napisany od samego początku ...
Epatel

Co się stanie, jeśli użytkownik doda B.hpierwszy?
Pan Fooz

3
Pamiętaj, że twój nagłówek używa zarezerwowanego identyfikatora, wszystko z podwójnymi sąsiadującymi znakami podkreślenia jest zarezerwowane.
Lars Viklund,

6

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:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

    A(int val)
        :_val(val)
    {
    }

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

    void Print()
    {
        cout<<"Type:A val="<<_val<<endl;
    }
};

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

    B(double val)
        :_val(val)
    {
    }

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

    void Print()
    {
        cout<<"Type:B val="<<_val<<endl;
    }
};

//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"

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;
}

2
Należy pamiętać, że użycie interfejsów i virtualma wpływ na wydajność środowiska wykonawczego.
cemper93


2

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”:

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

Plik „b.h”:

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

Plik 'main.cpp':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

1

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ść:

  • rozwiązanie jest dokładnie takie samo, jak pierwotnie planowano
  • funkcje inline wciąż inline
  • użytkownicy Ai Bmogą uwzględniać Ah i Bh w dowolnej kolejności

Utwórz dwa pliki, A_def.h, B_def.h. Będą one zawierać tylko A„s i B” s definicję:

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

A potem Ah i Bh będą zawierały to:

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

Pamiętaj, że A_def.h i B_def.h są „prywatnymi” nagłówkami, użytkownikami Ai Bnie powinny ich używać. Nagłówek publiczny to Ah i Bh


1
Czy ma to jakąś przewagę nad rozwiązaniem Tony'ego Delroya ? Oba są oparte na nagłówkach „pomocników”, ale Tony są mniejsze (zawierają tylko deklarację przekazywania) i wydają się działać w ten sam sposób (przynajmniej na pierwszy rzut oka).
Fabio mówi Przywróć Monikę

1
Ta odpowiedź nie rozwiązuje pierwotnego problemu. Mówi tylko „przedkładaj deklaracje w osobnym nagłówku”. Nic o rozwiązywaniu zależność cykliczna (kwestia wymaga rozwiązania, gdzie A„S B” s definicja jest dostępna, deklaracja nie wystarczy do przodu).
geza

0

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 .ccplikach, na przykład jeśli chcesz zaimplementować bibliotekę tylko z nagłówkami.

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}

0

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:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

Powoduje to następujący kod:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

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:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

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.

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.