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

C++ 为什么在两个不同的cpp文件中定义内联全局函数会产生神奇的结果?,c++,inline,translation-unit,C++,Inline,Translation Unit,假设我有两个.cpp文件file1.cpp和file2.cpp: // file1.cpp #include <iostream> inline void foo() { std::cout << "f1\n"; } void f1() { foo(); } 结果(不依赖于生成,调试/发布生成的结果相同): 哇:编译器不知何故只从file1.cpp中选取定义,并在f2()中使用它。这种行为的确切解释是什么 请注意,将inline更改为static是此

假设我有两个.cpp文件
file1.cpp
file2.cpp

// file1.cpp
#include <iostream>

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

void f1()
{
    foo();
}
结果(不依赖于生成,调试/发布生成的结果相同):

哇:编译器不知何故只从
file1.cpp
中选取定义,并在
f2()
中使用它。这种行为的确切解释是什么

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

f1
f2

编译器可能会假设同一
内联
函数的所有定义在所有翻译单元中都是相同的,因为标准是这样规定的。所以它可以选择它想要的任何定义。在您的例子中,正好是带有
f1

请注意,您不能依赖编译器总是选择相同的定义,违反上述规则会导致程序格式错误。编译器还可以诊断该错误并将其排除

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


相关标准供参考:

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


<4141中的P>7.1.2/4,强调MI. < /P> < P>这是未定义行为,因为同一个内联函数与外部链接的两个定义打破了对可以在几个地方定义的对象的C++要求,称为一个定义规则:

3.2一条定义规则

  • 在一个程序中,一个类类型(第9条)、枚举类型(7.2)、带外部链接的内联函数(7.1.2)、类模板(第14条)、[…]可以有多个定义,只要每个定义出现在不同的翻译单元中,并且这些定义满足以下要求。给定在多个翻译单元中定义的名为D的实体,则
  • 6.1 D的每个定义应由相同的令牌序列组成;[……]


    <>这不是一个与静态> /Cuff>函数有关的问题,因为一个定义规则不适用于它们:C++认为不同的翻译单元中定义的代码>静态< /代码>函数是相互独立的。
    尽管C++中的内联规则的答案是正确的,但只有在两个源编译在一起时才适用。如果它们是单独编译的,那么正如一位评论员所指出的,每个生成的对象文件都将包含自己的“foo()”。但是:如果将这两个对象文件链接在一起,则由于两个“foo()”-s都是非静态的,因此名称“foo()”将出现在两个对象文件的导出符号表中;然后,链接器必须合并两个表项,因此所有内部调用都会重新绑定到两个例程中的一个(可能是处理的第一个对象文件中的一个,因为它已经绑定了[即链接器会将第二个记录视为'extern',而不管绑定如何]。

    如其他人所述,编译器遵守C++标准,因为<强>一条定义规则表示只有一个函数定义,除非函数是内联的,否则定义必须相同。

    实际上,函数被标记为内联,在链接阶段,如果它运行到内联标记令牌的多个定义中,链接器会自动放弃除一个外的所有定义。如果它遇到未内联标记的令牌的多个定义,则会生成一个错误

    此属性称为
    inline
    ,因为在LTO(链接时间优化)之前,获取函数体并在调用站点“内联”函数体需要编译器拥有函数体<代码>内联
    函数可以放在头文件中,每个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) {}
    };
    
    //A.cpp
    结构辅助程序{
    std::矢量foo;
    助手(){
    粮食储备(100);
    }
    };
    //B.cpp
    结构辅助程序{
    双x,y;
    Helper():x(0),y(0){}
    };
    
    类主体中定义的方法是隐式内联的。ODR规则适用。这里我们有两个不同的
    Helper::Helper()
    ,都是内联的,而且它们不同

    这两个班级的规模不同。在一种情况下,我们使用
    0
    初始化两个
    sizeof(double)
    (因为在大多数情况下,零浮点是零字节)

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

    在链接时,这两个实现中的一个被丢弃,另一个使用。更重要的是,哪一个被丢弃在一个完整的构建中可能是相当有威慑力的。在部分构建中,它可能会改变顺序

    因此,现在您有了可以在完整构建中构建并工作“良好”的代码,但部分构建会导致内存损坏。更改makefiles中文件的顺序可能会导致内存损坏,或者
    f1
    f1
    
    f1
    f2
    
    // A.cpp
    struct Helper {
      std::vector<int> foo;
      Helper() {
        foo.reserve(100);
      }
    };
    // B.cpp
    struct Helper {
      double x, y;
      Helper():x(0),y(0) {}
    };