为什么在2个不同的cpp文件中定义内联全局函数会产生神奇的结果?

Nar*_*yan 30 c++ inline translation-unit

假设我有两个.cpp文件file1.cppfile2.cpp:

// file1.cpp
#include <iostream>

inline void foo()
{
    std::cout << "f1\n";
}

void f1()
{
    foo();
}
Run Code Online (Sandbox Code Playgroud)

// file2.cpp
#include <iostream>

inline void foo()
{
   std::cout << "f2\n";
}

void f2()
{
    foo();
}
Run Code Online (Sandbox Code Playgroud)

main.cpp我已经向前宣布f1()f2():

void f1();
void f2();

int main()
{
    f1();
    f2();
}
Run Code Online (Sandbox Code Playgroud)

结果(不依赖于构建,调试/发布版本的结果相同):

f1
f1
Run Code Online (Sandbox Code Playgroud)

哇:编译器以某种方式仅选择定义file1.cpp并在其中使用它f2().这种行为的确切解释是什么?

注意,改变inlinestatic是这个问题的解决方案.将内联定义放在未命名的命名空间中也可以解决问题并且程序打印:

f1
f2
Run Code Online (Sandbox Code Playgroud)

das*_*ght 40

这是未定义的行为,因为具有外部链接的同一内联函数的两个定义会破坏可在多个位置定义的对象的C++要求,称为一个定义规则:

3.2一个定义规则

...

  1. 可以有多个类类型的定义(第9条),枚举类型(7.2),带有外部链接的内联函数(7.1.2),类模板(第14条),[...]在程序中提供的每个定义出现在不同的翻译单元中,并且定义满足以下要求.鉴于这样一个名为D的实体在多个翻译单元中定义,那么

6.1 D的每个定义应由相同的令牌序列组成; [...]

这不是static函数的问题,因为一个定义规则不适用于它们:C++认为static在不同的转换单元中定义的函数彼此独立.


Bau*_*gen 30

编译器可以假设inline所有翻译单元的相同功能的所有定义都是相同的,因为标准是这样说的.所以它可以选择它想要的任何定义.在你的情况下,碰巧是那个f1.

请注意,您不能依赖编译器始终选择相同的定义,违反上述规则会使程序格式错误.编译器也可以诊断出错误并输出错误.

如果函数是static或在匿名命名空间中,则调用两个不同的函数foo,编译器必须从右侧文件中选择一个.


相关标准供参考:

内联函数应在每个使用过的翻译单元中定义,并且在每种情况下都应具有完全相同的定义(3.2).[...]

N4141中的7.1.2/4,强调我的.


Yak*_*ont 11

正如其他人所指出的那样,编译器符合C++标准,因为One定义规则规定您只有一个函数定义,除非函数是内联的,那么定义必须相同.

在实践中,会发生的事情是该函数被标记为内联,并且在链接阶段,如果它遇到内联标记令牌的多个定义,则链接器会静默地丢弃除一个之外的所有内容.如果它遇到未标记为内联的令牌的多个定义,则会生成错误.

调用此属性是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) {}
};
Run Code Online (Sandbox Code Playgroud)

类的主体中定义的方法是隐式内联的.ODR规则适用.这里我们有两个不同的Helper::Helper(),内联的,它们不同.

这两个类的大小不同.在一种情况下,我们初始化2 sizeof(double)0(作为零浮动是在大多数情况下零个字节).

在另一个中,我们首先用零初始化三个 sizeof(void*),然后调用.reserve(100)那些将它们解释为向量的字节.

在链接时,这两个实现中的一个被丢弃并被另一个实现使用.更重要的是,哪一个被丢弃在完整版本中可能是相当确定的.在部分构建中,它可以改变顺序.

所以现在你有了可以在完整版本中构建和工作"正常"的代码,但是部分构建会导致内存损坏.并且更改makefile中文件的顺序可能会导致内存损坏,甚至更改lib文件的链接,或升级编译器等.

如果两个cpp文件都有一个namespace {}包含除导出内容之外的所有内容的块(可以使用完全限定的命名空间名称),则不会发生这种情况.

我已经多次在生产中发现了这个错误.考虑到它是多么微妙,我不知道它有多少次滑过,等待它突然爆发的时刻.