Pregunta

A menudo me encuentro a mí mismo en una situación en la que me estoy enfrentando múltiples compilación/enlazador de errores en un proyecto de C++ debido a algunas malas decisiones de diseño (hecha por alguien más :) ) que conducen a circular las dependencias entre las clases de C++ en diferentes archivos de encabezado (puede suceder también en el mismo archivo).Pero afortunadamente(?) esto no sucede a menudo suficiente para mí recordar la solución a este problema para que la próxima vez que esto suceda de nuevo.

Así que para los propósitos de fácil de recordar en el futuro, voy a publicar un representante problema y una solución junto con él.Las mejores soluciones son de curso de la recepción.


  • 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;
    }
    
¿Fue útil?

Solución

La forma de pensar sobre 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 compila el archivo .cc (recuerde que el .cc y no el .h es la unidad de compilación), necesita asignar espacio para el objeto A. Entonces, ¿cuánto espacio entonces? ¡Suficiente para almacenar B! ¿Cuál es el tamaño de main() entonces? ¡Suficiente para almacenar #include! ¡Vaya!

Claramente una referencia circular que debe romper.

Puede dividirlo permitiendo que el compilador reserve en su lugar tanto espacio como sepa sobre el inicio: 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 ( uno) por un puntero o referencia, las cosas serían geniales. Digamos que reemplazamos en #include "A.h":

// 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. <=> todavía dice:

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

<=>, para todos los fines y propósitos (si saca el preprocesador) simplemente copie 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 "B.h"
int main (...) {
  A a;
}

Puede ver por qué el compilador no puede lidiar con esto, no tiene idea de qué es <=>, nunca antes había visto el símbolo.

Entonces, hablemos al compilador sobre <=>. Esto se conoce como declaración de reenvío , y se trata más adelante en esta respuesta .

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

Este funciona . No es genial . Pero en este punto debe comprender el problema de referencia circular y lo que hicimos para & Quot; corregir & Quot; aunque la solución es mala.

La razón por la que esta solución es mala es porque la siguiente persona en <=> tendrá que declarar <=> antes de que pueda usarla y obtendrá un terrible error <=>. Así que pasemos la declaración a A.h en sí misma.

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

Y en B.h , en este punto, puede simplemente <=> 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.

Otros consejos

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 deben colocarse en un archivo .cpp (tal como dice una guía de mejores prácticas).

El lado negativo 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 e intenta 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;
}

Cosas para recordar:

  • Esto no funcionará si class A tiene un objeto de class B como miembro o viceversa.
  • La declaración de avance es un 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.

Lea las preguntas frecuentes:

Estoy finales de responder a esta pregunta, pero no hay una respuesta razonable a la fecha, a pesar de ser una pregunta popular, con muy upvoted respuestas....

Mejor práctica:adelante de la declaración de los encabezados

Como se ilustra en la biblioteca Estándar de la <iosfwd> encabezado, la forma correcta para proporcionar declaraciones forward para otros es tener un adelante encabezado de declaración.Por ejemplo:

una.fwd.h:

#pragma once
class A;

una.h:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

b.h:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

Los mantenedores de la A y B las bibliotecas deberían ser responsable de mantener su declaración encabezados en sincronía con sus cabeceras y archivos de implementación, de modo que - por ejemplo - si el mantenedor de la "B" viene y vuelve a escribir el código para que sea...

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

b.h:

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

...entonces la recompilación del código de "a" será provocada por los cambios a la inclusión del b.fwd.h y debe completar de manera limpia.


Pobre, pero la práctica común:adelante declarar cosas en otras librerías

Decir, en lugar de hacia adelante y a la declaración de encabezado, como se explicó anteriormente - código de a.h o a.cc en lugar de avanzar-declara class B; sí:

  • si a.h o a.cc hizo incluyen b.h más tarde:
    • compilación de Un terminará con un error una vez que se llega a los conflictos de la declaración o definición de B (es decir,el cambio anterior a B, se rompió Una y otros clientes abusar de declaraciones forward, en lugar de trabajar de forma transparente).
  • de lo contrario (si no eventualmente incluir b.h - posible si sólo almacena/pasa alrededor de Bs mediante punteros y/o de referencia)
    • herramientas de generación de confiar en #include análisis y archivo cambiado las marcas de tiempo no reconstruir A (y su código dependiente) después del cambio de a por B, causando errores en tiempo de vínculo o tiempo de ejecución.Si B es distribuido como un runtime DLL de carga, el código "A" puede no encontrar la manera diferente-alterados símbolos en tiempo de ejecución, que puede o no puede ser manejado bien suficiente para activar el apagado ordenado o aceptable de funcionalidad reducida.

Si el código de la plantilla de especializaciones / "rasgos" de la vieja B, de que no surta efecto.

Una vez resolví este tipo de problema moviendo todas las líneas después de la definición de la clase y colocando el #include para las otras clases justo antes de las líneas en el archivo de encabezado . De esta forma, 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 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

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

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). & # 8217; no es posible dividir la declaración 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 "a.h"
#include "b.h"

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

Por desgracia, todas las respuestas anteriores son los que faltan algunos detalles.La solución correcta es un poco engorroso, pero esta es la única manera de hacerlo correctamente.Y se escala fácilmente, maneja más complejas dependencias así.

He aquí cómo usted puede hacer esto, exactamente retener todos los detalles, y la capacidad de uso:

  • la solución es exactamente el mismo que el previsto originalmente
  • funciones en línea todavía en línea
  • los usuarios de A y B puede incluir A. h y B. h en cualquier orden

Crear dos archivos, A_def.h, B_def.h.Estos contienen sólo 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, A. h y B. h contendrá esta:

// 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 "privadas" de los encabezados, los usuarios de A y B no debe usarlos.El encabezado público es A. h y B. h.

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 implican definiciones. De esta forma, puede evitar tener que poner definiciones en .cc archivos, 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();
}

Desafortunadamente no puedo comentar la respuesta de geza.

No solo dice " presenta las declaraciones en un encabezado separado " ;. Dice que debe derramar los encabezados de definición de clase y las definiciones de función en línea en diferentes archivos de encabezado para permitir & Quot; dependencias anuladas & Quot ;.

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

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 & Quot; encabezado público & Quot ;.

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top