una tipo serializar referencia objeto detectó c++ compiler-errors circular-dependency c++-faq

c++ - tipo - Resolver errores de compilación debido a la dependencia circular entre clases



referencia circular c# (9)

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 declaración y la definición en archivos separados, pero puede estructurarlos como si estuvieran en archivos separados.

A menudo me encuentro en una situación en la que me enfrento a varios errores de compilación / vinculador 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 frecuencia suficiente para que yo recuerde la solución a este problema para la próxima vez que vuelva a suceder.

Por lo tanto, para los fines de fácil recuperación en el futuro, voy a publicar un problema representativo y una solución junto con él. Por supuesto, las mejores soluciones son 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 "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; }


Cosas para recordar:

  • Esto no funcionará si la class A tiene un objeto de la class B como miembro o viceversa.
  • Declaración hacia adelante es el camino a seguir.
  • La orden de la declaración es importante (por lo que se están moviendo las definiciones).
    • Si ambas clases llaman a las funciones de la otra, tiene que sacar las definiciones.

Lea las preguntas frecuentes:


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

Aquí le mostramos cómo puede hacer esto, conservando exactamente todos los detalles y la facilidad de uso:

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

Cree 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 entonces, 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", los usuarios de A y B no deben usarlos. El encabezado público es Ah y Bh


El ejemplo simple 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; }


He escrito un post sobre esto una vez: Resolviendo dependencias circulares en c ++

La técnica básica es desacoplar las clases usando interfaces. Así que 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; }


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

Imagina que estás escribiendo un compilador. Y ves 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 .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 almacenar B ! ¿Cuál es el tamaño de B entonces? Suficiente para almacenar A ! Ups.

Claramente una referencia circular que debes romper.

Puede romperlo permitiendo que el compilador, en cambio, reserve tanto espacio como sabe acerca de los punteros y referencias, por ejemplo, siempre será de 32 o 64 bits (según la arquitectura) y, por lo tanto, si reemplaza (cualquiera de los dos) 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 todas las extensiones y propósitos (si saca el preprocesador) simplemente copia el archivo en .cc . Así que realmente, el .cc se parece a:

// 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 lo que es B , nunca antes había visto el símbolo.

Así que vamos a decirle al compilador sobre B Esto se conoce como una declaración hacia adelante , y se discute más adelante en esta respuesta .

// main.cc class B; #include "A.h" #include "B.h" 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 "solucionarlo", aunque la solución sea incorrecta.

La razón por la que esta solución es mala es porque la siguiente persona en #include "Ah" tendrá que declarar B antes de que puedan usarla y obtendrá un terrible error de #include . Así que movamos la declaración a Ah mismo.

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

Y en Bh , en este punto, puedes simplemente #include "Ah" 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.


Puede evitar errores de compilació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 de los métodos se deben colocar en un archivo .cpp (tal como dice una guía de mejores prácticas).

El lado negativo de la siguiente solución es (asumiendo que usted colocó los métodos en el archivo de encabezado para alinearlos) que el compilador ya no integra los métodos y al intentar usar la palabra clave en línea produce errores de vinculador.

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


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 poniendo el #include para las otras clases justo antes de las líneas en el archivo de encabezado. De esta manera, asegúrese de que todas las definiciones + inlines estén configuradas antes de analizar las inlines.

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

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 Bh


Me demoré en contestar esto, pero no hay una respuesta razonable hasta la fecha, a pesar de ser una pregunta popular con respuestas altamente calificadas ...

Mejores prácticas: encabezados de declaración hacia adelante

Como se ilustra en el encabezado <iosfwd> la biblioteca estándar, la forma correcta de proporcionar declaraciones a futuro para otros es tener un encabezado de declaración en adelante . 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 deben 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 vuelve a escribir el código para ...

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 en el b.fwd.h incluido y se completará de forma limpia.

Práctica pobre pero común: reenviar cosas en otras librerías

Diga - en lugar de usar un encabezado de declaración hacia adelante como se explicó anteriormente - el código en ah o a.cc lugar de reenviar declara class B; sí mismo:

  • Si ah o a.cc incluyó bh más tarde:
    • la compilación de A terminará con un error una vez que llegue a la declaración / definición de B conflicto (es decir, el cambio anterior a B rompió a A y cualquier otro cliente que abusara de las declaraciones en adelante, en lugar de trabajar de manera transparente).
  • de lo contrario (si A finalmente no incluyó bh , es posible si A simplemente almacena / pasa alrededor de Bs por puntero y / o referencia)
    • las herramientas de compilación basadas en el análisis de #include y las marcas de tiempo de los archivos modificados no reconstruirán A (y su código más dependiente) después del cambio a B, lo que causará errores en el momento del enlace o en el tiempo de ejecución. Si B se distribuye como un DLL cargado en tiempo de ejecución, el código en "A" puede fallar en encontrar los símbolos con diferentes destellos 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 la antigua B , no tendrán efecto.