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; }
c++
compiler-errors
circular-dependency
c++-faq
Autodidakt
quelle
quelle
Antworten:
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.
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 AufbewahrenB
! Wie groß istB
dann? Genug zum AufbewahrenA
! 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
:Jetzt ist es besser. Etwas.
main()
sagt immer noch:#include
Fü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:Sie können sehen, warum der Compiler damit nicht umgehen kann - er hat keine Ahnung, was es
B
ist - 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 .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,B
bevor sie es verwenden kann, und einen schrecklichen#include
Fehler erhält . Verschieben wir also die Erklärung in Ah selbst.Und in Bh können Sie an dieser Stelle einfach
#include "A.h"
direkt.HTH.
quelle
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.
quelle
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:
Ah:
b.fwd.h:
bh:
Die Betreuer der
A
und derB
Bibliotheken 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:
bh:
... dann wird die Neukompilierung des Codes für "A" durch die Änderungen am enthaltenen ausgelöst
b.fwd.h
und 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.h
odera.cc
deklarieren Sie sichclass B;
selbst vorwärts :a.h
odera.cc
enthieltb.h
später:B
(dh die obige Änderung von B hat A und alle anderen Clients, die Vorwärtsdeklarationen missbrauchen, gebrochen, anstatt transparent zu arbeiten).b.h
- möglich, wenn A Bs nur durch Zeiger und / oder Referenz speichert / umgibt)#include
Analysen und geänderten Dateizeitstempeln basieren, werdenA
nach 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.quelle
a.fwd.h
ina.h
, um sie synchron bleiben zu gewährleisten. Der Beispielcode fehlt, wenn diese Klassen verwendet werden.a.h
undb.h
beide 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. Wob.h
beinhalteta.h
undmain.cpp
beinhaltetb.h
main.cpp
, aber schön, dass Sie dokumentiert haben, was es in Ihrem Kommentar enthalten sollte. Prost<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.#include
s).Dinge, an die man sich erinnern sollte:
class A
ein Objektclass B
als Mitglied haben oder umgekehrt.Lesen Sie die FAQ:
quelle
Ich habe diese Art von Problem einmal gelöst, indem ich alle Inlines nach der Klassendefinition verschoben und die
#include
fü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
... und das Gleiche tun in
B.h
quelle
B.h
zuerst einschließt ?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:
quelle
virtual
Auswirkungen auf die Laufzeitleistung hat.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.
quelle
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' '':
Datei '' 'b.h' '':
Datei '' 'main.cpp' '':
quelle
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:
A
undB
können Ah und Bh in beliebiger Reihenfolge einschließenErstellen Sie zwei Dateien, A_def.h, B_def.h. Diese enthalten nur
A
dieB
Definitionen von 'und ':Und dann werden Ah und Bh Folgendes enthalten:
Beachten Sie, dass A_def.h und B_def.h "private" Header sind, Benutzer von
A
und dieseB
nicht verwenden sollten. Der öffentliche Header ist Ah und Bhquelle
A
dieB
Definition von 'und ' verfügbar ist, die Vorwärtsdeklaration nicht ausreicht).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
.cc
Dateien eingefügt werden müssen, z. B. wenn Sie nur eine Header-Bibliothek implementieren möchten.quelle
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:
Dies würde zu folgendem Code führen:
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:
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.
quelle