解决由于类之间的循环依赖而导致的构建错误
我经常发现自己在 C++ 项目中面临多个编译/链接器错误,这是由于一些错误的设计决策(由其他人做出的:))导致不同头文件中的 C++ 类之间的循环依赖(也可能发生在同一个文件中)。 但幸运的是(?)这种情况发生的频率并不高,以至于我无法在下次再次发生时记住该问题的解决方案。
因此,为了将来方便回忆,我将发布一个有代表性的问题和解决方案。 当然欢迎更好的解决方案。
-
<前><代码>B类; A级 { int_val; B*_b; 民众: A(整数值) :_val(val) { } 无效 SetB(B *b) { _b = b; _b->打印(); // 编译器错误:C2027:使用未定义的类型“B” } 无效打印() { cout<<"类型:A val="<<_val<啊
-
<前><代码>#include“啊” B级 { 双_val; A* _a; 民众: B(双值) :_val(val) { } 无效 SetA(A *a) { _a = a; _a->打印(); } 无效打印() { cout<<"类型:B val="<<_val<Bh
-
<前><代码>#include“Bh” #includemain.cpp
; int main(int argc, char* argv[]) { 一个(10); Bb(3.14); 打印(); a.SetB(&b); b.打印(); b.SetA(&a); 返回0; }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
思考这个问题的方法是“像编译器一样思考”。
想象一下您正在编写一个编译器。 你会看到这样的代码。
当您编译.cc文件时(请记住.cc而不是.h是编译的单位),您需要为对象
A
分配空间。 那么,那么,有多少空间呢? 足够存储B
! 那么B
的大小是多少? 足够存储A
! 哎呀。显然,这是一个必须打破的循环引用。
您可以通过允许编译器保留尽可能多的空间来打破它 - 例如,指针和引用将始终是 32 或 64 位(取决于体系结构),因此如果您将(其中之一)替换为一个指针或参考,事情会很棒。 假设我们替换为
A
:现在情况好多了。 有些。
main()
仍然显示:#include
,出于所有范围和目的(如果您取出预处理器),只需将文件复制到 .cc。 所以实际上, .cc 看起来像:你可以看到为什么编译器不能处理这个 - 它不知道
B
是什么 - 它甚至从未见过之前的符号。因此,让我们告诉编译器有关
B
的信息。 这称为前向声明,并在 这个答案。这有效。 这不是很好。 但此时您应该了解循环引用问题以及我们为“修复”它所做的事情,尽管修复很糟糕。
此修复不好的原因是因为下一个
#include "Ah"
的人必须先声明B
才能使用它,并且会得到一个可怕的#包括错误。 因此,让我们将声明移至 Ah 本身。
而在Bh中,此时直接
#include "Ah"
即可。HTH。
The way to think about this is to "think like a compiler".
Imagine you are writing a compiler. And you see code like this.
When you are compiling the .cc file (remember that the .cc and not the .h is the unit of compilation), you need to allocate space for object
A
. So, well, how much space then? Enough to storeB
! What's the size ofB
then? Enough to storeA
! Oops.Clearly a circular reference that you must break.
You can break it by allowing the compiler to instead reserve as much space as it knows about upfront - pointers and references, for example, will always be 32 or 64 bits (depending on the architecture) and so if you replaced (either one) by a pointer or reference, things would be great. Let's say we replace in
A
:Now things are better. Somewhat.
main()
still says:#include
, for all extents and purposes (if you take the preprocessor out) just copies the file into the .cc. So really, the .cc looks like:You can see why the compiler can't deal with this - it has no idea what
B
is - it has never even seen the symbol before.So let's tell the compiler about
B
. This is known as a forward declaration, and is discussed further in this answer.This works. It is not great. But at this point you should have an understanding of the circular reference problem and what we did to "fix" it, albeit the fix is bad.
The reason this fix is bad is because the next person to
#include "A.h"
will have to declareB
before they can use it and will get a terrible#include
error. So let's move the declaration into A.h itself.And in B.h, at this point, you can just
#include "A.h"
directly.HTH.
如果从头文件中删除方法定义并让类仅包含方法声明和变量声明/定义,则可以避免编译错误。 方法定义应放置在 .cpp 文件中(就像最佳实践指南所述)。
以下解决方案的缺点是(假设您已将方法放入头文件中以内联它们),编译器不再内联这些方法,并且尝试使用 inline 关键字会产生链接器错误。
You can avoid compilation errors if you remove the method definitions from the header files and let the classes contain only the method declarations and variable declarations/definitions. The method definitions should be placed in a .cpp file (just like a best practice guideline says).
The down side of the following solution is (assuming that you had placed the methods in the header file to inline them) that the methods are no longer inlined by the compiler and trying to use the inline keyword produces linker errors.
我迟到了回答这个问题,但迄今为止还没有一个合理的答案,尽管这是一个热门问题,答案得到了高度赞扬......
最佳实践:前向声明标头
如标准库的
标头,为其他人提供前向声明的正确方法是拥有一个前向声明标头。 例如:a.fwd.h:
ah:
b.fwd.h:
bh:
A
和B
库的维护者应该各自负责保留其前向声明标头与其标头和实现文件同步,因此 - 例如 - 如果“B”的维护者出现并将代码重写为...b.fwd.h:
bh:
...然后重新编译代码“A”将由对所包含的
b.fwd.h
的更改触发,并且应该干净地完成。糟糕但常见的做法:在其他库中转发声明内容
说 - 不要使用上面解释的转发声明标头 - 使用
ah
或a.cc
中的代码代替转发声明 < code>class B; 本身:ah
或a.cc
稍后确实包含bh
:B
的冲突声明/定义,A 的编译将终止并出现错误(即上述对 B 的更改破坏了 A 和任何其他滥用前向声明的客户端,而不是透明地工作) )。bh
- 如果 A 只是通过指针和/或引用存储/传递 B,则可能)#include
分析和更改的文件时间戳的构建工具在更改为 B 后不会重建A
(及其进一步依赖的代码),从而导致错误链接时间或运行时间。 如果 B 作为运行时加载的 DLL 进行分发,“A”中的代码可能无法在运行时找到不同损坏的符号,这些符号可能会或可能不会处理得足够好以触发有序关闭或可接受的减少功能。如果 A 的代码具有旧
B
的模板专业化/“特征”,它们将不会生效。I'm late answering this, but there's not one reasonable answer to date, despite being a popular question with highly upvoted answers....
Best practice: forward declaration headers
As illustrated by the Standard library's
<iosfwd>
header, the proper way to provide forward declarations for others is to have a forward declaration header. For example:a.fwd.h:
a.h:
b.fwd.h:
b.h:
The maintainers of the
A
andB
libraries should each be responsible for keeping their forward declaration headers in sync with their headers and implementation files, so - for example - if the maintainer of "B" comes along and rewrites the code to be...b.fwd.h:
b.h:
...then recompilation of the code for "A" will be triggered by the changes to the included
b.fwd.h
and should complete cleanly.Poor but common practice: forward declare stuff in other libs
Say - instead of using a forward declaration header as explained above - code in
a.h
ora.cc
instead forward-declaresclass B;
itself:a.h
ora.cc
did includeb.h
later:B
(i.e. the above change to B broke A and any other clients abusing forward declarations, instead of working transparently).b.h
- possible if A just stores/passes around Bs by pointer and/or reference)#include
analysis and changed file timestamps won't rebuildA
(and its further-dependent code) after the change to B, causing errors at link time or run time. If B is distributed as a runtime loaded DLL, code in "A" may fail to find the differently-mangled symbols at runtime, which may or may not be handled well enough to trigger orderly shutdown or acceptably reduced functionality.If A's code has template specialisations / "traits" for the old
B
, they won't take effect.需要记住的事情:
A 类
具有B 类
的对象作为成员,则此方法将不起作用,反之亦然。阅读常见问题解答:
Things to remember:
class A
has an object ofclass B
as a member or vice versa.Read the FAQ:
我曾经通过将所有内联移动到类定义之后并将其他类的
#include
放在内联之前解决了此类问题头文件。 这样可以确保在解析内联之前设置所有定义+内联。这样做使得两个(或多个)头文件中仍然可以有一堆内联。 但有必要包括警卫。
像这样
......并在
Bh
中做同样的事情I once solved this kind of problem by moving all inlines after the class definition and putting the
#include
for the other classes just before the inlines in the header file. This way one make sure all definitions+inlines are set prior the inlines are parsed.Doing like this makes it possible to still have a bunch of inlines in both(or multiple) header files. But it's necessary to have include guards.
Like this
...and doing the same in
B.h
我曾经写过一篇关于此的文章: 解决 c++ 中的循环依赖
基本技术是使用接口来解耦类。 所以在你的情况下:
I've written a post about this once: Resolving circular dependencies in c++
The basic technique is to decouple the classes using interfaces. So in your case:
这是模板的解决方案: 如何处理与模板的循环依赖
解决此问题的线索是在提供定义(实现)之前声明这两个类。 无法将声明和定义拆分为单独的文件,但您可以将它们结构化,就像它们位于单独的文件中一样。
Here is the solution for templates: How to handle circular dependencies with templates
The clue to solving this problem is to declare both classes before providing the definitions (implementations). It’s not possible to split the declaration and definition into separate files, but you can structure them as if they were in separate files.
维基百科上提供的简单示例对我有用。
(您可以在 http://en.wikipedia.org/wiki 阅读完整说明/Circular_dependency#Example_of_circular_dependency_in_C.2B.2B )
文件 '''ah''':
文件 '''bh''':
文件 '''main.cpp''':
The simple example presented on Wikipedia worked for me.
(you can read the complete description at http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )
File '''a.h''':
File '''b.h''':
File '''main.cpp''':
不幸的是,之前的所有答案都缺少一些细节。 正确的解决方案有点麻烦,但这是唯一正确的方法。 而且它可以轻松扩展,还可以处理更复杂的依赖关系。
以下是您如何做到这一点,完全保留所有细节和可用性:
A
和B
的内联用户仍然可以包括Ah 和 Bh 以任意顺序创建两个文件,A_def.h、B_def.h。 这些将仅包含
A
和B
的定义:然后,Ah 和 Bh 将包含以下内容:
请注意,A_def.h 和 B_def.h 是“私有”标头,
A
和B
的用户不应使用它们。 公共标头是 Ah 和 BhUnfortunately, all the previous answers are missing some details. The correct solution is a little bit cumbersome, but this is the only way to do it properly. And it scales easily, handles more complex dependencies as well.
Here's how you can do this, exactly retaining all the details, and usability:
A
andB
can include A.h and B.h in any orderCreate two files, A_def.h, B_def.h. These will contain only
A
's andB
's definition:And then, A.h and B.h will contain this:
Note that A_def.h and B_def.h are "private" headers, users of
A
andB
should not use them. The public header is A.h and B.h.不幸的是我无法评论geza的答案。
他不仅仅是说“将声明放入单独的标头中”。 他说,您必须将类定义标头和内联函数定义溢出到不同的标头文件中,以允许“延迟依赖项”。
但他的插图并不好。 因为两个类(A 和 B)只需要彼此不完整的类型(指针字段/参数)。
为了更好地理解它,想象一下 A 类有一个类型为 B 而不是 B* 的字段。 另外,类 A 和 B 想要定义一个带有其他类型参数的内联函数:
这个简单的代码不起作用:
它将产生以下代码:
该代码无法编译,因为 B::Do 需要 A 的完整类型稍后定义。
为了确保它编译源代码应该如下所示:
对于每个需要定义内联函数的类来说,这两个头文件完全可以实现。
唯一的问题是循环类不能只包含“公共标头”。
为了解决这个问题,我想建议一个预处理器扩展:
#pragma process_pending_includes
该指令应该推迟当前文件的处理并完成所有挂起的包含。
Unfortunately I can't comment the answer from geza.
He is not just saying "put forward declarations into a separate header". He says that you have to spilt class definition headers and inline function definitions into different header files to allow "defered dependencies".
But his illustration is not really good. Because both classes (A and B) only need an incomplete type of each other (pointer fields / parameters).
To understand it better imagine that class A has a field of type B not B*. In addition class A and B want to define an inline function with parameters of the other type:
This simple code would not work:
It would result in the following code:
This code does not compile because B::Do needs a complete type of A which is defined later.
To make sure that it compiles the source code should look like this:
This is exactly possible with these two header files for each class wich needs to define inline functions.
The only issue is that the circular classes can't just include the "public header".
To solve this issue I would like to suggest a preprocessor extension:
#pragma process_pending_includes
This directive should defer the processing of the current file and complete all pending includes.
首先我们需要一些定义。
定义
声明
定义
区别在于重复定义会导致一个定义规则 (ODR) 违规。 编译器将给出类似“
error: redefinition of '...'
”的错误。请注意,“前向声明”只是一个声明。 声明可以重复,因为它们没有定义任何内容,因此不会导致 ODR。
请注意,默认参数只能给出一次,可能是在声明期间,但如果有多个声明,则仅适用于其中一个声明。 因此,有人可能会说这是一个定义,因为它可能不会重复(从某种意义上说它是:它定义了默认参数)。 但是,由于它没有定义函数或模板,所以无论如何我们都将它们称为声明。 下面将忽略默认参数。
函数定义
(成员)函数定义生成代码。 拥有多个这些(在不同的翻译单元 (TU) 中,否则在编译时就会发生 ODR 冲突)通常会导致链接器错误; 除非链接器解决了它为内联函数和模板化函数所做的冲突。 两者都可能内联,也可能不内联; 如果它们不是 100% 内联,则需要存在一个正常函数(实例化); 这可能会导致我所说的碰撞。
非内联、非模板(成员)函数只需存在于单个 TU 中,因此应在单个
.cpp
中定义。然而,内联和/或模板(成员)函数是在标头中定义的,它们可能包含在多个 TU 中,因此需要链接器进行特殊处理。 然而,它们也被认为可以生成代码。
类定义
类定义可能会也可能不会生成代码。 如果是这样,那么链接器将解决其任何冲突的函数。
当然,类内部定义的任何成员函数根据定义都是“内联”的。 如果在类声明期间定义这样的函数存在问题,只需将其移到类声明之外即可。
因此,
我们
最感兴趣的是代码生成(函数实例化),两者都不能移动到类声明之外,并且需要一些其他定义才能实现实例化。
事实证明,这通常涉及智能指针和默认析构函数。 假设
struct B
无法定义,只能声明,并且struct A
如下所示:然后是
A
的实例化,而A
的定义code>B 不可见(某些编译器可能不介意B
是否稍后在同一 TU 中定义)将导致错误,因为B
的默认构造函数以及析构函数code>A,导致生成unique_ptr
的析构函数,需要B
的定义[例如error: invalid application of 'sizeof' 为不完整类型 'B'
]。 不过,仍然有一种方法可以解决这个问题:不要使用生成的默认构造函数/析构函数。例如,
将编译并且只有
A::A()
和A::~A()
两个未定义的符号,您仍然可以在定义之外内联编译它们像以前一样的A
(前提是您在执行此操作之前定义了B
)。三个部分,三个文件?
因此,我们可以区分结构/类定义的三个部分,我们可以将它们分别放入不同的文件中。
(转发)声明:
A.fwd.h
类定义:
啊
内联和模板成员函数定义:
A.inl.h
当然还有包含非内联和非模板成员函数定义的
A.cpp
; 但这些与循环标头依赖性无关。忽略默认参数,声明不需要任何其他声明或定义。
类定义可能需要声明某些其他类,还需要定义其他类。
内联/模板成员函数可能需要额外的定义。
因此,我们可以创建以下示例来显示所有可能性:
其中
B::B()
,B::~B()
,CA::f( )
和CD::c()
在一些.cpp
中定义。但是,我们也可以内联它们; 此时我们需要定义
C
因为所有四个都需要它(B::B
和B::~B
因为>unique_ptr
,见上文)。 在这个 TU 中这样做突然使得没有必要将B::B()
和B::~B()
放在B 的定义之外
(至少对于我正在使用的编译器)。 尽管如此,让B
保持原样。然后我们得到:
换句话说,
A
的定义如下所示:请注意,理论上我们可以创建多个
.inl.h
标头:每个函数一个,如果否则,它会拖拽超过所需的时间,从而导致问题。禁止的模式
请注意,所有
#include
都位于所有文件的顶部。(理论上)
.fwd.h
标头不包含其他标头。 因此它们可以随意包含并且永远不会导致循环依赖。.h
定义标头可能包含.inl.h
标头,但如果这导致循环标头依赖项,则始终可以通过移动使用内联的函数来避免这种情况函数从.inl.h
到当前类的.inl.h
; 对于智能指针,可能还需要将析构函数和/或构造函数移动到该.inl.h
。因此,唯一剩下的问题是
.h
定义标头的循环包含,即Ah
包含Bh
和Bh
包括啊
。 在这种情况下,您必须通过用指针替换类成员来解耦循环。最后,不可能有纯
.inl.h
文件的循环。 如果有必要,您可能应该将它们移动到单个文件中,在这种情况下,编译器可能无法解决问题; 但显然,当所有函数相互使用时,您无法内联所有函数,因此您不妨手动决定哪些函数可以是非内联的。First we need a few definitions.
Definitions
Declaration
Definition
The difference is that repeating a definition causes a One Definition Rule (ODR) violation. The compiler will give an error along the lines of "
error: redefinition of '...'
".Note that a "forward declaration" is just a declaration. Declarations can be repeated since they don't define anything and therefore cause no ODR.
Note that default arguments may only be given once, possibly during the declaration, but only for one of the declarations if there are multiple. Therefore one could argue that that is a definition because it may not be repeated (and in a sense it is: it defines the default arguments). However, since it doesn't define the function or template, lets call those a declaration anyway. Default arguments will be ignored below.
Function definitions
(Member) function definitions generate code. Having multiple of those (in different Translation Units (TU's), otherwise you'd get an ODR violation already during compile time) normally leads to a linker error; except when the linker resolves the collision which it does for inline functions and templated functions. Both might or might not be inlined; if they are not 100% of the time inlined then a normal function (instantiation) needs to exist; that might cause the collision that I am talking about.
Non-inline, non-template (member) functions need to exist only in a single TU and should therefore be defined in a single
.cpp
.However, inline- and/or template (member) functions are defined in headers, which might be included by multiple TU's, and therefore need special treatment by the linker. They too are considered to generate code however.
Class definitions
Class definitions might or might not generate code. If they do, then that is for functions that the linker will resolve any collisions of.
Of course, any member function that is defined inside the class is per definition "inline". If it is a problem that such a function is defined during the declaration of the class, it can simply be moved outside the class declaration.
Instead of,
do
Therefore we are mostly interested in code generation (function instantiations) that both, can not be moved outside the class declaration and requires some other definition in order to be instantiated.
It turns out that this usually involves smart pointers and default destructors. Assume that
struct B
can not be defined, only declared, andstruct A
looks as follows:then an instantiation of
A
while the definition ofB
is not visible (some compilers might not mind ifB
is defined later in the same TU) will cause an error because both, the default constructor as well as the destructor ofA
, cause the destructor ofunique_ptr<B>
to be generated, which needs the definition ofB
[e.g.error: invalid application of ‘sizeof’ to incomplete type ‘B’
]. There is still a way around this though: do not use generated default constructor/destructor.For example,
will compile and just have two undefined symbols for
A::A()
andA::~A()
which you can still compile inline outside of the definition ofA
as before (provided you defineB
before you do so).Three parts, three files?
As such we can distinguish three part of a struct/class definition that we could each put in a different file.
The (forward) declaration:
A.fwd.h
The class definition:
A.h
The inline and template member function definitions:
A.inl.h
And then there is of course
A.cpp
with the non-inline and non-template member function definitions; but those are not relevant for circular header dependencies.Ignoring default arguments, declarations won't require any other declaration or definition.
Class definitions might require certain other classes to be declared, yet others to be defined.
Inline/template member functions might require additional definitions.
We can therefore create the following example that show all possibilities:
where
B::B()
,B::~B()
,C A::f()
andC D::c()
are defined in some.cpp
.But, lets inline those as well; at that point we need to define
C
because all four need that (B::B
andB::~B
because of theunique_ptr
, see above). And doing so in this TU then suddenly makes it unnecessary to putB::B()
andB::~B()
outside of the definition ofB
(at least with the compiler that I am using). Nevertheless, lets keepB
as it is.Then we get:
In other words, the definition of
A
looks like this:Note that in theory we could make multiple
.inl.h
headers: one for each function, if otherwise it drags in more than required and that causes a problem.Forbidden patterns
Note that all
#include
's are at the top of all files.(In theory)
.fwd.h
headers do not include other headers. Therefore they can be included at will and never lead to a circular dependency..h
definition headers might include a.inl.h
header, but if that leads to a circular header dependency then that can always be avoided by moving the function that uses the inlined function from that.inl.h
to the.inl.h
of the current class; in the case of smart pointers that might require to also move the destructor and/or constructor to that.inl.h
.Hence, the only remaining problem is a circular inclusion of
.h
definition headers, ieA.h
includesB.h
andB.h
includesA.h
. In that case you must decouple the loop by replacing a class member with a pointer.Finally, it is not possible to have a loop of pure
.inl.h
files. If that is necessary you probably should move them to a single file in which case the compiler might or might not be able to solve the problem; but clearly you can't get ALL functions inlined when they use eachother, so you might as well manually decide which can be non-inlined.在某些情况下,可以在类 A 的头文件中定义类 B 的方法或构造函数来解决涉及定义的循环依赖。
通过这种方式,您可以避免将定义放入
.cc
文件中,例如,如果您想实现仅包含头文件的库。In some cases it is possible to define a method or a constructor of class B in the header file of class A to resolve circular dependencies involving definitions.
In this way you can avoid having to put definitions in
.cc
files, for example if you want to implement a header only library.