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; }
c++
compiler-errors
circular-dependency
c++-faq
Autodidacta
fuente
fuente
Respuestas:
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.
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 guardarB
! ¿Cuál es el tamaño deB
entonces? Suficiente para almacenarA
! UpsClaramente 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
:Ahora las cosas están mejor. Algo.
main()
todavía dice:#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í:Puedes ver por qué el compilador no puede lidiar con esto, no tiene idea de qué
B
es, 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 .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 declararB
antes de poder usarla y obtendrá un terrible#include
error. Así que pasemos la declaración al Ah mismo.Y en Bh , en este punto, puedes hacerlo
#include "A.h"
directamente.HTH.
fuente
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.
fuente
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:
ah:
b.fwd.h:
bh:
Los mantenedores de las bibliotecas
A
yB
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 reescribe el código para que sea ...b.fwd.h:
bh:
... luego la recompilación del código para "A" se activará por los cambios a los incluidos
b.fwd.h
y 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.h
oa.cc
en lugar de hacia adelante Se declara aclass B;
sí mismo:a.h
oa.cc
incluyób.h
más tarde: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).b.h
, posible si A solo almacena / pasa alrededor de Bs por puntero y / o referencia)#include
análisis y las marcas de tiempo modificadas del archivo no se reconstruiránA
(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.fuente
a.fwd.h
ena.h
, para asegurar que se mantengan sincronizados. Falta el código de ejemplo donde se usan estas clases.a.h
yb.h
ambos 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. Dondeb.h
incluyea.h
emain.cpp
incluyeb.h
main.cpp
, pero es bueno que hayas documentado lo que debe contener en tu comentario. Saludos<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.#include
s).Cosas para recordar:
class A
tiene un objetoclass B
como miembro o viceversa.Lee las preguntas frecuentes:
fuente
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
#include
las 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
... y haciendo lo mismo en
B.h
fuente
B.h
primero?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:
fuente
virtual
tiene un impacto en el rendimiento del tiempo de ejecución.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.
fuente
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' '':
Archivo '' 'b.h' '':
Archivo '' 'main.cpp' '':
fuente
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:
A
yB
pueden incluir Ah y Bh en cualquier ordenCree dos archivos, A_def.h, B_def.h. Estas memorias contienen únicamente
A
's yB
' s definición:Y luego, Ah y Bh contendrán esto:
Tenga en cuenta que A_def.h y B_def.h son encabezados "privados", usuarios de
A
yB
no deben usarlos. El encabezado público es Ah y Bhfuente
A
la queB
estén disponibles las definiciones 's y ', la declaración directa no es suficiente).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
.cc
archivos, por ejemplo, si desea implementar una biblioteca de solo encabezado.fuente
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:
Resultaría en el siguiente código:
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í:
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.
fuente