Risolvi errori di compilazione dovuti alla dipendenza circolare tra le classi
-
05-07-2019 - |
Domanda
Mi trovo spesso in una situazione in cui mi trovo ad affrontare più errori di compilazione / linker in un progetto C ++ a causa di alcune decisioni di progettazione errate (prese da qualcun altro :)) che portano a dipendenze circolari tra le classi C ++ in diversi file di intestazione < em> (può accadere anche nello stesso file) . Ma per fortuna (?) Questo non succede abbastanza spesso da ricordare la soluzione a questo problema per la prossima volta che si ripete.
Quindi, ai fini di un facile richiamo in futuro, pubblicherò un problema rappresentativo e una soluzione insieme ad esso. Naturalmente sono benvenute soluzioni migliori.
-
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; }
Soluzione
Il modo di pensare a questo è di "pensare come un compilatore".
Immagina di scrivere un compilatore. E vedi un codice come questo.
// 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;
}
Quando si compila il file .cc (ricordare che .cc e non .h è l'unità di compilazione), devi allocare spazio per l'oggetto A
. Allora, quanto spazio allora? Abbastanza per memorizzare B
! Qual è la dimensione di B
allora? Abbastanza per memorizzare A
! Oops.
Chiaramente un riferimento circolare che devi rompere.
Puoi romperlo consentendo al compilatore di riservare invece tutto lo spazio che conosce in anticipo - puntatori e riferimenti, ad esempio, saranno sempre 32 o 64 bit (a seconda dell'architettura) e quindi se lo hai sostituito ( uno) da un puntatore o riferimento, le cose sarebbero grandi. Diciamo che sostituiamo 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;
};
Ora le cose vanno meglio. Un po '. main ()
dice ancora:
// file: main.cc
#include "A.h" // <-- Houston, we have a problem
#include
, a tutti gli effetti (se si estrae il preprocessore) copia semplicemente il file in .cc . Quindi, davvero, il .cc assomiglia a:
// file: partially_pre_processed_main.cc
class A {
B& _b_ref;
B* _b_ptr;
};
#include "B.h"
int main (...) {
A a;
}
Puoi capire perché il compilatore non può gestirlo - non ha idea di cosa sia B
- non ha mai visto il simbolo prima.
Quindi diciamo al compilatore di B
. Questa è nota come dichiarazione a termine , ed è discussa ulteriormente in questa risposta .
// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
A a;
}
Questo funziona . Non è fantastico . Ma a questo punto dovresti avere una comprensione del problema di riferimento circolare e di cosa abbiamo fatto per "risolvere". anche se la correzione è male.
Il motivo per cui questa correzione è sbagliata è perché la persona successiva a #include " A.h "
dovrà dichiarare B
prima che possano usarla e otterrà un terribile errore #include
. Quindi spostiamo la dichiarazione in A.h stessa.
// file: A.h
class B;
class A {
B* _b; // or any of the other variants.
};
E in B.h , a questo punto, puoi semplicemente #include " A.h "
direttamente.
// 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.
Altri suggerimenti
È possibile evitare errori di compilazione se si rimuovono le definizioni dei metodi dai file di intestazione e si lascia che le classi contengano solo le dichiarazioni dei metodi e le dichiarazioni / definizioni delle variabili. Le definizioni dei metodi devono essere inserite in un file .cpp (proprio come dice una linea guida sulle migliori pratiche).
Il lato negativo della seguente soluzione è (supponendo che tu abbia inserito i metodi nel file di intestazione per incorporarli) che i metodi non sono più incorporati dal compilatore e provare a usare la parola chiave inline produce errori di linker.
//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;
}
Cose da ricordare:
- Questo non funzionerà se
classe A
ha un oggetto diclasse B
come membro o viceversa. - La dichiarazione in avanti è la strada da percorrere.
- L'ordine delle dichiarazioni è importante (motivo per cui si stanno spostando le definizioni).
- Se entrambe le classi chiamano funzioni dell'altra, è necessario spostare le definizioni fuori.
Leggi le FAQ:
Sto rispondendo in ritardo a questa domanda, ma non esiste una risposta ragionevole fino ad oggi, nonostante sia una domanda popolare con risposte altamente votate ....
Best practice: intestazioni di dichiarazione a termine
Come illustrato dall'intestazione < iosfwd >
della libreria standard, il modo corretto di fornire dichiarazioni in avanti per gli altri è avere un intestazione dichiarazione in avanti . Ad esempio:
a.fwd.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*);
};
I manutentori delle librerie A
e B
dovrebbero essere responsabili della sincronizzazione delle intestazioni delle dichiarazioni forward con intestazioni e file di implementazione, quindi - per esempio - se il manutentore di "B" arriva e riscrive il codice per essere ...
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;
... quindi ricompilazione del codice per " A " verrà attivato dalle modifiche al b.fwd.h
incluso e dovrebbe essere completato in modo pulito.
Scarsa ma pratica comune: inoltra dichiarare roba in altre librerie
Dire - invece di usare un'intestazione di dichiarazione diretta come spiegato sopra - codice in ah
o a.cc
invece forward-dichiara classe B;
stesso:
- se
a.h
oa.cc
includesserob.h
in seguito:- la compilazione di A terminerà con un errore una volta che si arriva alla dichiarazione / definizione in conflitto di
B
(ovvero la modifica di cui sopra in B ha rotto A e tutti gli altri client che hanno abusato delle dichiarazioni in avanti, invece di lavorare in modo trasparente ).
- la compilazione di A terminerà con un errore una volta che si arriva alla dichiarazione / definizione in conflitto di
- altrimenti (se A alla fine non includeva
b.h
- possibile se A memorizza / passa in giro Bs con puntatore e / o riferimento)- costruisci strumenti basandosi sull'analisi
#include
e i timestamp dei file modificati non ricostruirannoA
(e il suo codice ulteriormente dipendente) dopo la modifica in B, causando errori in tempo di collegamento o tempo di esecuzione. Se B è distribuito come una DLL caricata in fase di runtime, codifica in " A " potrebbe non essere in grado di trovare i simboli di diversa mentalità in fase di esecuzione, che possono o meno essere gestiti abbastanza bene da innescare l'arresto ordinato o una funzionalità accettabilmente ridotta.
- costruisci strumenti basandosi sull'analisi
Se il codice di A ha specializzazioni di modello / "tratti" " per il vecchio B
, non avranno effetto.
Una volta ho risolto questo tipo di problema spostando tutti inline dopo la definizione della classe e mettendo #include
per le altre classi appena prima di inline nel file di intestazione. In questo modo, assicurarsi che tutte le definizioni + linee siano impostate prima che le linee siano analizzate.
In questo modo è possibile avere ancora un sacco di inline in entrambi (o più) file di intestazione. Ma è necessario che includa guardie .
In questo modo
// 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__ */
... e fare lo stesso in B.h
Ho scritto un post su questo argomento una volta: Risoluzione delle dipendenze circolari in C ++
La tecnica di base è disaccoppiare le classi usando le interfacce. Quindi nel tuo caso:
//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;
}
Ecco la soluzione per i modelli: Come gestire le dipendenze circolari con i modelli
L'indizio per risolvere questo problema è dichiarare entrambe le classi prima di fornire le definizioni (implementazioni). Non è possibile dividere la dichiarazione e la definizione in file separati, ma è possibile strutturarli come se fossero in file separati.
Il semplice esempio presentato su Wikipedia ha funzionato per me. (puoi leggere la descrizione completa su http://it.wikipedia.org/wiki /Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )
File '' 'a.h' '':
#ifndef A_H
#define A_H
class B; //forward declaration
class A {
public:
B* b;
};
#endif //A_H
File '' 'b.h' '':
#ifndef B_H
#define B_H
class A; //forward declaration
class B {
public:
A* a;
};
#endif //B_H
File '' 'main.cpp' '':
#include "a.h"
#include "b.h"
int main() {
A a;
B b;
a.b = &b;
b.a = &a;
}
Sfortunatamente, in tutte le risposte precedenti mancano alcuni dettagli. La soluzione corretta è un po 'ingombrante, ma questo è l'unico modo per farlo correttamente. E si ridimensiona facilmente, gestisce anche dipendenze più complesse.
Ecco come puoi farlo, mantenendo esattamente tutti i dettagli e l'usabilità:
- la soluzione è esattamente la stessa prevista inizialmente
- funzioni inline ancora in linea
- gli utenti di
A
eB
possono includere A.h e B.h in qualsiasi ordine
Crea due file, A_def.h, B_def.h. Conterranno solo la definizione di A
e B
:
// 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
E poi, A.h e B.h conterranno questo:
// 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
Nota che A_def.h e B_def.h sono " private " le intestazioni, gli utenti di A
e B
non devono usarli. L'intestazione pubblica è A.h e B.h.
In alcuni casi è possibile definire un metodo o un costruttore di classe B nel file di intestazione di classe A per risolvere le dipendenze circolari che coinvolgono le definizioni.
In questo modo è possibile evitare di dover inserire le definizioni nei file .cc
, ad esempio se si desidera implementare una libreria solo intestazione.
// 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();
}
Purtroppo non posso commentare la risposta di geza.
Non sta solo dicendo "metti le dichiarazioni in un'intestazione separata". Dice che devi versare le intestazioni di definizione della classe e le definizioni di funzione incorporate in diversi file di intestazione per consentire "dipendenze differite".
Ma la sua illustrazione non è davvero buona. Perché entrambe le classi (A e B) richiedono solo un tipo incompleto l'una dell'altra (campi / parametri puntatore).
Per capirlo meglio immaginare che la classe A abbia un campo di tipo B non B *. Inoltre le classi A e B vogliono definire una funzione inline con parametri dell'altro tipo:
Questo semplice codice non funzionerebbe:
// 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"
Si tradurrebbe nel seguente codice:
//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"
Questo codice non viene compilato perché B :: Do richiede un tipo completo di A che verrà definito in seguito.
Per assicurarsi che compili il codice sorgente dovrebbe apparire così:
//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
}
Questo è esattamente possibile con questi due file di intestazione per ogni classe che deve definire funzioni incorporate. L'unico problema è che le classi circolari non possono semplicemente includere " intestazione pubblica " ;.
Per risolvere questo problema vorrei suggerire un'estensione del preprocessore: #pragma process_pending_includes
Questa direttiva dovrebbe rinviare l'elaborazione del file corrente e completare tutte le inclusioni in sospeso.