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;
    }
    
È stato utile?

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 di classe 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 o a.cc includessero b.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 ).
  • 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 ricostruiranno A (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.

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 e B 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top