Beheben Sie Buildfehler aufgrund von zirkulärer Abhängigkeit zwischen Klassen

Lesezeit: 13 Minuten

Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
Autodidakt

Ich befinde mich oft in einer Situation, in der ich in einem C++-Projekt aufgrund einiger schlechter Designentscheidungen (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 in derselben Datei vorkommen). Aber zum Glück (?) passiert das nicht oft genug, um mich für das nächste Mal, wenn es wieder passiert, an die Lösung für dieses Problem zu erinnern.

Damit ich mich in Zukunft leichter daran erinnern kann, werde ich ein repräsentatives Problem und eine Lösung zusammen damit posten. 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;
    }
    

  • Beim Arbeiten mit Visual Studio wird die /showIncludes flag hilft sehr beim Debuggen dieser Art von Problemen.

    – wischen

    12. September 2012 um 3:08 Uhr


  • Gibt es etwas Ähnliches für Visual Studio-Code?

    – Erich

    9. November 2021 um 13:41 Uhr

1647308414 587 Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
Rosch

Die Art, darüber nachzudenken, ist, “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;
}

Beim Kompilieren der .cc Datei (denken Sie daran, dass die .cc und nicht die .h die Einheit der Kompilierung ist), müssen Sie dem Objekt Platz zuweisen A. Also, na ja, wie viel Platz dann? Genug zum Aufbewahren B! Was ist die Größe von B dann? Genug zum Aufbewahren A! Hoppla.

Eindeutig ein Zirkelverweis, den Sie unterbrechen müssen.

Sie können es brechen, indem Sie dem Compiler erlauben, stattdessen so viel Platz zu reservieren, wie er im Voraus weiß – Zeiger und Referenzen sind beispielsweise immer 32 oder 64 Bit (abhängig von der Architektur) und so, wenn Sie (beide) durch ersetzen ein Zeiger oder 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 ist alles besser. Etwas. main() sagt noch:

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

#includefür alle Bereiche und Zwecke (wenn Sie den Präprozessor herausnehmen) kopiert die Datei einfach in die .cc. Also wirklich, die .cc sieht aus wie:

// 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 – es hat das Symbol noch nie zuvor gesehen.

Sagen wir also dem Compiler Bescheid B. Dies ist als bekannt Vorwärtserklärungund wird in dieser Antwort weiter erörtert.

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

Dies funktioniert. Es ist nicht großartig. Aber an diesem Punkt sollten Sie das Zirkelverweisproblem verstehen und wissen, was wir getan haben, um es zu “reparieren”, obwohl die Lösung schlecht ist.

Der Grund, warum dieser Fix schlecht ist, liegt darin, dass die nächste Person zu #include "A.h" wird deklarieren müssen B bevor sie es benutzen können und einen schrecklichen bekommen #include Error. Lassen Sie uns also die Deklaration in verschieben Ah selbst.

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

Und in Bhan dieser Stelle können Sie 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.

  • “Dem Compiler von B erzählen” wird als Vorwärtsdeklaration von B bezeichnet.

    – Peter Ajtai

    17. November 2010 um 1:57 Uhr

  • OMG! völlig übersehen, dass Referenzen in Bezug auf belegten Raum bekannt sind. Jetzt kann ich endlich richtig designen!

    – kellogs

    7. November 2011 um 2:31 Uhr

  • Aber Sie können immer noch keine Funktion auf B verwenden (wie in der Frage _b->Printt())

    – Rang 1

    17. April 2013 um 11:02 Uhr

  • @sydan: Das kannst du nicht. Zirkuläre Abhängigkeiten auflösen erfordert klassenfremde Definitionen.

    – Ben Voigt

    11. April 2015 um 14:03 Uhr

  • Aber ich muss in verwenden A Klasse B als kompletter Typ und in B Klasse A als vollständiger Typ. Mit vollständigem Typ meine ich den Aufruf einer Funktion von einem Objekt dieses Typs. Wie würde ich es tun? Ich bekomme nur Fehler, invalid use of incomplete type B in class A.

    – Silidron

    5. September 2017 um 12:47 Uhr

Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
Autodidakt

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 platziert werden (genau wie es eine Best-Practice-Richtlinie sagt).

Die Kehrseite der folgenden Lösung ist (vorausgesetzt, Sie haben die Methoden in die Headerdatei eingefügt, um sie zu inlinen), dass die Methoden nicht mehr vom Compiler eingebunden werden und der Versuch, das Schlüsselwort inline zu verwenden, zu Linkerfehlern führt.

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

  • Danke. Damit war das Problem problemlos gelöst. Ich habe einfach die kreisförmigen Includes in die .cpp-Dateien verschoben.

    – Lenar Hoyt

    4. Oktober 2014 um 18:16 Uhr

  • 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

    1. September 2016 um 12:55 Uhr

  • Du fügst „Ah“ und „Bh“ immer zusammen ein. Warum fügen Sie “Ah” nicht in “Bh” ein und fügen dann nur “Bh” sowohl in “A.cpp” als auch in “B.cpp” ein?

    – Gussew Slawa

    30. September 2018 um 4:25 Uhr

  • Danke, schöne Antwort für diejenigen, die diese Abhängigkeit zwischen 2 Klassen brauchen und sie nicht anders umgestalten können

    – HanniBaL90

    22. Dezember 2020 um 14:34 Uhr

1647308415 441 Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
Toni Delroy

Ich bin spät dran, darauf zu antworten, aber es gibt bisher keine vernünftige Antwort, obwohl es sich um eine beliebte Frage mit hoch bewerteten Antworten handelt ….

Best Practice: Forward-Deklarationsheader

Wie durch die Standardbibliothek veranschaulicht <iosfwd> Header, der richtige Weg, Forward-Deklarationen für andere bereitzustellen, ist a Header der Forward-Deklaration. 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 A und B Bibliotheken sollten jeweils dafür verantwortlich sein, ihre Forward-Deklarations-Header mit ihren Headern und Implementierungsdateien synchron zu halten, also – zum Beispiel – wenn der Betreuer von “B” vorbeikommt und den Code umschreibt, 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 an den enthaltenen ausgelöst b.fwd.h und sollte sauber abschließen.


Schlechte, aber gängige Praxis: Dinge in anderen Bibliotheken vorwärts deklarieren

Sagen Sie – anstatt wie oben erklärt einen Forward-Deklarations-Header zu verwenden – Code in a.h oder a.cc stattdessen vorwärts-deklariert class B; selbst:

  • wenn a.h oder a.cc beinhaltete b.h später:
    • Die Kompilierung von A wird mit einem Fehler beendet, sobald die widersprüchliche Deklaration/Definition von erreicht wird B (dh die obige Änderung an B brach A und alle anderen Clients, die Vorwärtsdeklarationen missbrauchten, anstatt transparent zu arbeiten).
  • andernfalls (wenn A nicht schließlich eingeschlossen hat b.h – möglich, wenn A nur Bs per Zeiger und/oder Referenz speichert/umgibt)
    • Bauen Sie Tools auf #include Analyse und geänderte Dateizeitstempel werden nicht neu erstellt A (und sein weiterer abhängiger Code) nach der Änderung zu B, wodurch Fehler zur Verbindungszeit oder zur Laufzeit verursacht werden. Wenn B als zur Laufzeit geladene DLL vertrieben wird, findet der Code in „A“ möglicherweise die unterschiedlich verstümmelten Symbole zur Laufzeit nicht, was möglicherweise gut genug gehandhabt wird, um ein ordnungsgemäßes Herunterfahren oder eine akzeptabel reduzierte Funktionalität auszulösen.

Wenn der Code von A Vorlagenspezialisierungen / “Eigenschaften” für den alten hat Bwerden sie nicht wirksam.

  • Dies ist ein wirklich sauberer Weg, um die Vorwärtsdeklarationen zu handhaben. Das einzige “Nachteil” würde in den zusätzlichen Dateien sein. Ich nehme an, Sie schließen immer ein a.fwd.h in a.h, um sicherzustellen, dass sie synchron bleiben. Wo diese Klassen verwendet werden, fehlt der Beispielcode. a.h und b.h müssen beide eingebunden werden, da sie nicht isoliert funktionieren: “` //main.cpp #include “ah” #include “bh” int main() { … } “` Oder einer von ihnen braucht in der anderen wie in der Eröffnungsfrage vollständig enthalten sein. Woher b.h beinhaltet a.h und main.cpp beinhaltet b.h

    – Weiter Weg

    5. Mai 2017 um 16:37 Uhr


  • @Farway In jeder Hinsicht richtig. Ich habe mich nicht darum gekümmert, es zu zeigen main.cpp, aber schön, dass Sie in Ihrem Kommentar dokumentiert haben, was es enthalten sollte. Prost

    – Toni Delroy

    5. Mai 2017 um 20:42 Uhr

  • Eine der besseren Antworten mit einer schönen detaillierten Erklärung, warum mit den Vor- und Nachteilen zu tun und zu lassen ist …

    – Franz Cugler

    16. Januar 2018 um 5:06 Uhr

  • @RezaHajianpour: Es ist sinnvoll, einen Header für die Vorwärtsdeklaration für alle Klassen zu haben, für die Sie Deklarationen weiterleiten möchten, ob kreisförmig oder nicht. Sie werden sie jedoch nur benötigen, wenn: 1) die Einbeziehung der eigentlichen Deklaration kostspielig ist (oder voraussichtlich später werden wird) (z. B. enthält sie viele Kopfzeilen, 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 ein paar Stream-Objekte geben, auf die von vielen Stellen aus verwiesen wird, und <iostream> ist viel einzubeziehen.

    – Toni Delroy

    23. Januar 2019 um 5:02 Uhr

  • @RezaHajianpour: Ich denke, Sie haben die richtige Idee, aber es gibt ein terminologisches Problem mit Ihrer Aussage: „Wir brauchen nur den Typ, der sein soll erklärt” wäre richtig. Der Typ ist erklärt bedeutet, dass die Vorwärtsdeklaration gesehen wurde; es ist definiert Sobald die vollständige Definition analysiert wurde (und dafür Sie dürfen brauche mehr #includeS).

    – Toni Delroy

    25. Januar 2019 um 14:45 Uhr


1647308415 1 Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
dirkly

Dinge, an die Sie sich erinnern sollten:

  • Das geht nicht, wenn class A hat ein Objekt von class B als Mitglied oder umgekehrt.
  • Forward-Deklaration ist ein Weg zu gehen.
  • Die Reihenfolge der Deklaration ist wichtig (weshalb Sie die Definitionen verschieben).
    • Wenn beide Klassen Funktionen der anderen aufrufen, müssen Sie die Definitionen auslagern.

Lesen Sie die häufig gestellten Fragen:

Beheben Sie Buildfehler aufgrund von zirkularer Abhangigkeit zwischen Klassen
epatel

Ich habe diese Art von Problem einmal gelöst, indem ich alle verschoben habe in Linien nach der Klassendefinition und dem Setzen der #include für die anderen Klassen kurz vor dem in Linien in der Header-Datei. Auf diese Weise stellen Sie sicher, dass alle Definitionen + Inlines gesetzt sind, bevor die Inlines geparst werden.

Auf diese Weise ist es möglich, immer noch eine Reihe von Inlines in beiden (oder mehreren) Header-Dateien zu haben. Aber man muss es haben gehören Wächter.

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 mache das gleiche in B.h

  • Warum? Ich denke, es ist eine elegante Lösung für ein kniffliges Problem … wenn man Inlines haben möchte. Wenn man keine Inlines will, hätte man den Code nicht so schreiben sollen, wie er von Anfang an geschrieben wurde …

    – Epatel

    10. März 2009 um 20:01 Uhr

  • Was passiert, wenn ein Benutzer einschließt B.h Erste?

    – Herr Fooz

    18. März 2014 um 16:00 Uhr

  • Beachten Sie, dass Ihr Kopfschutz einen reservierten Bezeichner verwendet, alles mit doppelten benachbarten Unterstrichen ist reserviert.

    – Lars Viklund

    10. August 2015 um 15:09 Uhr

Dazu habe ich mal einen Beitrag geschrieben: Zirkuläre Abhängigkeiten in c++ auflösen

Die grundlegende Technik besteht darin, die Klassen durch 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;
}

  • Warum? Ich denke, es ist eine elegante Lösung für ein kniffliges Problem … wenn man Inlines haben möchte. Wenn man keine Inlines will, hätte man den Code nicht so schreiben sollen, wie er von Anfang an geschrieben wurde …

    – Epatel

    10. März 2009 um 20:01 Uhr

  • Was passiert, wenn ein Benutzer einschließt B.h Erste?

    – Herr Fooz

    18. März 2014 um 16:00 Uhr

  • Beachten Sie, dass Ihr Kopfschutz einen reservierten Bezeichner verwendet, alles mit doppelten benachbarten Unterstrichen ist reserviert.

    – Lars Viklund

    10. August 2015 um 15:09 Uhr

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 Definition in separate Dateien aufzuteilen, aber Sie können sie so strukturieren, als wären sie in separaten Dateien.

1003600cookie-checkBeheben Sie Buildfehler aufgrund von zirkulärer Abhängigkeit zwischen Klassen

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy