正如其他人所指出的那样,编译器符合 C++ 标准,因为“一个定义”规则规定您只能有一个函数的定义,除非该函数是内联的,那么定义必须相同。
实际上,发生的情况是函数被标记为内联,并且在链接阶段,如果它遇到内联标记标记的多个定义,则链接器会静默丢弃除一个之外的所有定义。如果它遇到多个未标记内联标记的令牌定义,则会生成错误。
之所以调用此属性,是inline
因为在 LTO(链接时间优化)之前,获取函数体并在调用站点“内联”它要求编译器具有函数体。 inline
函数可以放在头文件中,每个 cpp 文件都可以看到正文并将代码“内联”到调用站点。
这并不意味着代码实际上会被内联。相反,它使编译器更容易内联它。
但是,我不知道有一个编译器会在丢弃重复项之前检查定义是否相同。这包括以其他方式检查函数体定义是否相同的编译器,例如 MSVC 的 COMDAT 折叠。这让我很难过,因为这是一组非常微妙的错误。
解决问题的正确方法是将函数放在匿名命名空间中。通常,您应该考虑将所有内容放在匿名命名空间中的源文件中。
另一个非常讨厌的例子:
// A.cpp
struct Helper {
std::vector<int> foo;
Helper() {
foo.reserve(100);
}
};
// B.cpp
struct Helper {
double x, y;
Helper():x(0),y(0) {}
};
在类的主体中定义的方法是隐式内联的。ODR 规则适用。这里我们有两个不同Helper::Helper()
的,都是内联的,而且它们不同。
两个班级的规模不同。sizeof(double)
在一种情况下,我们用初始化两个0
(因为在大多数情况下,零浮点数是零字节)。
在另一种情况下,我们首先用零初始化三个 ,然后调用那些将它们解释为向量的字节。sizeof(void*)
.reserve(100)
在链接时,这两种实现中的一种被丢弃并被另一种使用。更重要的是,哪个被丢弃在完整构建中可能是相当确定的。在部分构建中,它可能会更改顺序。
因此,现在您的代码可以在完整构建中构建并“正常”工作,但部分构建会导致内存损坏。并且更改 makefile 中文件的顺序可能会导致内存损坏,甚至更改 lib 文件链接的顺序,或升级编译器等。
如果两个 cpp 文件都有一个namespace {}
块,其中包含您要导出的内容(可以使用完全限定的命名空间名称)之外的所有内容,则不会发生这种情况。
我已经在生产中多次准确地发现了这个错误。考虑到它是多么的微妙,我不知道它滑过多少次,等待它突袭的时刻。