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

时间:2017-05-02 11:07:18

标签: c++ inline translation-unit

假设我有两个.cpp文件 "stockEventsSettings": { "balloonColor": "#008800" } file1.cpp

file2.cpp

// file1.cpp
#include <iostream>

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

void f1()
{
    foo();
}

// file2.cpp #include <iostream> inline void foo() { std::cout << "f2\n"; } void f2() { foo(); } 我已向前宣布main.cppf1()

f2()

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

void f1();
void f2();

int main()
{
    f1();
    f2();
}

哇:编译器只选择f1 f1 中的定义并在file1.cpp中使用它。这种行为的确切解释是什么?。

注意,将f2()更改为inline是此问题的解决方案。将内联定义放在未命名的命名空间中也可以解决问题并打印程序:

static

4 个答案:

答案 0 :(得分:41)

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

  

3.2一个定义规则

     

...

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

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

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

答案 1 :(得分:31)

编译器可能会假设相同inline函数的所有定义在所有翻译单元中都是相同的,因为标准是这样说的。所以它可以选择它想要的任何定义。在您的情况下,恰好是f1的那个。

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

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

相关标准供参考:

  

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

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

答案 2 :(得分:11)

正如其他人所说,编译器符合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(因为在大多数情况下零浮点数是零字节)。

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

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

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

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

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

答案 3 :(得分:-3)

澄清点:

虽然根植于C ++内联规则的答案是正确的,但它仅适用于两个源一起编译的情况。如果它们是单独编译的,那么,正如一位评论员指出的那样,每个结果对象文件都包含自己的'foo()'。但是:如果这两个目标文件然后链接在一起,那么因为'foo()' - s都是非静态的,所以名称'foo()'出现在两个目标文件的导出符号表中;然后链接器必须合并两个表条目,因此所有内部调用都重新绑定到两个例程之一(可能是处理的第一个目标文件中的一个,因为它已被绑定[即无论绑定如何,链接器都会将第二条记录视为“extern”。)