Beheben Sie Buildfehler aufgrund von zirkulärer Abhängigkeit zwischen Klassen
Lesezeit: 13 Minuten
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;
}
};
#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
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
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.
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
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;
… 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
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.
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.
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.
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.
10036000cookie-checkBeheben Sie Buildfehler aufgrund von zirkulärer Abhängigkeit zwischen Klassenyes
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