Resolver errores de compilación debido a la dependencia circular entre clases

353

A menudo me encuentro en una situación en la que me enfrento a múltiples errores de compilación / enlazador en un proyecto C ++ debido a algunas malas decisiones de diseño (hechas por otra persona :)) que conducen a dependencias circulares entre las clases C ++ en diferentes archivos de encabezado (también puede ocurrir en el mismo archivo) . Pero afortunadamente (?) Esto no sucede con la frecuencia suficiente para recordar la solución de este problema la próxima vez que vuelva a ocurrir.

Por lo tanto, para facilitar el recuerdo en el futuro, publicaré un problema representativo y una solución junto con él. Las mejores soluciones son, por supuesto, bienvenidas.


  • 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;
    }
Autodidacta
fuente
23
Cuando se trabaja con Visual Studio, el indicador / showIncludes ayuda mucho a depurar este tipo de problemas.
wip

Respuestas:

288

La forma de pensar en esto es "pensar como un compilador".

Imagina que estás escribiendo un compilador. Y ves un código como este.

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

Cuando está compilando el archivo .cc (recuerde que el .cc y no el .h es la unidad de compilación), debe asignar espacio para el objeto A. Entonces, ¿cuánto espacio entonces? ¡Suficiente para guardar B! ¿Cuál es el tamaño de Bentonces? Suficiente para almacenarA ! Ups

Claramente una referencia circular que debes romper.

Puede romperlo permitiendo que el compilador reserve en su lugar tanto espacio como sabe sobre el principio: los punteros y las referencias, por ejemplo, siempre serán de 32 o 64 bits (dependiendo de la arquitectura) y, por lo tanto, si lo reemplaza (cualquiera de ellos) por un puntero o referencia, las cosas serían geniales. Digamos que reemplazamos en A:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

Ahora las cosas están mejor. Algo. main()todavía dice:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, para todos los fines y propósitos (si saca el preprocesador) simplemente copie el archivo en el .cc . Entonces, realmente, el .cc se ve así:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

Puedes ver por qué el compilador no puede lidiar con esto, no tiene idea de qué Bes, nunca antes había visto el símbolo.

Entonces, hablemos sobre el compilador B. Esto se conoce como declaración adelantada , y se trata más adelante en esta respuesta .

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

Esto funciona . No es grande . Pero en este punto, debe comprender el problema de referencia circular y lo que hicimos para "solucionarlo", aunque la solución es mala.

La razón por la que esta solución es mala es porque la siguiente persona #include "A.h"tendrá que declarar Bantes de poder usarla y obtendrá un terrible #includeerror. Así que pasemos la declaración al Ah mismo.

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

Y en Bh , en este punto, puedes hacerlo #include "A.h"directamente.

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

Roosh
fuente
20
"Contarle al compilador sobre B" se conoce como una declaración adelantada de B.
Peter Ajtai
8
¡Dios mio! Perdí totalmente el hecho de que las referencias son conocidas en términos de espacio ocupado. ¡Finalmente, ahora puedo diseñar correctamente!
Kellogs
47
Pero aún así, no puede usar ninguna función en B (como en la pregunta _b-> Printt ())
rank1
3
Este es el problema que estoy teniendo. ¿Cómo incorpora las funciones con la declaración hacia adelante sin reescribir completamente el archivo de encabezado?
sydan
101

Puede evitar errores de compilación si elimina las definiciones de método de los archivos de encabezado y deja que las clases contengan solo las declaraciones de método y las declaraciones / definiciones de variables. Las definiciones de los métodos deben colocarse en un archivo .cpp (tal como dice una guía de mejores prácticas).

La desventaja de la siguiente solución es (suponiendo que haya colocado los métodos en el archivo de encabezado para alinearlos) que el compilador ya no alinea los métodos y que tratar de usar la palabra clave en línea produce errores de enlace.

//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;
}
Autodidacta
fuente
Gracias. Esto resolvió el problema fácilmente. Simplemente moví la circular incluye a los archivos .cpp.
Lenar Hoyt
3
¿Qué pasa si tienes un método de plantilla? Entonces realmente no puede moverlo a un archivo CPP a menos que cree una instancia de las plantillas manualmente.
Malcolm
Siempre incluye "Ah" y "Bh" juntos. ¿Por qué no incluye "Ah" en "Bh" y luego incluye "Bh" en "A.cpp" y "B.cpp"?
Gusev Slava
28

Llego tarde a responder esto, pero no hay una respuesta razonable hasta la fecha, a pesar de ser una pregunta popular con respuestas altamente votadas ...

Mejor práctica: encabezados de declaración directa

Como se ilustra en el <iosfwd>encabezado de la biblioteca estándar , la forma correcta de proporcionar declaraciones de reenvío para otros es tener un encabezado de declaración de reenvío . Por ejemplo:

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

Los mantenedores de las bibliotecas Ay Bdeben ser responsables de mantener sus encabezados de declaración hacia adelante sincronizados con sus encabezados y archivos de implementación, por ejemplo, si el mantenedor de "B" aparece y reescribe el código para que sea ...

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;

... luego la recompilación del código para "A" se activará por los cambios a los incluidos b.fwd.hy debe completarse limpiamente.


Práctica pobre pero común: adelante declarar cosas en otras bibliotecas

Decir - en lugar de utilizar una cabecera declaración adelantada como se explicó anteriormente - código en a.ho a.ccen lugar de hacia adelante Se declara a class B;sí mismo:

  • si a.ho a.ccincluyó b.hmás tarde:
    • la compilación de A terminará con un error una vez que llegue a la declaración / definición conflictiva de B(es decir, el cambio anterior a B rompió A y cualquier otro cliente que abusa de las declaraciones hacia adelante, en lugar de trabajar de manera transparente).
  • de lo contrario (si A no incluyó eventualmente b.h, posible si A solo almacena / pasa alrededor de Bs por puntero y / o referencia)
    • las herramientas de compilación que se basan en el #includeanálisis y las marcas de tiempo modificadas del archivo no se reconstruirán A(y su código dependiente) después del cambio a B, causando errores en el tiempo de enlace o en el tiempo de ejecución. Si B se distribuye como una DLL cargada en tiempo de ejecución, el código en "A" puede fallar al encontrar los símbolos maltratados de manera diferente en tiempo de ejecución, que pueden o no manejarse lo suficientemente bien como para activar el apagado ordenado o una funcionalidad aceptablemente reducida.

Si el código de A tiene especializaciones de plantilla / "rasgos" para el anterior B, no tendrán efecto.

Tony Delroy
fuente
2
Esta es una forma realmente limpia de manejar las declaraciones directas. La única "desventaja" estaría en los archivos adicionales. Supongo que siempre se incluye a.fwd.hen a.h, para asegurar que se mantengan sincronizados. Falta el código de ejemplo donde se usan estas clases. a.hy b.hambos deberán incluirse ya que no funcionarán de forma aislada: `` `// // main.cpp #include" ah "#include" bh "int main () {...}` `` O uno de ellos necesita estar completamente incluido en el otro como en la pregunta de apertura. Donde b.hincluye a.he main.cppincluyeb.h
Farway
2
@Farway Right en todos los aspectos. No me molesté en mostrar main.cpp, pero es bueno que hayas documentado lo que debe contener en tu comentario. Saludos
Tony Delroy
1
Una de las mejores respuestas con una buena explicación detallada de por qué con lo que se debe y no se debe debido a los pros y los contras ...
Francis Cugler
1
@RezaHajianpour: tiene sentido tener un encabezado de declaración de reenvío para todas las clases de las que desee declaraciones de reenvío, circulares o no. Dicho esto, solo los querrá cuando: 1) incluir la declaración real sea (o pueda anticiparse que se volverá más tarde) costosa (por ejemplo, incluye muchos encabezados que su unidad de traducción podría no necesitar de otra manera), y 2) el código del cliente es Es probable que pueda hacer uso de punteros o referencias a los objetos. <iosfwd>Es un ejemplo clásico: puede haber algunos objetos de flujo referenciados desde muchos lugares, y <iostream>es mucho lo que se debe incluir.
Tony Delroy el
1
@RezaHajianpour: Creo que tienes la idea correcta, pero hay un problema terminológico con tu declaración: "solo necesitamos que se declare el tipo " sería correcto. El tipo que se declara significa que se ha visto la declaración directa; se define una vez que se ha analizado la definición completa (y para eso puede que necesite más #includes).
Tony Delroy
20

Cosas para recordar:

  • Esto no funcionará si class Atiene un objeto class Bcomo miembro o viceversa.
  • Declaración de avance es el camino a seguir.
  • El orden de la declaración es importante (es por eso que está eliminando las definiciones).
    • Si ambas clases llaman a funciones de la otra, debe mover las definiciones.

Lee las preguntas frecuentes:

Dirkgently
fuente
1
los enlaces que proporcionó ya no funcionan, ¿conoce los nuevos a los que hace referencia?
Ramya Rao
11

Una vez resolví este tipo de problema moviendo todas las líneas en línea después de la definición de la clase y colocando #includelas otras clases justo antes del líneas en el archivo de encabezado. De esta manera, asegúrese de que todas las definiciones + líneas estén configuradas antes de analizar las líneas.

Hacer esto hace posible que todavía tenga un montón de líneas en ambos (o múltiples) archivos de encabezado. Pero es necesario tener guardias incluidos .

Me gusta esto

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

... y haciendo lo mismo en B.h

epatel
fuente
¿Por qué? Creo que es una solución elegante para un problema complicado ... cuando uno quiere en línea. Si uno no quiere inlines uno no debería haber escrito el código como si hubiera sido escrito desde el principio ...
epatel
¿Qué sucede si un usuario incluye B.hprimero?
Sr. Fooz
3
Tenga en cuenta que su protector de encabezado está utilizando un identificador reservado, cualquier cosa con guiones bajos adyacentes dobles está reservada.
Lars Viklund
6

He escrito una publicación sobre esto una vez: Resolver dependencias circulares en c ++

La técnica básica es desacoplar las clases usando interfaces. Entonces en tu 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;
}
Eduard Wirch
fuente
2
Tenga en cuenta que el uso de interfaces y virtualtiene un impacto en el rendimiento del tiempo de ejecución.
cemper93
4

Aquí está la solución para las plantillas: cómo manejar dependencias circulares con plantillas

La clave para resolver este problema es declarar ambas clases antes de proporcionar las definiciones (implementaciones). No es posible dividir la declaración y la definición en archivos separados, pero puede estructurarlos como si estuvieran en archivos separados.

Tatiana
fuente
2

El simple ejemplo presentado en Wikipedia funcionó para mí. (puede leer la descripción completa en http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )

Archivo '' 'a.h' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

Archivo '' 'b.h' '':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

Archivo '' 'main.cpp' '':

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}
madx
fuente
1

Desafortunadamente, a todas las respuestas anteriores les faltan algunos detalles. La solución correcta es un poco engorrosa, pero esta es la única forma de hacerlo correctamente. Y se escala fácilmente, maneja dependencias más complejas también.

Así es como puede hacer esto, conservando exactamente todos los detalles y usabilidad:

  • la solución es exactamente la misma que la prevista originalmente
  • funciones en línea todavía en línea
  • usuarios Ay Bpueden incluir Ah y Bh en cualquier orden

Cree dos archivos, A_def.h, B_def.h. Estas memorias contienen únicamente A's y B' s definición:

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

Y luego, Ah y Bh contendrán esto:

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

Tenga en cuenta que A_def.h y B_def.h son encabezados "privados", usuarios de Ay Bno deben usarlos. El encabezado público es Ah y Bh

geza
fuente
1
¿Tiene esto alguna ventaja sobre la solución de Tony Delroy ? Ambos se basan en encabezados "auxiliares", pero los de Tony son más pequeños (solo contienen la declaración directa) y parecen funcionar de la misma manera (al menos a primera vista).
Fabio dice reinstalar a Mónica el
1
Esa respuesta no resuelve el problema original. Simplemente dice "presentar declaraciones en un encabezado separado". No se trata de resolver la dependencia circular (la pregunta necesita una solución en Ala que Bestén disponibles las definiciones 's y ', la declaración directa no es suficiente).
geza
0

En algunos casos, es posible definir un método o un constructor de clase B en el archivo de encabezado de clase A para resolver dependencias circulares que impliquen definiciones. De esta forma, puede evitar tener que poner definiciones en los .ccarchivos, por ejemplo, si desea implementar una biblioteca de solo encabezado.

// 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();
}
jkoendev
fuente
0

Lamentablemente no puedo comentar la respuesta de geza.

No solo está diciendo "presentar declaraciones en un encabezado separado". Él dice que hay que derramar encabezados de definición de clase y definiciones de funciones en línea en diferentes archivos de encabezado para permitir "dependencias anuladas".

Pero su ilustración no es realmente buena. Debido a que ambas clases (A y B) solo necesitan un tipo incompleto entre sí (campos de puntero / parámetros).

Para entenderlo mejor, imagine que la clase A tiene un campo de tipo B, no B *. Además, las clases A y B desean definir una función en línea con parámetros del otro tipo:

Este código simple no funcionaría:

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

Resultaría en el siguiente código:

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

Este código no se compila porque B :: Do necesita un tipo completo de A que se define más adelante.

Para asegurarse de que compila el código fuente debería verse así:

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

Esto es exactamente posible con estos dos archivos de encabezado para cada clase que necesita definir funciones en línea. El único problema es que las clases circulares no pueden incluir simplemente el "encabezado público".

Para resolver este problema, me gustaría sugerir una extensión de preprocesador: #pragma process_pending_includes

Esta directiva debería diferir el procesamiento del archivo actual y completar todas las inclusiones pendientes.

Bernd Baumanns
fuente