Resolver erros de compilação devido à dependência circular entre classes

353

Costumo me encontrar em uma situação em que estou enfrentando vários erros de compilação / vinculador em um projeto C ++ devido a algumas decisões ruins de design (tomadas por outra pessoa :)) que levam a dependências circulares entre classes C ++ em diferentes arquivos de cabeçalho (também pode acontecer no mesmo arquivo) . Mas, felizmente (?), Isso não acontece com frequência suficiente para que eu lembre da solução para esse problema na próxima vez que acontecer novamente.

Portanto, para facilitar o recall no futuro, postarei um problema representativo e uma solução junto com ele. Obviamente, melhores soluções são bem-vindas.


  • 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;
    }
    
Autodidata
fonte
23
Ao trabalhar com o Visual Studio, o sinalizador / showIncludes ajuda muito a depurar esse tipo de problema.
wip

Respostas:

288

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

Imagine que você está escrevendo um compilador. E você vê código assim.

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

Ao compilar o arquivo .cc (lembre-se de que o .cc e não o .h é a unidade de compilação), você precisa alocar espaço para o objeto A. Então, bem, quanto espaço então? O suficiente para guardar B! Qual é o tamanho Bentão? O suficiente para guardar A! Opa

Claramente uma referência circular que você deve quebrar.

Você pode quebrá-lo, permitindo que o compilador reserve o máximo de espaço que sabe sobre o início - ponteiros e referências, por exemplo, sempre terão 32 ou 64 bits (dependendo da arquitetura) e, portanto, se você substituir (um) por um ponteiro ou referência, as coisas seriam ótimas. Digamos que substituímos 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ê retirar o pré-processador), basta copiar o arquivo no arquivo .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 por que o compilador não pode lidar com isso - ele não tem idéia do que Bé - ele nunca viu o símbolo antes.

Então, vamos falar sobre o compilador B. Isso é conhecido como declaração direta e é discutido mais adiante nesta resposta .

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

Isso funciona . Isso não é ótimo . Mas, nesse ponto, você deve ter uma compreensão do problema de referência circular e o que fizemos para "corrigi-lo", embora a correção seja ruim.

O motivo dessa correção é ruim porque a próxima pessoa #include "A.h"terá que declarar Bantes de poder usá-la e receberá um #includeerro terrível . Então, vamos passar a declaração para o próprio Ah .

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

E em Bh , 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.

Roosh
fonte
20
"Contar ao compilador sobre B" é conhecido como uma declaração de B.
Peter Ajtai
8
AMD! perdeu totalmente o fato de que as referências são conhecidas em termos de espaço ocupado. Finalmente, agora eu posso projetar corretamente!
Kellogs #
47
Mas ainda assim Você não pode usar qualquer outra função a B (como na questão _b-> Printt ())
rank1
3
Este é o problema que estou tendo. Como você traz as funções com declaração direta sem reescrever completamente o arquivo de cabeçalho?
sydan
101

Você pode evitar erros de compilação se remover as definições de método dos arquivos de cabeçalho e deixar que as classes contenham apenas as declarações de método e as declarações / definições de variáveis. As definições de método devem ser colocadas em um arquivo .cpp (como a diretriz de melhores práticas diz).

O lado negativo da solução a seguir é (supondo que você tenha colocado os métodos no arquivo de cabeçalho para incorporá-los) que os métodos não são mais incorporados pelo compilador e a tentativa de usar a palavra-chave inline produz erros 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;
}
Autodidata
fonte
Obrigado. Isso resolveu o problema facilmente. Simplesmente mudei as inclusões circulares para os arquivos .cpp.
Lenar Hoyt
3
E se você tiver um método de modelo? Então você não pode realmente movê-lo para um arquivo CPP, a menos que instancie os modelos manualmente.
Malcolm
Você sempre inclui "Ah" e "Bh" juntos. Por que você não inclui "Ah" em "Bh" e depois inclui apenas "Bh" em "A.cpp" e "B.cpp"?
Gusev Slava
28

Estou atrasado em responder isso, mas não há uma resposta razoável até o momento, apesar de ser uma pergunta popular com respostas altamente votadas ....

Prática recomendada: cabeçalhos de declaração direta

Conforme ilustrado pelo <iosfwd>cabeçalho da biblioteca Standard , a maneira correta de fornecer declarações de encaminhamento para terceiros é ter um cabeçalho de declaração de encaminhamento . Por exemplo:

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

Os mantenedores das bibliotecas Ae e Bdevem ser responsáveis ​​por manter seus cabeçalhos de declaração direta sincronizados com seus cabeçalhos e arquivos de implementação; por exemplo, se o mantenedor de "B" aparecer e reescrever o código para ser ...

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;

... a recompilação do código para "A" será acionada pelas alterações no incluído b.fwd.he deverá ser concluída de forma limpa.


Prática pobre, mas comum: para a frente declarar coisas em outras bibliotecas

Diga - em vez de usar um cabeçalho de declaração de encaminhamento, conforme explicado acima - no código a.hou em a.ccvez disso, declare- class B;se:

  • se a.hou a.ccincluiu b.hmais tarde:
    • a compilação de A terminará com um erro assim que chegar à declaração / definição conflitante de B(ou seja, a alteração acima em B quebrou A e quaisquer outros clientes que abusam de declarações avançadas, em vez de trabalhar de forma transparente).
  • caso contrário (se A não tiver incluído eventualmente b.h- possível se A apenas armazena / passa Bs por ponteiro e / ou referência)
    • as ferramentas de construção baseadas na #includeanálise e os carimbos de data e hora alterados do arquivo não serão reconstruídos A(e seu código dependente adicional) após a alteração para B, causando erros no tempo do link ou no tempo de execução. Se B for distribuído como uma DLL carregada em tempo de execução, o código em "A" poderá falhar ao encontrar os símbolos diferentes no tempo de execução, que podem ou não ser manipulados suficientemente bem para acionar o desligamento ordenado ou a funcionalidade reduzida aceitável.

Se o código de A tiver especializações / "características" de modelo para o antigo B, elas não terão efeito.

Tony Delroy
fonte
2
Essa é uma maneira realmente limpa de lidar com as declarações de encaminhamento. A única "desvantagem" estaria nos arquivos extras. Eu suponho que você sempre incluir a.fwd.hem a.h, para garantir que eles permanecer em sincronia. O código de exemplo está ausente onde essas classes são usadas. a.he b.hambos precisarão ser incluídos, pois não funcionarão isoladamente: `` `//main.cpp #include" ah "#include" bh "int main () {...}` `` Ou um deles precisa ser totalmente incluído no outro, como na questão de abertura. Onde b.hinclui a.he main.cppincluib.h
Farway
2
@ Farway Direito em todos os aspectos. Não me incomodei em mostrar main.cpp, mas é bom que você tenha documentado o que deve conter em seu comentário. Cheers
Tony Delroy
11
Uma das melhores respostas com uma boa explicação detalhada de por que o faz e não fazer devido aos prós e contras ...
Francis Cugler
11
@RezaHajianpour: faz sentido ter um cabeçalho de declaração direta para todas as classes das quais você deseja declarações avançadas, circulares ou não. Dito isso, você só as desejará quando: 1) incluir a declaração real for (ou pode-se esperar que se torne mais tarde) dispendiosa (por exemplo, inclui muitos cabeçalhos que sua unidade de tradução não precisaria) e 2) o código do cliente é provavelmente será capaz de fazer uso de ponteiros ou referências aos objetos. <iosfwd>é um exemplo clássico: pode haver alguns objetos de fluxo referenciados de vários lugares e <iostream>é muito para incluir.
Tony Delroy
11
@RezaHajianpour: Acho que você tem a idéia certa, mas há uma questão terminológica com sua afirmação: "só precisamos do tipo a ser declarado " estaria correto. O tipo que está sendo declarado significa que a declaração de encaminhamento foi vista; é definido assim que a definição completa é analisada (e para isso você pode precisar de mais #includes).
Tony Delroy
20

Coisas para lembrar:

  • Isso não funcionará se class Ativer um objeto declass B como membro ou vice-versa.
  • A declaração de encaminhamento é o caminho a percorrer.
  • A ordem da declaração é importante (e é por isso que você está mudando as definições).
    • Se ambas as classes chamam funções da outra, é necessário mover as definições.

Leia o FAQ:

dirkgently
fonte
11
os links que você forneceu não funcionam mais, você conhece os novos para se referir?
Ramya Rao 22/02
11

Certa vez, resolvi esse tipo de problema movendo todas as linhas após a definição da classe e colocando as #includepara as outras classes imediatamente antes das linhas. no arquivo de cabeçalho. Dessa forma, verifique se todas as definições + linhas estão definidas antes que as linhas sejam analisadas.

Fazendo assim, é possível ainda ter várias linhas inline nos dois (ou vários) arquivos de cabeçalho. Mas é necessário ter guardas .

Como isso

// 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 fazendo o mesmo em B.h

epatel
fonte
Por quê? Eu acho que é uma solução elegante para um problema complicado ... quando se quer inline. Se alguém não quer inlines não se deve ter escrito o código como ele foi escrito desde o início ...
epatel
O que acontece se um usuário incluir B.hprimeiro?
Sr. Fooz
3
Observe que o seu protetor de cabeçalho está usando um identificador reservado, qualquer coisa com sublinhados duplos adjacentes é reservada.
Lars Viklund
6

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

A técnica básica é desacoplar as classes usando interfaces. Então, 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;
}
Eduard Wirch
fonte
2
Observe que o uso de interfaces e virtualafeta o desempenho em tempo de execução.
cemper93
4

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

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

Tatyana
fonte
2

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;
}
madx
fonte
1

Infelizmente, todas as respostas anteriores estão faltando alguns detalhes. A solução correta é um pouco complicada, mas esta é a única maneira de fazê-lo corretamente. E dimensiona facilmente, também lida com dependências mais complexas.

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

  • a solução é exatamente igual à originalmente pretendida
  • funções embutidas ainda embutidas
  • usuários Ae Bpodem incluir Ah e Bh em qualquer ordem

Crie dois arquivos, A_def.h, B_def.h. Eles conterão apenas Aas Bdefinições de s e :

// 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 então, Ah e Bh conterão isso:

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

Observe que A_def.h e B_def.h são cabeçalhos "particulares", usuários Ae Bnão devem usá-los. O cabeçalho público é Ah e Bh

geza
fonte
11
Isso tem alguma vantagem sobre a solução de Tony Delroy ? Ambos são baseados em cabeçalhos "auxiliares", mas os de Tony são menores (apenas contêm a declaração de encaminhamento) e parecem estar funcionando da mesma maneira (pelo menos à primeira vista).
Fabio diz Restabelecer Monica
11
Essa resposta não resolve o problema original. Apenas diz "apresentar declarações em um cabeçalho separado". Nada sobre a resolução de dependência circular (a questão precisa de uma solução em que Aa Bdefinição de ' e ' esteja disponível, a declaração de encaminhamento não é suficiente).
geza
0

Em alguns casos, é possível definir um método ou construtor da classe B no arquivo de cabeçalho da classe A para resolver dependências circulares envolvendo definições. Dessa forma, você pode evitar precisar colocar definições nos .ccarquivos, por exemplo, se desejar implementar uma biblioteca apenas de cabeçalho.

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

Infelizmente não posso comentar a resposta de geza.

Ele não está apenas dizendo "apresentar declarações em um cabeçalho separado". Ele diz que você deve aplicar cabeçalhos de definição de classe e definições de função embutida em diferentes arquivos de cabeçalho para permitir "dependências adiadas".

Mas sua ilustração não é realmente boa. Como as duas classes (A e B) precisam apenas de um tipo incompleto (campos / parâmetros do ponteiro).

Para entender melhor, imagine que a classe A tem um campo do tipo B e não B *. Além disso, as classes A e B desejam definir uma função embutida com parâmetros do outro tipo:

Este código simples não funcionaria:

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

Isso resultaria no seguinte 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"

Esse código não é compilado porque o B :: Do precisa de um tipo completo de A, definido posteriormente.

Para certificar-se de que ele compila o código fonte, fique 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
}

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

Para resolver esse problema, gostaria de sugerir uma extensão de pré-processador: #pragma process_pending_includes

Esta diretiva deve adiar o processamento do arquivo atual e concluir todas as inclusões pendentes.

Bernd Baumanns
fonte