erros Resolve construção devido à dependência circular entre as classes
-
05-07-2019 - |
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; }
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 declass 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
oua.cc
fez incluemb.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).
- compilação de um terminará com um erro, uma vez que ele chegue ao conflitantes declaração / definição de
- 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á reconstruirA
(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.
- ferramentas de compilação que dependem de análise
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
eB
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.