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

14 minutos de lectura

Resolver errores de compilacion debido a la dependencia circular entre
Autodidacta

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

Entonces, para recordarlo fácilmente en el futuro, voy a 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;
    }
    

  • Al trabajar con Visual Studio, el /mostrarIncluye flag ayuda mucho a depurar este tipo de problemas.

    – limpiar

    12 de septiembre de 2012 a las 3:08


  • ¿Hay algo similar para el código de Visual Studio?

    – Erik

    9 de noviembre de 2021 a las 13:41

1647666372 166 Resolver errores de compilacion debido a la dependencia circular entre
Roosh

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ás compilando el .cc archivo (recuerde que el .cc y no el .h es la unidad de compilació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 ¿luego? Suficiente para almacenar A! UPS.

Claramente una referencia circular que debes romper.

Puede romperlo permitiendo que el compilador reserve tanto espacio como sepa por adelantado: los punteros y las referencias, por ejemplo, siempre serán de 32 o 64 bits (dependiendo de la arquitectura) y, por lo tanto, si reemplazó (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

#includepara todos los efectos (si quita el preprocesador), simplemente copie el archivo en el .cc. Así que realmente, el .cc parece:

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

Puede ver por qué el compilador no puede lidiar con esto: no tiene idea de qué B es – ni siquiera ha visto el símbolo antes.

Así que hablemos al compilador sobre B. Esto se conoce como declaración hacia adelantey se discute más adelante en esta respuesta.

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

Esta obras. No lo es estupendo. Pero en este punto, debe comprender el problema de la referencia circular y lo que hicimos para “arreglarlo”, aunque la solución es mala.

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

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

Y en bhen este punto, solo puedes #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; 
}

HH.

  • “Decirle al compilador acerca de B” se conoce como una declaración directa de B.

    -Peter Ajtai

    17 de noviembre de 2010 a las 1:57

  • ¡Dios mío! Se perdió por completo el hecho de que las referencias se conocen en términos de espacio ocupado. ¡Finalmente, ahora puedo diseñar correctamente!

    – kellogs

    7 de noviembre de 2011 a las 2:31

  • Pero aún no puedes usar ninguna función en B (como en la pregunta _b->Printt())

    – rango1

    17 de abril de 2013 a las 11:02

  • @sydan: No puedes. Resolución de dependencias circulares requiere Definiciones fuera de clase.

    – Ben Voigt

    11 de abril de 2015 a las 14:03

  • Pero necesito usar en A clase B como un tipo completo y en B clase A como un tipo completo. Al decir tipo completo, me refiero a llamar a una función desde un objeto de ese tipo. ¿Cómo lo haría? acabo de recibir un error, invalid use of incomplete type B in class A.

    – Silidrona

    5 de septiembre de 2017 a las 12:47

Resolver errores de compilacion debido a la dependencia circular entre
Autodidacta

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 declaraciones/definiciones de variables. Las definiciones de 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 tratar de usar la palabra clave en línea produce errores de vinculación.

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

  • Gracias. Esto resolvió el problema fácilmente. Simplemente moví las inclusiones circulares a los archivos .cpp.

    – Lenar Hoyt

    4 oct 2014 a las 18:16

  • ¿Qué sucede si tiene 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

    1 de septiembre de 2016 a las 12:55

  • Siempre incluye “Ah” y “Bh” juntos. ¿Por qué no incluye “Ah” en “Bh” y luego incluye solo “Bh” tanto en “A.cpp” como en “B.cpp”?

    – Gusev Slava

    30 de septiembre de 2018 a las 4:25

  • Gracias, buena respuesta para aquellos que necesitan esta interdependencia entre 2 clases y no pueden refactorizarla de manera diferente

    – HanniBaL90

    22 de diciembre de 2020 a las 14:34

1647666372 297 Resolver errores de compilacion debido a la dependencia circular entre
tony delroy

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

Práctica recomendada: encabezados de declaración de reenvío

Como se ilustra en la biblioteca estándar <iosfwd> encabezado, la forma correcta de proporcionar declaraciones hacia adelante para otros es tener un encabezado de declaración hacia 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 la A y B Cada una de las bibliotecas debe ser responsable de mantener sincronizados sus encabezados de declaración hacia adelante 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 <typename T> class Basic_B;
typedef Basic_B<char> B;

bh:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

…entonces la recompilación del código para “A” será desencadenada por los cambios en el incluido b.fwd.h y debe completarse limpiamente.


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

Digamos, en lugar de usar un encabezado de declaración de reenvío como se explicó anteriormente, código en a.h o a.cc en lugar de adelante-declara class B; sí mismo:

  • si a.h o a.cc incluyó b.h luego:
    • 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 abusara de las declaraciones hacia adelante, en lugar de trabajar de manera transparente).
  • de lo contrario (si A finalmente no incluyó b.h – posible si A solo almacena/pasa Bs por puntero y/o referencia)
    • construir herramientas apoyándose en #include el análisis y las marcas de tiempo de archivos modificados no se reconstruirán A (y su código dependiente adicional) después del cambio a B, lo que provoca 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, es posible que el código en “A” no encuentre los símbolos manipulados de manera diferente en el tiempo de ejecución, lo que puede o no manejarse lo suficientemente bien como para desencadenar un apagado ordenado o una funcionalidad aceptablemente reducida.

Si el código de A tiene especializaciones de plantilla / “rasgos” para el antiguo Bno surtirán efecto.

  • Esta es una forma realmente limpia de manejar las declaraciones de avance. Lo único “desventaja” estaría en los archivos adicionales. Supongo que siempre incluyes a.fwd.h en a.h, para asegurar que permanezcan sincronizados. Falta el código de ejemplo donde se usan estas clases. a.h y b.h será necesario incluir ambos 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.h incluye a.h y main.cpp incluye b.h

    – Muy lejos

    5 mayo 2017 a las 16:37


  • @Farway Right en todos los aspectos. no me molesté en mostrar main.cpp, pero es bueno que hayas documentado lo que debería contener en tu comentario. Salud

    – Tony Delroy

    5 mayo 2017 a las 20:42

  • Una de las mejores respuestas con una buena explicación detallada de por qué con los pros y los contras …

    – Francis Cugler

    16 de enero de 2018 a las 5:06

  • @RezaHajianpour: tiene sentido tener un encabezado de declaración de reenvío para todas las clases de las que desea reenviar declaraciones, circulares o no. Dicho esto, solo los querrá cuando: 1) incluir la declaración real sea costoso (o se pueda anticipar que lo será más tarde) (por ejemplo, incluye muchos encabezados que su unidad de traducción podría no necesitar de otro modo), y 2) el código del cliente 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 a los que se hace referencia desde muchos lugares, y <iostream> es mucho para incluir.

    – Tony Delroy

    23 de enero de 2019 a las 5:02

  • @RezaHajianpour: Creo que tiene la idea correcta, pero hay un problema terminológico con su declaración: “solo necesitamos que el tipo sea declarado” sería correcto. El tipo siendo declarado significa que se ha visto la declaración directa; es definido una vez que la definición completa ha sido analizada (y para eso usted mayo necesitar más #includes).

    – Tony Delroy

    25 de enero de 2019 a las 14:45


1647666372 135 Resolver errores de compilacion debido a la dependencia circular entre
dirkgently

Cosas para recordar:

  • Esto no funcionará si class A tiene un objeto de class B como miembro o viceversa.
  • La declaración hacia adelante 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 funciones de la otra, debe sacar las definiciones.

Lea las preguntas frecuentes:

Resolver errores de compilacion debido a la dependencia circular entre
épatel

Una vez resolví este tipo de problema moviendo todos en lineas después de la definición de clase y poniendo el #include para las otras clases justo antes de la en lineas en el archivo de cabecera. De esta manera, uno se asegura de que todas las definiciones + líneas estén configuradas antes de que se analicen las líneas.

Hacer esto hace que sea posible tener un montón de líneas en ambos (o en varios) archivos de encabezado. Pero es necesario tener incluir 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 B.h

  • ¿Por qué? Creo que es una solución elegante para un problema complicado… cuando uno quiere en línea. Si uno no quiere líneas, no debería haber escrito el código como si estuviera escrito desde el principio …

    – epatel

    10 de marzo de 2009 a las 20:01

  • ¿Qué sucede si un usuario incluye B.h ¿primero?

    – Señor Fooz

    18/03/2014 a las 16:00

  • Tenga en cuenta que su protector de encabezado está usando un identificador reservado, cualquier cosa con guiones bajos adyacentes dobles está reservada.

    – Lars Viklund

    10 de agosto de 2015 a las 15:09

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

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

  • ¿Por qué? Creo que es una solución elegante para un problema complicado… cuando uno quiere en línea. Si uno no quiere líneas, no debería haber escrito el código como si estuviera escrito desde el principio …

    – epatel

    10 de marzo de 2009 a las 20:01

  • ¿Qué sucede si un usuario incluye B.h ¿primero?

    – Señor Fooz

    18/03/2014 a las 16:00

  • Tenga en cuenta que su protector de encabezado está usando un identificador reservado, cualquier cosa con guiones bajos adyacentes dobles está reservada.

    – Lars Viklund

    10 de agosto de 2015 a las 15:09

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.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad