477 Stimmen

Behebung von Build-Fehlern aufgrund von zirkulären Abhängigkeiten zwischen Klassen

Ich finde mich oft in einer Situation wieder, in der ich mit mehreren Kompilierungs-/Linkerfehlern in einem C++-Projekt konfrontiert bin, die auf einige schlechte Designentscheidungen zurückzuführen sind (die von jemand anderem getroffen wurden :) ), die zu zirkulären Abhängigkeiten zwischen C++-Klassen in verschiedenen Header-Dateien führen (kann auch in derselben Datei vorkommen) . Aber zum Glück(?) passiert das nicht so oft, dass ich mir die Lösung für dieses Problem merken kann, wenn es das nächste Mal wieder auftritt.

Damit Sie sich in Zukunft leicht daran erinnern können, werde ich ein repräsentatives Problem und die dazugehörige Lösung veröffentlichen. Bessere Lösungen sind natürlich willkommen.


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

8voto

Eduard Wirch Punkte 9548

Ich habe schon einmal einen Beitrag darüber geschrieben: Auflösen von zirkulären Abhängigkeiten in C++

Die grundlegende Technik besteht darin, die Klassen über Schnittstellen zu entkoppeln. In Ihrem Fall also:

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

8voto

Tatyana Punkte 81

Hier ist die Lösung für Vorlagen: Umgang mit zirkulären Abhängigkeiten mit Vorlagen

Der Schlüssel zur Lösung dieses Problems besteht darin, beide Klassen zu deklarieren, bevor die Definitionen (Implementierungen) bereitgestellt werden. Es ist nicht möglich, die Deklaration und die Definition in separate Dateien aufzuteilen, aber Sie können sie so strukturieren, als ob sie in separaten Dateien wären.

4voto

madx Punkte 6169

Das einfache Beispiel auf Wikipedia hat bei mir funktioniert. (Sie können die vollständige Beschreibung lesen unter http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )

Datei '''a.h''':

#ifndef A_H
#define A_H

class B;    //forward declaration

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

Datei '''b.h''':

#ifndef B_H
#define B_H

class A;    //forward declaration

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

Datei '''main.cpp''':

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

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

3voto

geza Punkte 27443

Leider fehlen bei allen bisherigen Antworten einige Details. Die korrekte Lösung ist ein wenig umständlich, aber das ist die einzige Möglichkeit, es richtig zu machen. Und sie lässt sich leicht skalieren und bewältigt auch komplexere Abhängigkeiten.

Hier erfahren Sie, wie Sie dies tun können, wobei alle Details und die Benutzerfreundlichkeit genau beibehalten werden:

  • die Lösung ist genau so, wie sie ursprünglich gedacht war
  • Inline-Funktionen weiterhin inline
  • Nutzer von A y B kann A.h und B.h in beliebiger Reihenfolge enthalten

Erstellen Sie zwei Dateien, A_def.h und B_def.h. Diese enthalten nur A und B Definition:

// 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

Und dann werden A.h und B.h dies enthalten:

// 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

Beachten Sie, dass A_def.h und B_def.h "private" Header sind, Benutzer von A y B sollten sie nicht verwenden. Der öffentliche Header ist A.h und B.h.

1voto

Bernd Punkte 1949

Leider kann ich die Antwort von geza nicht kommentieren.

Er sagt nicht nur: "Legen Sie Forward-Deklarationen in eine separate Kopfzeile". Er sagt, dass man Klassendefinitions-Header und Inline-Funktionsdefinitionen in verschiedene Header-Dateien aufteilen muss, um "aufgeschobene Abhängigkeiten" zu ermöglichen.

Aber seine Illustration ist nicht wirklich gut. Denn beide Klassen (A und B) benötigen nur einen unvollständigen Typ des jeweils anderen (Zeigerfelder/Parameter).

Um es besser zu verstehen, stellen Sie sich vor, dass die Klasse A ein Feld vom Typ B und nicht B* hat. Außerdem wollen die Klassen A und B eine Inline-Funktion mit Parametern des jeweils anderen Typs definieren:

Dieser einfache Code würde nicht funktionieren:

// 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"

Dies würde zu folgendem Code führen:

//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"

Dieser Code lässt sich nicht kompilieren, weil B::Do einen vollständigen Typ von A benötigt, der später definiert wird.

Um sicherzustellen, dass er kompiliert werden kann, sollte der Quellcode wie folgt aussehen:

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

Genau das ist mit diesen beiden Header-Dateien für jede Klasse möglich, die Inline-Funktionen definieren muss. Das einzige Problem ist, dass die kreisförmigen Klassen nicht einfach den "öffentlichen Header" enthalten können.

Um dieses Problem zu lösen, möchte ich eine Erweiterung des Präprozessors vorschlagen: #pragma process_pending_includes

Diese Anweisung sollte die Verarbeitung der aktuellen Datei zurückstellen und alle anstehenden Includes abschließen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X