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