Risolvi errori di compilazione dovuti alla dipendenza circolare tra le classi


353

Mi trovo spesso in una situazione in cui sto affrontando 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 (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. Le soluzioni migliori sono ovviamente benvenute.


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

23
Quando si lavora con Visual Studio, il flag / showIncludes aiuta molto a eseguire il debug di questo tipo di problemi.
pulire il

Risposte:


288

Il modo di pensarci è "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), è necessario allocare spazio per l'oggetto A. Allora, quanto spazio allora? Abbastanza da conservare B! Qual è la dimensione di Ballora? Abbastanza da conservare A! Ops.

Chiaramente un riferimento circolare che devi rompere.

Puoi romperlo consentendo al compilatore di riservare invece tutto lo spazio che sa in anticipo - puntatori e riferimenti, ad esempio, saranno sempre 32 o 64 bit (a seconda dell'architettura) e quindi se hai sostituito (uno dei due) con un puntatore o un 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;
};

Adesso 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ò farcela - non ha idea di cosa Bsia - non ha mai visto il simbolo prima.

Quindi parliamo del compilatore 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 "risolverlo", sebbene la correzione sia sbagliata.

Il motivo per cui questa correzione è sbagliata è perché la prossima persona #include "A.h"dovrà dichiarare Bprima di poterla usare e otterrà un terribile #includeerrore. Quindi spostiamo la dichiarazione in Ah stessa.

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

E in Bh , 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.


20
"Raccontare il compilatore su B" è noto come una dichiarazione in avanti di B.
Peter Ajtai,

8
Oh mio Dio! totalmente mancato il fatto che i riferimenti sono noti in termini di spazio occupato. Finalmente ora posso progettare correttamente!
Kellogs

47
Ma comunque non puoi usare nessuna funzione su B (come nella domanda _b-> Printt ())
rank1

3
Questo è il problema che sto riscontrando. Come si inseriscono le funzioni con la dichiarazione forward senza riscrivere completamente il file di intestazione?
Sydney


101

È 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 dovrebbero 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 il tentativo di utilizzare 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;
}

Grazie. Questo ha risolto facilmente il problema. Ho semplicemente spostato le circolari incluse nei file .cpp.
Lenar Hoyt,

3
Cosa succede se si dispone di un metodo modello? Quindi non puoi davvero spostarlo in un file CPP a meno che non installi manualmente i modelli.
Malcolm,

Includete sempre "Ah" e "Bh" insieme. Perché non includi "Ah" in "Bh" e poi includi solo "Bh" in "A.cpp" e "B.cpp"?
Gusev Slava,

28

Sto rispondendo in ritardo a questo, ma non esiste una risposta ragionevole fino ad oggi, nonostante sia una domanda popolare con risposte altamente votate ....

Best practice: intestazioni delle dichiarazioni forward

Come illustrato dall'intestazione della libreria Standard <iosfwd>, il modo corretto di fornire dichiarazioni in avanti per gli altri è avere un'intestazione di dichiarazione in avanti . Per esempio:

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

I manutentori delle librerie Ae Bdovrebbero essere responsabili della sincronizzazione delle intestazioni delle dichiarazioni anticipate con le intestazioni e i 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;

BH:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

... quindi la ricompilazione del codice per "A" verrà attivata dalle modifiche all'incluso b.fwd.he dovrebbe essere completata in modo pulito.


Scarsa ma pratica comune: inoltra dichiarare roba in altre librerie

Dì - invece di usare un'intestazione di dichiarazione diretta come spiegato sopra - il codice in a.ho a.ccinvece inoltra class B;se stesso:

  • se a.ho a.ccincludesse b.hpiù tardi:
    • la compilazione di A termina con un errore una volta che si arriva alla dichiarazione / definizione in conflitto di B(ovvero la modifica di cui sopra a B ha rotto A e qualsiasi altro client che ha abusato delle dichiarazioni a termine, invece di lavorare in modo trasparente).
  • altrimenti (se A alla fine non includesse b.h- possibile se A memorizza / passa intorno a Bs per puntatore e / o riferimento)
    • gli strumenti di costruzione basati #includesull'analisi e i timestamp dei file modificati non verranno ricostruiti A(e il suo codice ulteriormente dipendente) dopo la modifica in B, causando errori al momento del collegamento o al momento dell'esecuzione. Se B è distribuito come una DLL caricata in fase di runtime, il codice in "A" potrebbe non riuscire a trovare i simboli con una diversa virgola in fase di runtime, che possono o meno essere gestiti abbastanza bene da innescare l'arresto ordinato o ridurre la funzionalità in modo accettabile.

Se il codice di A ha specializzazioni / "tratti" modello per il vecchio B, non avranno effetto.


2
Questo è un modo davvero pulito per gestire le dichiarazioni a termine. L'unico "svantaggio" sarebbe nei file extra. Presumo che tu includa sempre a.fwd.hin a.h, per assicurarti che rimangano sincronizzati. Manca il codice di esempio in cui vengono utilizzate queste classi. a.he b.hdovranno entrambi essere inclusi poiché non funzioneranno isolatamente: `` `//main.cpp #include" ah "#include" bh "int main () {...}` `` O uno di loro deve essere completamente incluso nell'altro come nella domanda iniziale. Dove b.hinclude a.he main.cppincludeb.h
Farway

2
@Farway Proprio sotto tutti i punti di vista. Non mi sono preoccupato di mostrare main.cpp, ma è bello che tu abbia documentato cosa dovrebbe contenere nel tuo commento. Saluti
Tony Delroy,

1
Una delle risposte migliori con una bella spiegazione dettagliata del perché con le cose da fare e da non fare a causa di pro e contro ...
Francis Cugler

1
@RezaHajianpour: ha senso avere un'intestazione di dichiarazione in avanti per tutte le classi di cui si desidera dichiarazioni in avanti, circolari o meno. Detto questo, li vorrai solo quando: 1) inclusa la dichiarazione effettiva è (o si prevede che diventerà successivamente) costosa (ad esempio include molte intestazioni che la tua unità di traduzione potrebbe non aver bisogno altrimenti), e 2) il codice cliente è probabilmente in grado di utilizzare puntatori o riferimenti agli oggetti. <iosfwd>è un classico esempio: possono esserci alcuni oggetti stream referenziati da molti luoghi ed <iostream>è molto da includere.
Tony Delroy,

1
@RezaHajianpour: Penso che tu abbia l'idea giusta, ma c'è un problema terminologico con la tua affermazione: "abbiamo solo bisogno del tipo da dichiarare " sarebbe giusto. Il tipo dichiarato significa che è stata vista la dichiarazione diretta; viene definito una volta analizzata la definizione completa (e per questo potrebbe essere necessario un numero maggiore di #includes).
Tony Delroy,

20

Cose da ricordare:

  • Questo non funzionerà se class Aha un oggetto class Bcome 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:


1
i link forniti non funzionano più, ti capita di conoscere quelli nuovi a cui fare riferimento?
Ramya Rao,

11

Una volta ho risolto questo tipo di problema spostando tutte le righe dopo la definizione della classe e inserendo #includele altre classi appena prima delle righe 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 mucchio di inline in entrambi (o più) file di intestazione. Ma è necessario includere guardie .

Come questo

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


Perché? Penso che sia una soluzione elegante a un problema difficile ... quando si vogliono inline. Se uno non vuole inline non dovrebbe aver scritto il codice come se fosse stato scritto dall'inizio ...
epatel

Cosa succede se un utente include per B.hprimo?
Fooz,

3
Si noti che la protezione dell'intestazione utilizza un identificatore riservato, qualsiasi cosa con doppio trattino basso adiacente è riservata.
Lars Viklund,

6

Ho scritto un post su questo una volta: Risolvere dipendenze circolari in c ++

La tecnica di base consiste nel disaccoppiare le classi utilizzando 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;
}

2
Si noti che l'uso di interfacce e virtualha un impatto sulle prestazioni di runtime.
cemper93,

4

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.


2

Il semplice esempio presentato su Wikipedia ha funzionato per me. (puoi leggere la descrizione completa su http://en.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;
}

1

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 in linea ancora in linea
  • gli utenti di Ae Bpossono includere Ah e Bh in qualsiasi ordine

Creare due file, A_def.h, B_def.h. Questi conterranno solo la definizione di " Ae 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, Ah e Bh 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

Si noti che A_def.h e B_def.h sono intestazioni "private", utenti di Ae Bnon dovrebbero usarle. L'intestazione pubblica è Ah e Bh


1
Questo ha dei vantaggi rispetto alla soluzione di Tony Delroy ? Entrambi si basano su intestazioni "helper", ma quelle di Tony sono più piccole (contengono solo la dichiarazione in avanti) e sembrano funzionare allo stesso modo (almeno a prima vista).
Fabio dice di reintegrare Monica il

1
Questa risposta non risolve il problema originale. Dice semplicemente "metti le dichiarazioni in un'intestazione separata". Nulla sulla risoluzione della dipendenza circolare (la domanda necessita di una soluzione in cui la definizione di " Ae B" è disponibile, la dichiarazione diretta non è sufficiente).
geza,

0

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 definizioni nei .ccfile, 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();
}

0

Purtroppo non posso commentare la risposta di Geza.

Non sta solo dicendo "metti le dichiarazioni in un'intestazione separata". Dice che è necessario riversare le intestazioni delle definizioni di classe e le definizioni delle funzioni 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"

Ciò comporterebbe il 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
}

Ciò è esattamente possibile con questi due file di intestazione per ogni classe che deve definire le funzioni incorporate. L'unico problema è che le classi circolari non possono semplicemente includere "l'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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.