首先,我们需要一些定义。
定义
宣言
extern int n;
int f();
template<typename T> int g(T);
struct A;
template<typename T> struct B;
定义
int n;
int f() { return 42; }
template<typename T> int g(T) { return 42; }
struct A { int f(); };
template<typename T> struct B { int g(T*); };
不同之处在于重复定义会导致违反One definition Rule (ODR)。编译器会给出一个类似"error: redefinition of '…'"的错误。
注意,“向前声明”只是一种声明。声明可以重复,因为它们没有定义任何东西,因此不会导致ODR。
注意,默认参数只能给出一次,可能是在声明期间,但如果有多个声明,则只能给出其中一个。因此,有人可能会说这是一个定义,因为它可能不会被重复(在某种意义上它是:它定义了默认参数)。但是,由于它没有定义函数或模板,我们无论如何都将其称为声明。下面将忽略默认参数。
函数定义
(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.
非内联、非模板(成员)函数只需要存在于单个TU中,因此应该在单个.cpp中定义。
然而,内联和/或模板(成员)函数定义在头文件中,可能被多个TU包含,因此需要链接器进行特殊处理。然而,它们也被认为是生成代码的。
类定义
类定义可能生成代码,也可能不生成代码。如果有,那是针对链接器将解决冲突的函数。
当然,在类内部定义的任何成员函数都是“内联”定义。如果在类声明期间定义了这样一个函数,那么可以简单地将它移到类声明之外。
相反的,
struct A {
int f() const { return 42; }
};
do
struct A {
inline int f() const;
}; // struct declaration ends here.
int A::f() const { return 42; }
因此,我们最感兴趣的是代码生成(函数实例化),它们不能被移到类声明之外,并且需要一些其他定义才能被实例化。
事实证明,这通常涉及智能指针和默认析构函数。假设结构B不能定义,只能声明,结构A如下所示:
struct B;
struct A { std::unique_ptr<B> ptr; };
那么A的实例化而B的定义不可见(一些编译器可能不介意稍后在同一TU中定义B)将导致错误,因为A的默认构造函数和析构函数都会生成unique_ptr<B>的析构函数,这需要B的定义[例如:' sizeof '对不完整类型' B '的无效应用]。不过,还是有办法解决这个问题:不要使用生成的默认构造函数/析构函数。
例如,
struct B;
struct A {
A();
~A();
std::unique_ptr<B> ptr;
};
将编译并只有两个未定义的符号A::A()和A::~A(),您仍然可以像以前一样在A的定义之外内联编译(前提是您在这样做之前定义了B)。
三个部分,三个文件?
因此,我们可以区分结构/类定义的三个部分,分别放在不同的文件中。
(向前)声明:
A.fwd.h
类定义:
A.h
内联和模板成员函数定义:
A.inl.h
当然还有带有非内联和非模板成员函数定义的A.cpp;但这些与循环头依赖关系无关。
忽略默认参数,声明将不需要任何其他声明或定义。
类定义可能需要声明某些其他类,也可能需要定义其他类。
内联/模板成员函数可能需要其他定义。
因此,我们可以创建以下示例来展示所有可能性:
struct C;
struct B
{
B();
~B();
std::unique_ptr<C> ptr; // Need declaration of C.
};
struct A
{
B b; // Needs definition of B.
C f(); // Needs declaration of C.
};
inline A g() // Needs definition of A.
{
return {};
}
struct D
{
A a = g(); // Needs definition of A.
C c(); // Needs declaration of C.
};
B: B (), B:: ~ B (), C:: f()和C D:: C()定义一些. cpp。
但是,我们把它们也内联起来;在这一点上,我们需要定义C,因为这四个都需要(B::B和B::~B,因为unique_ptr,见上文)。在这个TU中这样做,突然就没有必要把B::B()和B::~B()放在B的定义之外(至少在我使用的编译器中)。尽管如此,我们保持B不变。
然后我们得到:
// C.fwd.h:
struct C;
// B.h:
struct B
{
inline B();
inline ~B();
std::unique_ptr<C> ptr;
};
// A.h:
struct A
{
B b;
inline C f();
};
// D.h:
inline A g()
{
return {};
}
struct D
{
A a = g();
inline C c();
};
// C.h:
struct C {};
// B.inl.h:
B::B() {}
B::~B() {}
// A.inl.h:
C A::f()
{
D d;
return d.c();
}
// D.inl.h:
C D::c()
{
return {};
}
换句话说,A的定义是这样的
// A.fwd.h:
struct A;
// A.h:
#include "B.h" // Already includes C.fwd.h, but well...
#include "C.fwd.h" // We need C to be declared too.
struct A
{
B b;
inline C f();
};
// A.inl.h:
#include "A.h"
#include "C.h"
#include "D.inl.h"
C A::f()
{
D d;
return d.c();
}
请注意,理论上我们可以创建多个.inl.h头文件:每个函数一个头文件,如果不这样做,就会导致问题。
禁止模式
注意,所有#include都位于所有文件的顶部。
(理论上).fwd.h头文件不包括其他头文件。因此,可以随意包含它们,而不会导致循环依赖。
.h定义头文件可能包含.inl.h头文件,但如果这导致循环头文件依赖,那么总是可以通过将使用内联函数的函数从.inl.h移到当前类的.inl.h来避免这种情况;在智能指针的情况下,可能还需要将析构函数和/或构造函数移动到.inl.h。
因此,唯一剩下的问题是.h定义头的循环包含,即A.h包含B.h, B.h包含A.h。在这种情况下,必须通过用指针替换类成员来解耦循环。
最后,纯.inl.h文件的循环是不可能的。如果有必要,你可能应该把它们移动到一个文件中,在这种情况下,编译器可能无法解决问题;但显然,当它们相互使用时,你不能让所有函数都内联,所以你不妨手动决定哪些可以非内联。