Beheben Sie Buildfehler aufgrund der zirkulären Abhängigkeit zwischen Klassen

353

Ich befinde mich oft in einer Situation, in der ich in einem C ++ - Projekt aufgrund einiger schlechter Entwurfsentscheidungen (von jemand anderem getroffen :) mit mehreren Kompilierungs- / Linkerfehlern konfrontiert bin, die zu zirkulären Abhängigkeiten zwischen C ++ - Klassen in verschiedenen Header-Dateien führen (kann auch vorkommen) in der gleichen Datei) . Aber zum Glück (?) Kommt das nicht oft genug vor, damit ich mich an die Lösung dieses Problems erinnere, wenn es das nächste Mal wieder passiert.

Um in Zukunft einen einfachen Rückruf zu ermöglichen, werde ich ein repräsentatives Problem und eine Lösung dazu 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;
    }
    
Autodidakt
quelle
23
Bei der Arbeit mit Visual Studio hilft das Flag / showIncludes beim Debuggen dieser Art von Problemen.
Wischen

Antworten:

288

Der Weg, darüber nachzudenken, besteht darin, "wie ein Compiler zu denken".

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

// 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 .cc- Datei kompilieren (denken Sie daran, dass die .cc und nicht die .h die Kompilierungseinheit ist), müssen Sie Speicherplatz für das Objekt zuweisen A. Also, wie viel Platz dann? Genug zum Aufbewahren B! Wie groß ist Bdann? Genug zum Aufbewahren A! Hoppla.

Klar 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 Upfront weiß. Zeiger und Referenzen sind beispielsweise immer 32 oder 64 Bit (abhängig von der Architektur), und wenn Sie (eines davon) durch ersetzen ein Zeiger oder eine Referenz, 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 ist es besser. Etwas. main()sagt immer noch:

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

#includeFür alle Bereiche und Zwecke (wenn Sie den Präprozessor herausnehmen) wird die Datei einfach in die .cc- Datei kopiert . Die .cc sieht also wirklich 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 es Bist - er hat das Symbol noch nie zuvor gesehen.

Erzählen wir dem Compiler davon B. Dies ist als Vorwärtserklärung bekannt und wird in dieser Antwort weiter erörtert .

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

Das funktioniert . Es ist nicht großartig . Aber an diesem Punkt sollten Sie ein Verständnis für das Zirkelverweisproblem haben und wissen, was wir getan haben, um es zu "beheben", obwohl das Problem schlecht ist.

Der Grund, warum dieses Update schlecht ist, ist, dass die nächste Person #include "A.h"deklarieren muss, Bbevor sie es verwenden kann, und einen schrecklichen #includeFehler erhält . Verschieben wir also die Erklärung in Ah selbst.

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

Und in Bh 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.

Roosh
quelle
20
"Dem Compiler von B erzählen" ist als Vorwärtserklärung von B.
Peter Ajtai
8
Oh mein Gott! völlig übersehen die Tatsache, dass Referenzen in Bezug auf den belegten Raum bekannt sind. Endlich kann ich jetzt richtig gestalten!
Kellogs
47
Sie können jedoch keine Funktion für B verwenden (wie in der Frage _b-> Printt ())
Rang 1
3
Das ist das Problem, das ich habe. Wie bringen Sie die Funktionen mit der Vorwärtsdeklaration ein, ohne die Header-Datei vollständig neu zu schreiben?
Sydney
101

Sie können Kompilierungsfehler vermeiden, wenn Sie die Methodendefinitionen aus den Header-Dateien entfernen und die Klassen nur die Methodendeklarationen und Variablendeklarationen / -definitionen enthalten lassen. Die Methodendefinitionen sollten in einer CPP-Datei abgelegt werden (genau wie in einer Best-Practice-Richtlinie angegeben).

Die Kehrseite der folgenden Lösung ist (vorausgesetzt, Sie haben die Methoden in die Header-Datei eingefügt, um sie zu inline), dass die Methoden vom Compiler nicht mehr inline sind 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;
}
Autodidakt
quelle
Vielen Dank. Dies löste das Problem leicht. Ich habe die Rundschreiben-Includes einfach in die CPP-Dateien verschoben.
Lenar Hoyt
3
Was ist, wenn Sie eine Vorlagenmethode haben? Dann können Sie es nicht wirklich in eine CPP-Datei verschieben, es sei denn, Sie instanziieren die Vorlagen manuell.
Malcolm
Sie schließen immer "Ah" und "Bh" zusammen ein. Warum fügen Sie "Ah" nicht in "Bh" und dann nur "Bh" in "A.cpp" und "B.cpp" ein?
Gusev Slava
28

Ich beantworte dies zu spät, aber es gibt bisher keine vernünftige Antwort, obwohl es sich um eine beliebte Frage mit hoch bewerteten Antworten handelt.

Best Practice: Weiterleiten von Deklarationsheadern

Wie von der Standardbibliothek erläuterte <iosfwd>Header, ist der richtige Weg nach vorn Erklärungen für andere , um einen haben , vorwärts Erklärung 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;

bh:

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

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

Die Betreuer der Aund der BBibliotheken sollten jeweils dafür verantwortlich sein, ihre Forward-Deklarationsheader mit ihren Headern und Implementierungsdateien synchron zu halten. Wenn also beispielsweise der Betreuer von "B" vorbeikommt und den Code neu schreibt, um ...

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;

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


Schlechte, aber übliche Praxis: Vorwärts deklarieren Sachen in anderen Bibliotheken

Sagen Sie - anstatt wie oben erläutert einen Forward-Deklarationsheader zu verwenden - Code in a.hoder a.ccdeklarieren Sie sich class B;selbst vorwärts :

  • wenn a.hoder a.ccenthielt b.hspäter:
    • Die Kompilierung von A wird mit einem Fehler beendet, sobald die widersprüchliche Deklaration / Definition von erreicht ist B(dh die obige Änderung von B hat A und alle anderen Clients, die Vorwärtsdeklarationen missbrauchen, gebrochen, anstatt transparent zu arbeiten).
  • andernfalls (wenn A letztendlich nicht eingeschlossen hat b.h- möglich, wenn A Bs nur durch Zeiger und / oder Referenz speichert / umgibt)
    • Build-Tools, die auf #includeAnalysen und geänderten Dateizeitstempeln basieren, werden Anach dem Wechsel zu B nicht neu erstellt (und der weitere abhängige Code), was zu Fehlern bei der Verknüpfung oder zur Laufzeit führt. Wenn B als zur Laufzeit geladene DLL verteilt wird, kann es sein, dass der Code in "A" zur Laufzeit die unterschiedlich verstümmelten Symbole nicht findet, was möglicherweise gut genug gehandhabt wird oder nicht, um ein ordnungsgemäßes Herunterfahren oder eine akzeptabel reduzierte Funktionalität auszulösen.

Wenn der Code von A Vorlagenspezialisierungen / "Merkmale" für das alte enthält B, werden diese nicht wirksam.

Tony Delroy
quelle
2
Dies ist eine sehr saubere Methode, um mit den Vorwärtsdeklarationen umzugehen. Der einzige "Nachteil" wäre in den zusätzlichen Dateien. Ich nehme an, Sie immer auch a.fwd.hin a.h, um sie synchron bleiben zu gewährleisten. Der Beispielcode fehlt, wenn diese Klassen verwendet werden. a.hund b.hbeide müssen eingeschlossen werden, da sie nicht isoliert funktionieren: `` `//main.cpp #include" ah "#include" bh "int main () {...}` `` Oder einer von ihnen muss wie in der Eröffnungsfrage vollständig in die andere aufgenommen werden. Wo b.hbeinhaltet a.hund main.cppbeinhaltetb.h
Farway
2
@Farway In jeder Hinsicht richtig. Ich habe mich nicht darum gekümmert zu zeigen main.cpp, aber schön, dass Sie dokumentiert haben, was es in Ihrem Kommentar enthalten sollte. Prost
Tony Delroy
1
Eine der besseren Antworten mit einer schönen detaillierten Erklärung, warum mit den Vor- und Nachteilen aufgrund der Vor- und Nachteile ...
Francis Cugler
1
@RezaHajianpour: Es ist sinnvoll, einen Vorwärtsdeklarationsheader für alle Klassen zu haben, für die Sie Vorwärtsdeklarationen wünschen, zirkulär oder nicht. Das heißt, Sie werden sie nur dann wollen, wenn: 1) das Einfügen der tatsächlichen Deklaration kostspielig ist (oder voraussichtlich später wird) (z. B. viele Header enthält, die Ihre Übersetzungseinheit sonst möglicherweise nicht benötigt), und 2) der Client-Code ist wahrscheinlich in der Lage sein, Zeiger oder Verweise auf die Objekte zu verwenden. <iosfwd>ist ein klassisches Beispiel: Es kann einige Stream-Objekte geben, auf die von vielen Stellen aus verwiesen wird, und es <iostream>gibt viel zu berücksichtigen.
Tony Delroy
1
@RezaHajianpour: Ich denke, Sie haben die richtige Idee, aber es gibt ein terminologisches Problem mit Ihrer Aussage: "Wir müssen nur den Typ deklarieren " wäre richtig. Der deklarierte Typ bedeutet, dass die Vorwärtsdeklaration gesehen wurde. Es wird definiert, sobald die vollständige Definition analysiert wurde (und dafür benötigen Sie möglicherweise mehr #includes).
Tony Delroy
20

Dinge, an die man sich erinnern sollte:

  • Dies funktioniert nicht, wenn Sie class Aein Objekt class Bals Mitglied haben oder umgekehrt.
  • Die Vorwärtserklärung ist ein langer Weg.
  • Die 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:

dirkgently
quelle
1
Die von Ihnen bereitgestellten Links funktionieren nicht mehr. Kennen Sie zufällig die neuen Links, auf die Sie verweisen können?
Ramya Rao
11

Ich habe diese Art von Problem einmal gelöst, indem ich alle Inlines nach der Klassendefinition verschoben und die #includefür die anderen Klassen direkt vor den Inlines in die Header-Datei eingefügt habe . Auf diese Weise stellen Sie sicher, dass alle Definitionen + Inlines festgelegt sind, bevor die Inlines analysiert werden.

Auf diese Weise können immer noch eine Reihe von Inlines in beiden (oder mehreren) Header-Dateien vorhanden sein. Aber es ist notwendig, Wachen einzubeziehen .

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 das Gleiche tun in B.h

Epatel
quelle
Warum? Ich denke, es ist eine elegante Lösung für ein kniffliges Problem ... wenn man Inlines will. Wenn man keine Inlines will, hätte man den Code nicht so schreiben sollen, wie er von Anfang an geschrieben wurde ...
Epatel
Was passiert, wenn ein Benutzer B.hzuerst einschließt ?
Herr Fooz
3
Beachten Sie, dass Ihr Header Guard eine reservierte Kennung verwendet. Alles mit doppelt benachbarten Unterstrichen ist reserviert.
Lars Viklund
6

Ich habe einmal einen Beitrag dazu geschrieben: Zirkuläre Abhängigkeiten in c ++ auflösen

Die grundlegende Technik besteht darin, die Klassen mithilfe von 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;
}
Eduard Wirch
quelle
2
Bitte beachten Sie, dass die Verwendung von Schnittstellen virtualAuswirkungen auf die Laufzeitleistung hat.
Cemper93
4

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

Tatyana
quelle
2

Das einfache Beispiel auf Wikipedia hat für mich funktioniert. (Die vollständige Beschreibung finden Sie 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;
}
Madx
quelle
1

Leider fehlen allen vorherigen Antworten einige Details. Die richtige Lösung ist etwas umständlich, aber nur so kann man es richtig machen. Und es lässt sich leicht skalieren und behandelt auch komplexere Abhängigkeiten.

So können Sie dies tun, indem Sie alle Details und die Benutzerfreundlichkeit genau beibehalten:

  • Die Lösung ist genau die gleiche wie ursprünglich beabsichtigt
  • Inline-Funktionen noch inline
  • Benutzer von Aund Bkönnen Ah und Bh in beliebiger Reihenfolge einschließen

Erstellen Sie zwei Dateien, A_def.h, B_def.h. Diese enthalten nur Adie BDefinitionen von 'und ':

// 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 Ah und Bh Folgendes 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 Aund diese Bnicht verwenden sollten. Der öffentliche Header ist Ah und Bh

geza
quelle
1
Hat dies irgendwelche Vorteile gegenüber der Lösung von Tony Delroy ? Beide basieren auf "Helfer" -Headern, aber Tonys sind kleiner (sie enthalten nur die Vorwärtsdeklaration) und sie scheinen auf die gleiche Weise zu funktionieren (zumindest auf den ersten Blick).
Fabio sagt Reinstate Monica
1
Diese Antwort löst das ursprüngliche Problem nicht. Es heißt nur "Deklarationen in einen separaten Header einfügen". Nichts über das Auflösen der zirkulären Abhängigkeit (die Frage benötigt eine Lösung, bei der Adie BDefinition von 'und ' verfügbar ist, die Vorwärtsdeklaration nicht ausreicht).
Geza
0

In einigen Fällen ist es möglich , 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, dass Definitionen in .ccDateien eingefügt werden müssen, z. B. wenn Sie 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();
}
jkoendev
quelle
0

Leider kann ich die Antwort von geza nicht kommentieren.

Er sagt nicht nur "Erklärungen in einem separaten Header vorlegen". Er sagt, dass Sie Klassendefinitionsheader und Inline-Funktionsdefinitionen in verschiedene Headerdateien verschütten 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, dass Klasse A ein Feld vom Typ B hat, nicht B *. Zusätzlich möchten die Klassen 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"

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

Dies ist mit diesen beiden Header-Dateien für jede Klasse, die Inline-Funktionen definieren muss, genau möglich. 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 abschließen.

Bernd Baumanns
quelle