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; }
c++
compiler-errors
circular-dependency
c++-faq
Autodidata
fonte
fonte
Respostas:
A maneira de pensar sobre isso é "pensar como um compilador".
Imagine que você está escrevendo um compilador. E você vê código assim.
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 guardarB
! Qual é o tamanhoB
então? O suficiente para guardarA
! OpaClaramente 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
:Agora as coisas estão melhores. Um pouco.
main()
ainda diz:#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: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 .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 declararB
antes de poder usá-la e receberá um#include
erro terrível . Então, vamos passar a declaração para o próprio Ah .E em Bh , neste ponto, você pode apenas
#include "A.h"
diretamente.HTH.
fonte
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.
fonte
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:
ah:
b.fwd.h:
bh:
Os mantenedores das bibliotecas
A
e eB
devem 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:
bh:
... a recompilação do código para "A" será acionada pelas alterações no incluído
b.fwd.h
e 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.h
ou ema.cc
vez disso, declare-class B;
se:a.h
oua.cc
incluiub.h
mais tarde: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).b.h
- possível se A apenas armazena / passa Bs por ponteiro e / ou referência)#include
análise e os carimbos de data e hora alterados do arquivo não serão reconstruídosA
(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.fonte
a.fwd.h
ema.h
, para garantir que eles permanecer em sincronia. O código de exemplo está ausente onde essas classes são usadas.a.h
eb.h
ambos 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. Ondeb.h
incluia.h
emain.cpp
incluib.h
main.cpp
, mas é bom que você tenha documentado o que deve conter em seu comentário. Cheers<iosfwd>
é um exemplo clássico: pode haver alguns objetos de fluxo referenciados de vários lugares e<iostream>
é muito para incluir.#include
s).Coisas para lembrar:
class A
tiver um objeto declass B
como membro ou vice-versa.Leia o FAQ:
fonte
Certa vez, resolvi esse tipo de problema movendo todas as linhas após a definição da classe e colocando as
#include
para 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
... e fazendo o mesmo em
B.h
fonte
B.h
primeiro?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:
fonte
virtual
afeta o desempenho em tempo de execução.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.
fonte
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' '':
Arquivo '' 'b.h' '':
Arquivo '' 'main.cpp' '':
fonte
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
eB
podem incluir Ah e Bh em qualquer ordemCrie dois arquivos, A_def.h, B_def.h. Eles conterão apenas
A
asB
definições de s e :E então, Ah e Bh conterão isso:
Observe que A_def.h e B_def.h são cabeçalhos "particulares", usuários
A
eB
não devem usá-los. O cabeçalho público é Ah e Bhfonte
A
aB
definição de ' e ' esteja disponível, a declaração de encaminhamento não é suficiente).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
.cc
arquivos, por exemplo, se desejar implementar uma biblioteca apenas de cabeçalho.fonte
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:
Isso resultaria no seguinte código:
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:
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.
fonte