webentwicklung-frage-antwort-db.com.de

Beheben von Erstellungsfehlern aufgrund von zirkulären Abhängigkeiten zwischen Klassen

Ich befinde mich oft in einer Situation, in der in einem C++ - Projekt mehrere Kompilierungs-/Linker-Fehler auftreten, die auf falsche Entwurfsentscheidungen zurückzuführen sind (die von einer anderen Person getroffen wurden :), die zu zirkulären Abhängigkeiten zwischen C++ - Klassen in verschiedenen Header-Dateien führen ( kann auch in der gleichen Datei vorkommen) . Aber zum Glück (?) Passiert das nicht oft genug, damit ich mich beim nächsten Mal an die Lösung dieses Problems erinnere.

Um mich in Zukunft leichter zurückrufen zu können, werde ich ein repräsentatives Problem und eine entsprechende 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;
    }
    
297
Autodidact

Die Art und Weise darüber nachzudenken ist "wie ein Compiler zu denken".

Stellen Sie sich vor, Sie schreiben einen Compiler. Und Sie sehen Code so.

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

Wenn Sie die Datei .cc kompilieren (denken Sie daran, dass .cc und nicht .h die Einheit für die Kompilierung ist), müssen Sie Platz für das Objekt A zuweisen. Nun, wie viel Platz dann? Genug um B zu speichern! Was ist dann die Größe von B? Genug um A zu speichern! Hoppla.

Es ist eindeutig ein Zirkelverweis, den Sie brechen müssen.

Sie können es brechen, indem Sie dem Compiler erlauben, stattdessen so viel Speicherplatz zu reservieren, wie er über Vorabinformationen weiß. Beispielsweise werden Zeiger und Referenzen immer 32 oder 64 Bit (je nach Architektur) sein Ein Hinweis oder ein Hinweis, die Dinge wären großartig. Nehmen wir an, wir ersetzen in A:

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

Jetzt sind die Dinge besser. Etwas. main() sagt immer noch:

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

#include für alle Bereiche und Zwecke (wenn Sie den Präprozessor herausnehmen) kopiert die Datei einfach in die Datei .cc. So sieht das .cc so aus:

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

Sie können sehen, warum der Compiler damit nicht umgehen kann - er hat keine Ahnung, was B ist - er hat das Symbol noch nie zuvor gesehen.

Lassen Sie uns den Compiler also über B informieren. Dies wird als forward-Deklaration bezeichnet und wird weiter unten in dieser Antwort diskutiert.

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

Dieses arbeitet. Es ist nicht großartig. An diesem Punkt sollten Sie jedoch das Zirkelbezugsproblem kennen und wissen, was wir getan haben, um es zu "reparieren", auch wenn die Korrektur schlecht ist.

Der Grund für die Fehlerbehebung ist, dass die nächste Person, die #include "A.h" verwendet, B deklarieren muss, bevor sie sie verwenden kann, und einen schrecklichen #include-Fehler erhält. Verschieben wir also die Deklaration in A.h selbst.

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

Und in B.h können Sie an dieser Stelle einfach #include "A.h" direkt.

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

237
Roosh

Sie können Kompilierungsfehler vermeiden, wenn Sie die Methodendefinitionen aus den Headerdateien entfernen und die Klassen nur die Methodendeklarationen und Variablendeklarationen/Definitionen enthalten lassen. Die Methodendefinitionen sollten in einer .cpp-Datei abgelegt werden (so wie es in einer Best-Practice-Richtlinie heißt). 

Der Nachteil der folgenden Lösung ist (vorausgesetzt, Sie hatten die Methoden in der Headerdatei platziert, um sie inline zu setzen), dass die Methoden nicht mehr vom Compiler eingebettet werden und der Versuch, das Inline-Schlüsselwort zu verwenden, Linker-Fehler erzeugt.

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

Dinge zu beachten:

  • Dies funktioniert nicht, wenn class A ein Objekt von class B als Mitglied hat oder umgekehrt. 
  • Vorwärtsdeklaration ist der Weg zu gehen.
  • Reihenfolge der Deklaration ist wichtig (weshalb Sie die Definitionen verschieben) .
    • Wenn beide Klassen Funktionen der anderen aufrufen, müssen Sie die Definitionen verschieben.

Lesen Sie die FAQ:

17
dirkgently

Ich beantworte dies zu spät, aber es gibt keine vernünftige Antwort, obwohl es eine beliebte Frage mit sehr vielversprechenden Antworten ist ...

Best Practice: Weiterleiten von Deklarationsköpfen

Wie aus dem <iosfwd>-Header der Standard Library hervorgeht, ist der richtige Weg, Forward-Deklarationen für andere bereitzustellen, einenforward-Deklarations-Header. Zum Beispiel:

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;

b.h:

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

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

Die Betreuer der Bibliotheken A und B sollten jeweils dafür verantwortlich sein, dass ihre Header für die Vorwärtsdeklaration mit ihren Kopfzeilen und Implementierungsdateien synchronisiert werden. Wenn also der Betreuer von "B" mitkommt, schreibt er den Code ...

b.fwd.h:

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

b.h:

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

... dann wird die Neukompilierung des Codes für "A" durch die Änderungen am enthaltenen b.fwd.h ausgelöst und sollte sauber abgeschlossen werden.


Schlechte, aber gängige Praxis: Vorwärtsdeklaration in anderen Bibliotheken

Anstelle eines Forward-Deklarations-Headers, wie oben beschrieben, wird der Code in a.h oder a.cc anstelle von Forward-Deklarationen class B; selbst verwendet:

  • wenn a.h oder a.cc später b.h enthielt:
    • die Kompilierung von A wird mit einem Fehler beendet, sobald sie zu der widersprüchlichen Deklaration/Definition von B gelangt ist (d. h. die obige Änderung von B brach A und alle anderen Clients, die Forward-Deklarationen missbrauchen, anstatt transparent zu arbeiten).
  • ansonsten (wenn A schließlich nicht b.h enthielt - möglich, wenn A nur Bs nach Zeiger und/oder Verweis speichert/durchläuft)
    • build-Tools, die auf #include-Analyse und geänderten Dateizeitstempeln angewiesen sind, werden A (und den davon abhängigen Code) nach der Änderung von B nicht neu erstellen. Dies führt zu Fehlern bei der Link- oder Laufzeit. Wenn B als eine zur Laufzeit geladene DLL verteilt wird, kann der Code in "A" zur Laufzeit möglicherweise nicht die verschiedensten Symbole finden, die möglicherweise nicht gut genug gehandhabt werden, um ein ordnungsgemäßes Herunterfahren auszulösen oder die Funktionalität akzeptabel zu reduzieren.

Wenn der Code von A über Vorlagenspezialisierungen/"Eigenschaften" für die alte B verfügt, werden sie nicht wirksam.

14
Tony Delroy

Ich habe dieses Problem einmal gelöst, indem ich alle inlines nach der Klassendefinition verschob und den #include für die anderen Klassen direkt vor inlines in der Header-Datei platzierte. Auf diese Weise wird sichergestellt, dass alle Definitionen + Inlines vor den Inlines gesetzt werden.

Auf diese Weise können mehrere Inlines in beiden (oder mehreren) Header-Dateien gespeichert werden. Es ist jedoch notwendig, Wachen einzuschließen.

So was

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

... und dasselbe in B.h

11
epatel

Ich habe einmal einen Beitrag darüber geschrieben: Zirkuläre Abhängigkeiten in C++ auflösen

Die grundlegende Technik besteht darin, die Klassen über Schnittstellen zu entkoppeln. Also in deinem Fall:

//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;
}
6
Eduard Wirch

Hier ist die Lösung für Vorlagen: So umgehen Sie zirkuläre 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, Deklaration und Definition in separate Dateien aufzuteilen, aber Sie können sie so strukturieren, als wären sie in separaten Dateien.

3
Tatyana

Das auf Wikipedia vorgestellte einfache Beispiel hat für mich funktioniert. (Die vollständige Beschreibung finden Sie unter http://de.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;
}
2
madx

Leider kann ich die Antwort von geza nicht kommentieren.

Er sagt nicht nur "Erklärungen in einen separaten Header stellen". Er sagt, dass Sie Klassendefinitions-Header und Inline-Funktionsdefinitionen in verschiedene Header-Dateien verschieben müssen, um "verzögerte Abhängigkeiten" zu ermöglichen.

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

Um es besser zu verstehen, stellen Sie sich vor, Klasse A hat ein Feld vom Typ B, nicht B *. Zusätzlich wollen Klasse A und B eine Inline-Funktion mit Parametern des 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"

Es 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 wird nicht kompiliert, da B :: Do einen vollständigen Typ von A benötigt, der später definiert wird.

Um sicherzustellen, dass der Quellcode kompiliert wird, sollte er folgendermaßen 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 dies ist mit diesen beiden Header-Dateien für jede Klasse möglich, die Inline-Funktionen definieren muss. Das einzige Problem ist, dass die zirkulären Klassen nicht nur den "öffentlichen Header" enthalten können.

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

Diese Anweisung sollte die Verarbeitung der aktuellen Datei verschieben und alle ausstehenden Includes vervollständigen.

0
Bernd Baumanns

In einigen Fällen ist es möglich, define eine Methode oder einen Konstruktor der Klasse B in der Header-Datei der Klasse A zu definieren, um zirkuläre Abhängigkeiten mit Definitionen aufzulösen. Auf diese Weise können Sie vermeiden, Definitionen in .cc -Dateien ablegen zu müssen, wenn Sie beispielsweise nur eine Header-Bibliothek implementieren möchten.

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

Leider fehlen allen vorherigen Antworten einige Details. Die richtige Lösung ist etwas umständlich, aber dies ist die einzige Möglichkeit, dies richtig zu machen. Und es lässt sich problemlos skalieren und kann auch komplexere Abhängigkeiten verarbeiten.

So können Sie dies tun, wobei alle Details und die Verwendbarkeit genau beibehalten werden:

  • die Lösung ist genau die gleiche wie ursprünglich beabsichtigt
  • inline-Funktionen noch Inline
  • benutzer von A und B können A.h und B.h in beliebiger Reihenfolge einschließen

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

// 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 und B sollten sie nicht verwenden. Der öffentliche Kopf ist A.h und B.h.

0
geza