Resolver erros de construção devido à dependência circular entre classs

Muitas vezes me encontro em uma situação onde estou enfrentando vários erros de compilation / vinculador em um projeto C ++ devido a algumas decisões de design ruins (feitas por outra pessoa :)) que levam a dependencies circulares entre classs C ++ em diferentes arquivos de header (pode acontecer também no mesmo arquivo) . Mas felizmente (?) Isso não acontece com frequência suficiente para eu lembrar a solução desse problema para a próxima vez que isso acontecer novamente.

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


  • Ah

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


  • main.cpp

     #include "Bh" #include  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; } 

A maneira de pensar sobre isso é “pensar como um compilador”.

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

 // file: Ah class A { B _b; }; // file: Bh class B { A _a; }; // file main.cc #include "Ah" #include "Bh" int main(...) { A a; } 

Quando você está compilando o arquivo .cc (lembre-se que o .cc e não o .h é a unidade de compilation), você precisa alocar espaço para o object A Então, quanto espaço então? O suficiente para armazenar B ! Qual é o tamanho de B então? O suficiente para armazenar A ! Oops

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 ele conhece – pointers e referências, por exemplo, sempre serão 32 ou 64 bits (dependendo da arquitetura) e, portanto, se você replace (qualquer um) por um ponteiro ou referência, as coisas seriam ótimas. Vamos supor que substituímos em A :

 // file: Ah 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 "Ah" // <-- Houston, we have a problem 

#include , para todas as extensões e propósitos (se você retirar o pré-processador) 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 "Bh" int main (...) { A a; } 

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

Então, vamos dizer ao compilador sobre B Isso é conhecido como uma declaração antecipada e é discutido mais adiante nesta resposta .

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

Isso funciona Não é ótimo . Mas neste momento você deve ter uma compreensão do problema de referência circular e o que fizemos para "consertá-lo", embora a correção seja ruim.

O motivo pelo qual essa correção é ruim é porque a próxima pessoa a #include "Ah" terá que declarar B antes de poder usá-la e receberá um erro #include terrível. Então, vamos passar a declaração para o próprio Ah .

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

E em Bh , neste ponto, você pode simplesmente #include "Ah" diretamente.

 // file: Bh #include "Ah" class B { // note that this is cool because the compiler knows by this time // how much space A will need. A _a; } 

HTH.

Você pode evitar erros de compilation se remover as definições de método dos arquivos de header e permitir que as classs contenham apenas as declarações de methods e declarações / definições de variables. As definições do método devem ser colocadas em um arquivo .cpp (como diz uma diretriz de prática recomendada).

O lado negativo da solução a seguir é (supondo que você tenha colocado os methods no arquivo de header para inline-los) que os methods não são mais embutidos pelo compilador e tentar usar a palavra-chave in-line produz erros de vinculador.

 //Ah #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 //Bh #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 "Ah" #include "Bh" #include  using namespace std; A::A(int val) :_val(val) { } void A::SetB(B *b) { _b = b; cout<<"Inside SetB()"<Print(); } void A::Print() { cout<<"Type:A val="<<_val< using namespace std; B::B(double val) :_val(val) { } void B::SetA(A *a) { _a = a; cout<<"Inside SetA()"<Print(); } void B::Print() { cout<<"Type:B val="<<_val< 

Coisas para lembrar:

  • Isso não funcionará se a class A tiver um object da class B como membro ou vice-versa.
  • A declaração a seguir é um caminho a percorrer.
  • A ordem de declaração é importante (e é por isso que você está mudando as definições).
    • Se ambas as classs chamarem funções da outra, você terá que remover as definições.

Leia o FAQ:

  • Como posso criar duas classs que se conhecem?
  • Quais considerações especiais são necessárias quando declarações de encaminhamento são usadas com objects membros?
  • Quais considerações especiais são necessárias quando as declarações de encaminhamento são usadas com funções embutidas?

Uma vez resolvi esse tipo de problema movendo todas as linhas após a definição da class e colocando o #include para as outras classs antes das inlines no arquivo de header. Dessa forma, certifique-se de que todas as definições + inlines estejam definidas antes que as inlines sejam analisadas.

Fazendo assim, torna-se possível ter um monte de inline em ambos (ou múltiplos) arquivos de header. Mas é necessário include guardas .

Como isso

 // File: Ah #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 "Bh" 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< 

... e fazendo o mesmo em Bh

Estou atrasado respondendo isso, mas não há uma resposta razoável até hoje, apesar de ser uma pergunta popular com respostas altamente votadas …

Melhor prática: headers de declaração antecipada

Conforme ilustrado pelo header da biblioteca padrão, a maneira correta de fornecer declarações de encaminhamento para outras pessoas é ter um header 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 A e B devem ser responsáveis ​​por manter seus headers de declaração direta em sincronia com seus headers e arquivos de implementação, assim – por exemplo – se o mantenedor de “B” aparecer e rewrite o código para ser …

b.fwd.h:

 template  class Basic_B; typedef Basic_B B; 

bh:

 template  class Basic_B { ...class definition... }; typedef Basic_B B; 

… então a recompilation do código para “A” será acionada pelas mudanças no b.fwd.h incluído e deverá ser concluída de forma limpa.


Prática ruim, mas comum: encaminhar declarar coisas em outras libs

Diga – em vez de usar um header de declaração de encaminhamento conforme explicado acima – codifique em ah ou a.cc vez de declarar a class B; em si:

  • se ah ou a.cc incluíram bh depois:
    • A compilation de A terminará com um erro quando chegar à declaração / definição conflitante de B (isto é, a mudança acima para B quebrou A e quaisquer outros clientes que abusarem de declarações futuras, em vez de trabalharem de forma transparente).
  • caso contrário (se A não include eventualmente bh – possível se A apenas armazena / passa em torno de Bs por ponteiro e / ou referência)
    • as ferramentas de criação que dependem da análise #include e dos registros de data e hora de arquivo alterados não reconstruirão A (e seu código dependente adicional) após a alteração para B, causando erros no tempo de 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 diferentemente mutilados em tempo de execução, que podem ou não ser manipulados bem o suficiente para acionar o encerramento ordenado ou a funcionalidade aceitavelmente reduzida.

Se o código de A tiver especializações de template / “traits” para o antigo B , eles não terão efeito.

Eu escrevi um post sobre isso uma vez: Resolvendo dependencies circulares em c ++

A técnica básica é dissociar as classs usando interfaces. Então, no seu caso:

 //Printer.h class Printer { public: virtual Print() = 0; } //Ah #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<Print(); } void Print() { cout<<"Type:B val="<<_val< #include "Ah" #include "Bh" 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 modelos: Como lidar com dependencies circulares com modelos

A pista para resolver este problema é declarar ambas as classs 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.

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 "ah" #include "bh" int main() { A a; B b; ab = &b; ba = &a; } 

Infelizmente, todas as respostas anteriores estão faltando alguns detalhes. A solução correta é um pouco incômoda, mas essa é a única maneira de fazê-lo corretamente. E é facilmente dimensionável, além de lidar com dependencies mais complexas.

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

  • a solução é exatamente igual à pretendida originalmente
  • funções embutidas ainda em linha
  • os usuários de A e B podem include Ah e Bh em qualquer ordem

Crie dois arquivos, A_def.h, B_def.h. Estes irão conter apenas as definições de A e 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 então, Ah e Bh conterão isto:

 // Ah #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<Print(); } inline void B::Print() { cout<<"Type:B val="<<_val< 

Observe que A_def.he B_def.h são headers "privados", os usuários de A e B não devem usá-los. O header público é Ah e Bh