Aut*_*act 334 c++ compiler-errors circular-dependency c++-faq
我经常发现自己处于一种情况,我在C++项目中面临多个编译/链接器错误,因为一些糟糕的设计决策(由其他人做出:))导致不同头文件中C++类之间的循环依赖(也可能发生)在同一个文件中).但幸运的是(?)这种情况经常不足以让我在下次再次发生问题时记住这个问题的解决方案.
因此,为了便于将来回忆,我将发布一个代表性问题和解决方案.更好的解决方案当然是受欢迎的.
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;
}
};
Run Code Online (Sandbox Code Playgroud)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;
}
};
Run Code Online (Sandbox Code Playgroud)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;
}
Run Code Online (Sandbox Code Playgroud)小智 271
思考这个问题的方法是"像编译器一样思考".
想象一下,你正在编写一个编译器.你看到这样的代码.
// 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;
}
Run Code Online (Sandbox Code Playgroud)
当你编译.cc的文件(记住,.cc的,而不是.H是编译的单元),您需要为对象分配空间A
.那么,那么多少空间呢?足够存储B
!那么大小是B
多少?足够存储A
!哎呀.
显然是一个必须打破的循环引用.
你可以让编译器,而不是保留尽可能多的空间,因为它知道前期打破它-指针和引用,例如,将永远是32或64位(取决于架构),所以如果你更换(或一个)由一个指针或参考,事情会很棒.假设我们替换为A
:
// file: A.h
class A {
// both these are fine, so are various const versions of the same.
B& _b_ref;
B* _b_ptr;
};
Run Code Online (Sandbox Code Playgroud)
现在事情变得更好了.有些.main()
仍然说:
// file: main.cc
#include "A.h" // <-- Houston, we have a problem
Run Code Online (Sandbox Code Playgroud)
#include
,对于所有范围和目的(如果您将预处理器取出)只需将文件复制到.cc中.真的,.cc看起来像:
// file: partially_pre_processed_main.cc
class A {
B& _b_ref;
B* _b_ptr;
};
#include "B.h"
int main (...) {
A a;
}
Run Code Online (Sandbox Code Playgroud)
你可以看到为什么编译器无法解决这个问题 - 它不知道是什么B
- 它以前从未见过这个符号.
那么让我们告诉编译器B
.这被称为前向声明,将在本答复中进一步讨论.
// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
A a;
}
Run Code Online (Sandbox Code Playgroud)
这有效.这不是很好.但是在这一点上你应该了解循环引用问题以及我们做了什么来"修复"它,尽管修复很糟糕.
这个修复很糟糕的原因是因为下一个人#include "A.h"
必须B
在他们可以使用之前声明并且会得到一个可怕的#include
错误.那么让我们将声明转移到Ah本身.
// file: A.h
class B;
class A {
B* _b; // or any of the other variants.
};
Run Code Online (Sandbox Code Playgroud)
在Bh,在这一点上,你可以#include "A.h"
直接.
// 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;
}
Run Code Online (Sandbox Code Playgroud)
HTH.
Aut*_*act 100
如果从头文件中删除方法定义并且让类仅包含方法声明和变量声明/定义,则可以避免编译错误.方法定义应放在.cpp文件中(就像最佳实践指南所说).
以下解决方案的缺点是(假设您已将方法放在头文件中以内联它们),编译器不再内联这些方法,并尝试使用inline关键字产生链接器错误.
//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;
}
Run Code Online (Sandbox Code Playgroud)
Ton*_*roy 21
我迟迟没回答这个问题,但是迄今为止没有一个合理的答案,尽管这是一个受到高度赞扬的答案的热门问题....
如标准库的<iosfwd>
标题所示,为其他人提供前向声明的正确方法是使用前向声明标头.例如:
a.fwd.h:
#pragma once
class A;
Run Code Online (Sandbox Code Playgroud)
啊:
#pragma once
#include "a.fwd.h"
#include "b.fwd.h"
class A
{
public:
void f(B*);
};
Run Code Online (Sandbox Code Playgroud)
b.fwd.h:
#pragma once
class B;
Run Code Online (Sandbox Code Playgroud)
BH:
#pragma once
#include "b.fwd.h"
#include "a.fwd.h"
class B
{
public:
void f(A*);
};
Run Code Online (Sandbox Code Playgroud)
A
和B
库的维护者应该负责保持他们的前向声明标题与其标题和实现文件同步,所以 - 例如 - 如果"B"的维护者出现并重写代码...
b.fwd.h:
template <typename T> class Basic_B;
typedef Basic_B<char> B;
Run Code Online (Sandbox Code Playgroud)
BH:
template <typename T>
class Basic_B
{
...class definition...
};
typedef Basic_B<char> B;
Run Code Online (Sandbox Code Playgroud)
...然后重新编译"A"的代码将由包含的更改触发,b.fwd.h
并且应该干净地完成.
说 - 而不是使用如上所述的前向声明标头 - 代码在a.h
或a.cc
代替向前声明class B;
自己:
a.h
或a.cc
之后包括b.h
:
B
(即上述对B的更改破坏了A和任何其他客户滥用前向声明,而不是透明地工作).b.h
- 如果A仅通过指针和/或引用存储/传递Bs,则可能)
#include
分析和更改的文件时间戳的构建工具将不会重建A
(及其进一步依赖的代码),从而导致链接时或运行时出错.如果B作为运行时加载的DLL分发,则"A"中的代码可能无法在运行时找到不同的错位符号,这可能会或可能不会很好地处理以触发有序关闭或可接受的减少的功能.如果A的代码具有旧的模板特化/"特征" B
,它们将不会生效.
dir*_*tly 19
要记住的事情:
class A
具有class B
作为成员的对象或反之亦然,则这将不起作用.阅读常见问题:
epa*_*tel 11
我曾经通过在类定义之后移动所有内联并将#include
其他类放在头文件中的内联之前解决了这种问题.这样就可以确保在解析内联之前设置所有定义+内联.
这样做可以在两个(或多个)头文件中仍然有一堆内联.但是有必要包括警卫.
像这样
// 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__ */
Run Code Online (Sandbox Code Playgroud)
......并在做同样的事情 B.h
我曾写过一篇关于此的文章:解决c ++中的循环依赖
基本技术是使用接口来分离类.所以在你的情况下:
//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;
}
Run Code Online (Sandbox Code Playgroud)