Pergunta

Muitas vezes eu me encontrar em uma situação onde eu estou enfrentando vários erros de compilação / vinculador em um C ++ projeto devido a algumas más decisões de design (feitas por outra pessoa :)) que levam a dependências circulares entre C ++ classes em diferentes arquivos de cabeçalho < em> (pode acontecer também no mesmo arquivo) . Mas, felizmente (?) Isso não acontece com freqüência suficiente para me lembrar a solução para este problema para a próxima vez que isso acontecer novamente.

Assim, para os fins de recordação fácil no futuro eu vou postar um problema representativo e uma solução junto com ele. Melhores soluções são bem-vindos de curso.


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

Solução

A maneira de pensar sobre isso é a "pensar como um compilador".

Imagine que você está escrevendo um compilador. E você vê um 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;
}

Quando você está compilando o .cc arquivo (lembre-se que o .cc e não o .h é a unidade de compilação), você precisa alocar espaço para o objeto A. Então, assim, a quantidade de espaço, então? Suficiente para armazenar B! Qual é o tamanho de B então? Suficiente para armazenar A! Opa.

É evidente que uma referência circular que você deve quebrar.

Você pode quebrá-lo, permitindo que o compilador em vez de reserva tanto espaço quanto ele sabe sobre iniciais - ponteiros e referências, por exemplo, será sempre 32 ou 64 bits (dependendo da arquitectura) e por isso, se você substituiu (ou um) por um ponteiro ou referência, as coisas seria ótimo. Digamos que substituir em A:

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

Agora as coisas estão melhores. Um pouco. main() ainda diz:

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

#include, para todas as extensões e propósitos (se você tomar o pré-processador para fora) apenas copia o arquivo para o .cc . Então, realmente, o .cc se parece com:

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

Você pode ver porque o compilador não pode lidar com isso - ele não tem idéia do que B é -. Nunca tenha sequer visto o símbolo antes

Então, vamos dizer ao compilador sobre B. Isto é conhecido como um frente declaração , e é discutido mais adiante em this resposta .

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

Este funciona . Não é grande . Mas neste momento você deve ter uma compreensão do problema de referência circular e que fizemos a "solução" que, ainda que a correção é ruim.

A razão desta correção é ruim é porque a próxima pessoa a #include "A.h" terá de declarar B antes que eles possam usá-lo e receberá um erro #include terrível. Portanto, vamos passar a declaração em A.H. em si.

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

E B.h , neste ponto, você pode apenas #include "A.h" diretamente.

// 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.

Outras dicas

Você pode evitar erros de compilação se você remover as definições de métodos dos arquivos de cabeçalho e deixar as classes contêm apenas as declarações de método e declarações de variáveis ??/ definições. As definições de métodos devem ser colocados em um arquivo .cpp (apenas como uma melhor orientação prática diz).

O lado negativo da seguinte solução é (supondo que você tinha colocado os métodos no arquivo de cabeçalho para inline-los) que os métodos não são mais inlined pelo compilador e tentando usar a palavra-chave inline produz erros 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;
}

Coisas para lembrar:

  • Isto não vai funcionar se class A tem um objeto de class B como um membro ou vice-versa.
  • declaração Forward é caminho a percorrer.
  • Ordem de assuntos de declaração (que é por isso que você está se movendo para fora das definições).
    • Se ambas as classes chamar funções do outro, você tem que mover as definições de fora.

Leia o FAQ:

Eu estou atrasado responder isso, mas não há uma resposta razoável à data, apesar de ser uma questão popular com respostas altamente upvoted ....

Melhores práticas: declaração frente cabeçalhos

Como ilustrado pelo cabeçalho <iosfwd> da biblioteca padrão, a forma adequada de fornecer declarações para a frente para os outros é ter um cabeçalho de declaração para a frente . Por exemplo:

a.fwd.h:

#pragma once
class A;

A.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*);
};

Os mantenedores das bibliotecas A e B cada um deve ser responsável por manter seus cabeçalhos de declaração para a frente em sincronia com seus cabeçalhos e arquivos de implementação, então - por exemplo - se o mantenedor do "B" vem e reescreve o código para ser ...

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;

... então recompilação do código para "A" será acionado pelas mudanças no b.fwd.h incluído e deve ser concluída de forma limpa.


Pobre mas prática comum: o material declarar frente em outros libs

Say - em vez de usar um cabeçalho de declaração para a frente como explicado acima - código na a.h ou a.cc vez forward-se declara class B;:

  • se a.h ou a.cc fez incluem b.h mais tarde:
    • compilação de um terminará com um erro, uma vez que ele chegue ao conflitantes declaração / definição de B (ou seja, a mudança acima para B quebrou A e quaisquer outros clientes de abusar de declarações para a frente, em vez de trabalhar de forma transparente).
  • caso contrário (se A não eventualmente, incluir b.h - possível se um justo lojas / passa ao redor Bs pelo ponteiro e / ou referência)
    • ferramentas de compilação que dependem de análise #include e marcas de tempo de arquivos alterados não irá reconstruir A (e seu código ainda mais dependente) após a mudança para B, causando erros em tempo de link ou tempo de execução. Se B for distribuído como um tempo de execução carregado DLL, em código "A" pode falhar para encontrar os símbolos diferentemente-esmagadas em tempo de execução, as quais podem ou não podem ser manuseados bem o suficiente para provocar o desligamento ordenada ou funcionalidade aceitavelmente reduzidos.

Se o código de A tem especializações / Modelo de "traços" para o velho B, eles não terão efeito.

Uma vez eu resolvi esse tipo de problema, movendo tudo inlines após a definição de classe e colocando o #include para as outras classes, pouco antes do inlines no cabeçalho do arquivo. Dessa maneira, verifique se todas as definições + inlines são definidas antes os inlines são analisados.

Fazendo assim torna possível ainda tem um monte de elementos em linha em ambos (ou vários) arquivos de cabeçalho. Mas é necessário ter incluem guardas .

Como este

// 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__ */

... e fazer o mesmo em B.h

Eu escrevi um post sobre isso uma vez: Resolvendo dependências circulares em c ++

A técnica básica é dissociar as classes usando interfaces. Assim, no seu 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;
}

Aqui está a solução para os modelos: como lidar com dependências circulares com modelos

A pista para resolver este problema é declarar ambas as classes antes de fornecer as definições (implementações). Não é possível dividir a declaração e definição em arquivos separados, mas você pode estruturá-los como se fossem em arquivos separados.

O exemplo simples apresentado na Wikipedia funcionou para mim. (Você pode ler a descrição completa em http://en.wikipedia.org/wiki /Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )

Arquivo '' 'A.H.' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

Arquivo '' 'b.h' '':

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

Arquivo '' 'main.cpp' '':

#include "a.h"
#include "b.h"

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

Infelizmente, todas as respostas anteriores estão faltando alguns detalhes. A solução correta é um pouco pesado pouco, mas esta é a única maneira de fazê-lo corretamente. E ele pode ser expandido facilmente, alças dependências mais complexos também.

Veja como você pode fazer isso, exatamente reter todos os detalhes, e usabilidade:

  • a solução é exatamente o mesmo que originalmente destinado
  • funções inline ainda em linha
  • usuários de A e B pode incluir A.H. e B.h em qualquer ordem

Criar dois arquivos, A_def.h, B_def.h. Estes irão conter apenas de A e definição de 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

E, em seguida, A.H. e B.h conterá o seguinte:

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

Note que A_def.h e B_def.h são cabeçalhos "privadas", os usuários de A e B não deve usá-los. O cabeçalho público é A.H. e B.h.

Em alguns casos é possível definir um método ou um construtor de classe B no arquivo de cabeçalho da classe A para resolver dependências circulares envolvendo definições. Desta forma, você pode evitar ter que colocar definições em arquivos .cc, por exemplo, se você quiser implementar um cabeçalho única biblioteca.

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

Infelizmente não posso comentar a resposta do geza.

Ele não é apenas dizer "colocar declarações para a frente em um cabeçalho separado". Ele diz que você tem que cabeçalhos derramado classe definição e definições de funções embutidas em diferentes arquivos de cabeçalho para permitir "dependências defered".

Mas sua ilustração não é muito boa. Uma vez que ambas as classes (A e B) só precisa de um tipo incompleto de cada outro (campos do tipo apontador / parâmetros).

Para entender melhor imaginar que classe A tem um campo do tipo B não B *. Na aula Além A e B deseja definir uma função inline com os parâmetros de outro tipo:

Este código simples não iria funcionar:

// 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"

Ela resultaria no código a seguir:

//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 não compilar porque B :: Do precisa de um tipo completa de A que é definido mais tarde.

Para se certificar de que ele compila o código fonte deve ficar assim:

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

Este é exatamente possível com esses dois arquivos de cabeçalho para cada classe wich precisa definir funções inline. O único problema é que as classes circulares não pode simplesmente incluir o "cabeçalho público".

Para resolver este problema que eu gostaria de sugerir uma extensão pré-processador: #pragma process_pending_includes

Esta directiva deve adiar o processamento do arquivo atual e concluir todos os processos pendentes inclui.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top