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

385voto

Roosh Punkte 3626

Eine Möglichkeit, 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 Platz für das Objekt A . Also, wie viel Platz ist dann vorhanden? Genug um zu speichern B ! Was ist die Größe von B dann? Genug zum Speichern A ! Ups.

Es handelt sich eindeutig um einen Zirkelbezug, den Sie durchbrechen müssen.

Man kann das umgehen, indem man dem Compiler erlaubt, so viel Speicherplatz zu reservieren, wie ihm im Voraus bekannt ist - Zeiger und Referenzen zum Beispiel werden immer 32 oder 64 Bit sein (je nach Architektur), und wenn man also (eines von beiden) durch einen Zeiger oder eine Referenz ersetzt, wäre alles gut. Sagen wir, 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. Irgendwie. main() sagt immer noch:

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

#include (wenn man den Präprozessor ausschaltet) kopiert die Datei einfach in den Ordner .cc . Also wirklich, die .cc sieht 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 - es hat das Symbol noch nie zuvor gesehen.

Teilen wir dem Compiler also mit, dass B . Dies ist bekannt als Terminerklärung und wird weiter erörtert in diese Antwort .

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

Diese Werke . Es ist nicht großartig . Aber an diesem Punkt sollten Sie das Problem der zirkulären Referenz verstehen und wissen, wie wir es "behoben" haben, auch wenn die Lösung schlecht ist.

Der Grund, warum diese Lösung schlecht ist, ist, dass die nächste Person, die #include "A.h" wird erklären müssen B bevor sie es benutzen können, und sie werden eine schreckliche #include Fehler. Verschieben wir also die Erklärung 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.

120voto

Autodidact Punkte 27160

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 untergebracht werden (wie es in einer Best-Practice-Richtlinie heißt).

Der Nachteil der folgenden Lösung ist (unter der Annahme, dass Sie die Methoden in der Header-Datei platziert hatten, um sie zu inline), dass die Methoden nicht mehr vom Compiler 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;
}

50voto

Tony Delroy Punkte 98528

Ich bin spät dran mit der Beantwortung dieser Frage, aber bis heute gibt es keine einzige vernünftige Antwort, obwohl es sich um eine beliebte Frage mit vielen hochgestimmten Antworten handelt....

Bewährte Praxis: Kopfzeilen der Vorwärtserklärung

Wie das Beispiel der Standardbibliothek <iosfwd> Kopfzeile, ist der richtige Weg, um Forward-Deklarationen für andere bereitzustellen, eine Kopf der Vorwärtserklärung . Zum Beispiel:

a.fw.h:

#pragma once
class A;

a.h:

#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 A y B Bibliotheken sollten jeweils dafür verantwortlich sein, ihre Vorwärtsdeklarations-Header mit ihren Headern und Implementierungsdateien synchron zu halten, so dass - zum Beispiel - wenn der Maintainer von "B" daherkommt und den Code umschreibt, um...

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 an den enthaltenen b.fwd.h und sollte sauber abgeschlossen werden.


Schlechte, aber gängige Praxis: Vorwärtsdeklarationen in anderen Libs

Nehmen wir an, statt wie oben beschrieben einen Vorwärtsdeklarationskopf zu verwenden, wird der Code in a.h ou a.cc stattdessen vorwärts-deklariert class B; selbst:

  • si a.h ou a.cc enthielt b.h später:
    • Die Kompilierung von A bricht mit einem Fehler ab, sobald sie an die widersprüchliche Deklaration/Definition von B (d.h. die obige Änderung an B führte dazu, dass A und alle anderen Clients die Forward-Deklarationen missbrauchen, anstatt transparent zu arbeiten).
  • andernfalls (wenn A nicht schließlich auch b.h - möglich, wenn A die Bs nur per Zeiger und/oder Verweis speichert/umgibt)
    • Werkzeuge bauen, die auf #include Analyse und geänderte Datei-Zeitstempel werden nicht wiederhergestellt A (und den davon abhängigen Code) nach der Änderung in B, was zu Fehlern bei der Verknüpfung oder während der Laufzeit führt. Wenn B als zur Laufzeit geladene DLL verteilt wird, kann es sein, dass der Code in "A" die anders gemischten Symbole zur Laufzeit nicht findet, was möglicherweise nicht gut genug gehandhabt wird, um ein geordnetes Herunterfahren oder eine akzeptabel reduzierte Funktionalität auszulösen.

Wenn der Code von A Vorlagenspezialisierungen / "Eigenschaften" für die alte B werden sie nicht in Kraft treten.

26voto

dirkgently Punkte 104289

Das sollten Sie sich merken:

  • Dies wird nicht funktionieren, wenn class A hat ein Objekt von class B als Mitglied oder vice versa.
  • Die Vorwärtserklärung ist der richtige Weg.
  • Es kommt auf die Reihenfolge der Erklärung an (weshalb Sie die Definitionen auslagern).
    • Wenn beide Klassen Funktionen der jeweils anderen aufrufen, müssen Sie die Definitionen auslagern.

Lesen Sie die FAQ:

15voto

epatel Punkte 45365

Ich habe diese Art von Problem einmal gelöst, indem ich alle Inlines nach der Klassendefinition und setzen die #include für die anderen Klassen kurz vor der Inlines in der Header-Datei. Auf diese Weise kann man sicherstellen, dass alle Definitionen und 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 es ist notwendig, dass Wachen einschließen .

Wie diese

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

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