为什么在 2 个不同的 cpp 文件中定义内联全局函数会导致神奇的结果?
Why does defining inline global function in 2 different cpp files cause a magic result?
假设我有两个 .cpp 文件 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.cpp
中我已经向前声明了 f1()
和 f2()
:
void f1();
void f2();
int main()
{
f1();
f2();
}
结果(不依赖于构建,debug/release 构建 的结果相同):
f1
f1
哇哦:编译器以某种方式仅从 file1.cpp
中选取定义并在 f2()
中使用它。这种行为的确切解释是什么?
请注意,将 inline
更改为 static
是解决此问题的方法。将内联定义放在未命名的命名空间中也可以解决问题,程序打印:
f1
f2
编译器可能会假定同一 inline
函数的所有定义在所有翻译单元中都是相同的,因为标准是这么说的。所以它可以选择它想要的任何定义。在你的例子中,恰好是 f1
的那个。
请注意,您不能依赖编译器总是选择相同的定义,违反上述规则会使程序格式错误。编译器也可以诊断并出错。
如果函数是 static
或在匿名命名空间中,则有两个不同的函数称为 foo
,编译器必须从正确的文件中选择一个。
相关标准参考:
An inline function shall be defined in every translation unit in which it is odr-used and shall have exactly
the same definition in every case (3.2). [...]
N4141中的7.1.2/4,强调我的。
这是未定义的行为,因为具有外部链接的同一个内联函数的两个定义打破了 C++ 对可以在多个地方定义的对象的要求,称为 一个定义规则 :
3.2 One definition rule
...
- There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14),[...] in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then
6.1 each definition of D shall consist of the same sequence of tokens; [...]
这不是 static
函数的问题,因为一个定义规则不适用于它们:C++ 认为在不同翻译单元中定义的 static
函数是相互独立的。
澄清点:
虽然根植于 C++ 内联规则的答案是正确的,但它仅适用于同时编译两个源代码的情况。如果它们是单独编译的,那么,正如一位评论员指出的那样,每个生成的目标文件都将包含自己的 'foo()'。但是:如果这两个目标文件随后链接在一起,那么因为两个 'foo()'-s 都是非静态的,所以名称 'foo()' 出现在两个目标文件的导出符号 table 中;然后 链接器 必须合并两个 table 条目,因此所有内部调用都重新绑定到两个例程之一(大概是处理的第一个目标文件中的那个,因为它已经绑定 [即链接器将第二条记录视为 'extern' 而不管绑定])。
正如其他人指出的那样,编译器符合 C++ 标准,因为 一个定义规则 规定您只能定义一个函数,除非函数是内联的,那么定义必须相同。
在实践中,函数被标记为内联,在 linking 阶段,如果它遇到一个内联标记标记的多个定义,linker 会默默地丢弃所有但一个。如果它遇到未内联标记的令牌的多个定义,则会生成错误。
这个 属性 被称为 inline
因为,在 LTO 之前(link 时间优化),在调用站点获取函数体并 "inlining"要求编译器具有函数的主体。 inline
函数可以放在头文件中,每个 cpp 文件都可以看到主体和 "inline" 调用站点中的代码。
这并不意味着代码 实际上 将被内联;相反,它使编译器更容易内联它。
但是,我不知道编译器会在丢弃重复项之前检查定义是否相同。这包括以其他方式检查函数体定义是否相同的编译器,例如 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) {}
};
在 class 的主体中定义的方法是 隐式内联 。 ODR 规则适用。这里我们有两个不同的 Helper::Helper()
,都是内联的,而且它们不同。
两个 classes 的大小不同。在一种情况下,我们用 0
初始化两个 sizeof(double)
(因为在大多数情况下零浮点数是零字节)。
在另一个例子中,我们首先将 three sizeof(void*)
初始化为零,然后在这些字节上调用 .reserve(100)
将它们解释为向量。
在link时,这两个实现中的一个被丢弃并被另一个使用。更重要的是,在完整构建中丢弃哪个可能是非常确定的。在部分构建中,它可能会更改顺序。
所以现在您的代码可以在完整构建中构建和工作 "fine",但部分构建会导致内存损坏。更改 makefile 中文件的顺序可能会导致内存损坏,甚至更改 lib 文件的顺序 linked,或升级编译器等
如果两个 cpp 文件都有一个 namespace {}
块,其中包含除您要导出的内容(可以使用完全限定的命名空间名称)之外的所有内容,则不会发生这种情况。
我已经多次在生产中发现这个错误。如此微妙,不知溜过多少次,只待扑上去。
假设我有两个 .cpp 文件 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.cpp
中我已经向前声明了 f1()
和 f2()
:
void f1();
void f2();
int main()
{
f1();
f2();
}
结果(不依赖于构建,debug/release 构建 的结果相同):
f1
f1
哇哦:编译器以某种方式仅从 file1.cpp
中选取定义并在 f2()
中使用它。这种行为的确切解释是什么?
请注意,将 inline
更改为 static
是解决此问题的方法。将内联定义放在未命名的命名空间中也可以解决问题,程序打印:
f1
f2
编译器可能会假定同一 inline
函数的所有定义在所有翻译单元中都是相同的,因为标准是这么说的。所以它可以选择它想要的任何定义。在你的例子中,恰好是 f1
的那个。
请注意,您不能依赖编译器总是选择相同的定义,违反上述规则会使程序格式错误。编译器也可以诊断并出错。
如果函数是 static
或在匿名命名空间中,则有两个不同的函数称为 foo
,编译器必须从正确的文件中选择一个。
相关标准参考:
An inline function shall be defined in every translation unit in which it is odr-used and shall have exactly the same definition in every case (3.2). [...]
N4141中的7.1.2/4,强调我的。
这是未定义的行为,因为具有外部链接的同一个内联函数的两个定义打破了 C++ 对可以在多个地方定义的对象的要求,称为 一个定义规则 :
3.2 One definition rule
...
- There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14),[...] in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then
6.1 each definition of D shall consist of the same sequence of tokens; [...]
这不是 static
函数的问题,因为一个定义规则不适用于它们:C++ 认为在不同翻译单元中定义的 static
函数是相互独立的。
澄清点:
虽然根植于 C++ 内联规则的答案是正确的,但它仅适用于同时编译两个源代码的情况。如果它们是单独编译的,那么,正如一位评论员指出的那样,每个生成的目标文件都将包含自己的 'foo()'。但是:如果这两个目标文件随后链接在一起,那么因为两个 'foo()'-s 都是非静态的,所以名称 'foo()' 出现在两个目标文件的导出符号 table 中;然后 链接器 必须合并两个 table 条目,因此所有内部调用都重新绑定到两个例程之一(大概是处理的第一个目标文件中的那个,因为它已经绑定 [即链接器将第二条记录视为 'extern' 而不管绑定])。
正如其他人指出的那样,编译器符合 C++ 标准,因为 一个定义规则 规定您只能定义一个函数,除非函数是内联的,那么定义必须相同。
在实践中,函数被标记为内联,在 linking 阶段,如果它遇到一个内联标记标记的多个定义,linker 会默默地丢弃所有但一个。如果它遇到未内联标记的令牌的多个定义,则会生成错误。
这个 属性 被称为 inline
因为,在 LTO 之前(link 时间优化),在调用站点获取函数体并 "inlining"要求编译器具有函数的主体。 inline
函数可以放在头文件中,每个 cpp 文件都可以看到主体和 "inline" 调用站点中的代码。
这并不意味着代码 实际上 将被内联;相反,它使编译器更容易内联它。
但是,我不知道编译器会在丢弃重复项之前检查定义是否相同。这包括以其他方式检查函数体定义是否相同的编译器,例如 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) {}
};
在 class 的主体中定义的方法是 隐式内联 。 ODR 规则适用。这里我们有两个不同的 Helper::Helper()
,都是内联的,而且它们不同。
两个 classes 的大小不同。在一种情况下,我们用 0
初始化两个 sizeof(double)
(因为在大多数情况下零浮点数是零字节)。
在另一个例子中,我们首先将 three sizeof(void*)
初始化为零,然后在这些字节上调用 .reserve(100)
将它们解释为向量。
在link时,这两个实现中的一个被丢弃并被另一个使用。更重要的是,在完整构建中丢弃哪个可能是非常确定的。在部分构建中,它可能会更改顺序。
所以现在您的代码可以在完整构建中构建和工作 "fine",但部分构建会导致内存损坏。更改 makefile 中文件的顺序可能会导致内存损坏,甚至更改 lib 文件的顺序 linked,或升级编译器等
如果两个 cpp 文件都有一个 namespace {}
块,其中包含除您要导出的内容(可以使用完全限定的命名空间名称)之外的所有内容,则不会发生这种情况。
我已经多次在生产中发现这个错误。如此微妙,不知溜过多少次,只待扑上去。