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

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

Por lo tanto, a los fines de recordar fácilmente en el futuro, voy a publicar un problema representativo y una solución junto con él. Mejores soluciones son, por supuesto, bienvenidas.


  • Ah

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

  • Bh

     #include "Ah" 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 "Bh" #include  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; } 

La forma de pensar sobre esto es “pensar como un comstackdor”.

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

 // file: Ah class A { B _b; }; // file: Bh class B { A _a; }; // file main.cc #include "Ah" #include "Bh" int main(...) { A a; } 

Cuando está comstackndo el archivo .cc (recuerde que el archivo .cc y no el .h es la unidad de comstackción), necesita asignar espacio para el objeto A Entonces, bueno, ¿cuánto espacio entonces? ¡Suficiente para almacenar B ! ¿Cuál es el tamaño de B entonces? ¡Suficiente para almacenar A ! Oops.

Claramente, una referencia circular que debes romper.

Puede romperlo permitiendo que el comstackdor reserve todo el espacio que conoce desde el principio: los punteros y las referencias, por ejemplo, siempre serán de 32 o 64 bits (dependiendo de la architecture) y, por lo tanto, si reemplazó (uno) por un puntero o referencia, las cosas serían geniales. Digamos que reemplazamos en A :

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

Ahora las cosas son mejores Algo. main() todavía dice:

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

#include , para todas las extensiones y propósitos (si saca el preprocesador) simplemente copia el archivo en .cc . Entonces realmente, el .cc se ve así:

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

Puedes ver por qué el comstackdor no puede manejar esto - no tiene idea de qué es B - nunca antes ha visto el símbolo.

Entonces, digamos al comstackdor sobre B Esto se conoce como una statement avanzada , y se trata más adelante en esta respuesta .

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

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

La razón por la que esta solución es incorrecta es porque la siguiente persona que #include "Ah" tendrá que declarar B antes de que puedan usarlo y obtendrá un terrible error #include . Así que llevemos la statement al Ah mismo.

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

Y en Bh , en este punto, puedes simplemente #include "Ah" directamente.

 // file: Bh #include "Ah" class B { // note that this is cool because the compiler knows by this time // how much space A will need. A _a; } 

HTH.

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

El inconveniente de la siguiente solución es (suponiendo que haya colocado los métodos en el archivo de encabezado para alinearlos) que el comstackdor ya no inline los métodos y tratar de usar la palabra clave inline produce errores de enlazador.

 //Ah #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 //Bh #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 "Ah" #include "Bh" #include  using namespace std; A::A(int val) :_val(val) { } void A::SetB(B *b) { _b = b; cout<<"Inside SetB()"<Print(); } void A::Print() { cout<<"Type:A val="<<_val< using namespace std; B::B(double val) :_val(val) { } void B::SetA(A *a) { _a = a; cout<<"Inside SetA()"<Print(); } void B::Print() { cout<<"Type:B val="<<_val< 

Cosas para recordar:

  • Esto no funcionará si la class A tiene un objeto de class B como miembro o viceversa.
  • La statement directa es el camino a seguir.
  • El orden de la statement es importante (razón por la cual está eliminando las definiciones).
    • Si ambas clases llaman funciones de la otra, debe mover las definiciones.

Lea las preguntas frecuentes:

  • ¿Cómo puedo crear dos clases que ambas se conozcan?
  • ¿Qué consideraciones especiales son necesarias cuando se utilizan declaraciones avanzadas con objetos miembros?
  • ¿Qué consideraciones especiales son necesarias cuando se utilizan declaraciones directas con funciones en línea?

Una vez resolví este tipo de problema moviendo todas las líneas después de la definición de clase y colocando el #include para las otras clases justo antes de las líneas en el archivo de encabezado. De esta forma, uno se asegura de que todas las definiciones + líneas estén establecidas antes de que las líneas en línea sean analizadas.

Al hacer esto, es posible tener un montón de líneas en ambos (o múltiples) archivos de encabezado. Pero es necesario incluir guardias .

Me gusta esto

 // File: Ah #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 "Bh" 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< 

... y haciendo lo mismo en Bh

Llegué tarde a responder esto, pero no hay una respuesta razonable hasta la fecha, a pesar de ser una pregunta popular con respuestas muy vistos.

Buena práctica: encabezados de statement adelantada

Como se ilustra en el encabezado la biblioteca estándar, la forma correcta de proporcionar declaraciones para otros es tener un encabezado de statement directa . 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 A y B deberían ser responsables de mantener sus encabezados de statement adelantada sincronizados con sus encabezados y archivos de implementación, por lo que, por ejemplo, si el mantenedor de “B” aparece y reescribe el código para que sea …

b.fwd.h:

 template  class Basic_B; typedef Basic_B B; 

bh:

 template  class Basic_B { ...class definition... }; typedef Basic_B B; 

… luego la recomstackción del código para “A” se desencadenará por los cambios al b.fwd.h incluido y se completará limpiamente.


Práctica mala pero común: reenviar cosas declaradas en otras libs

Digamos que, en lugar de usar un encabezado de statement hacia delante como se explicó anteriormente, el código en ah o a.cc vez de delante declara la class B; sí mismo:

  • si ah o a.cc incluyeron bh después:
    • la comstackción de A terminará con un error una vez que llegue a la statement / definición conflictiva de B (es decir, el cambio anterior a B rompió A y cualquier otro cliente abuse de las declaraciones avanzadas, en lugar de trabajar de forma transparente).
  • de lo contrario (si A finalmente no incluyó bh , es posible si A solo almacena / pasa alrededor de Bs por puntero y / o referencia)
    • las herramientas de comstackción que se basan en el análisis de #include y las marcas de tiempo modificadas del archivo no reconstruirán A (y su código dependiente adicional) después del cambio a B, lo que ocasionará errores en el tiempo del 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 no encontrar los símbolos diferentes en el tiempo de ejecución, que pueden o no manejarse lo suficientemente bien como para desencadenar un cierre ordenado o una funcionalidad aceptablemente reducida.

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

Escribí una publicación sobre esto una vez: resolviendo 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; } //Ah #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<Print(); } void Print() { cout<<"Type:B val="<<_val< #include "Ah" #include "Bh" 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; } 

Aquí está la solución para 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 statement y la definición en archivos separados, pero puede estructurarlos como si estuvieran en archivos separados.

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 "ah" #include "bh" int main() { A a; B b; ab = &b; ba = &a; } 

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, también maneja dependencias más complejas.

A continuación, le mostramos cómo puede hacerlo, conservando exactamente todos los detalles y la facilidad de uso:

  • la solución es exactamente la misma que originalmente se pretendía
  • funciones en línea todavía en línea
  • los usuarios de A y B pueden incluir Ah y Bh en cualquier orden

Crea dos archivos, A_def.h, B_def.h. Estos contendrán solo la definición de A y 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 

Y luego, Ah y Bh contendrán esto:

 // Ah #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<Print(); } inline void B::Print() { cout<<"Type:B val="<<_val< 

Tenga en cuenta que A_def.hy B_def.h son encabezados "privados", los usuarios de A y B no deberían usarlos. El encabezado público es Ah y Bh