为什么调用成员函数不调用该对象的 ODR-USE?

Why doesn't calling member function invoke the ODR-USE of that object?

本文关键字:调用 ODR-USE 对象 成员 函数 为什么      更新时间:2023-10-16

在cppref中说,

如果非内联变量(自 C++17 起)的初始化推迟在主/线程函数的第一条语句之后发生,则它发生在第一次使用任何变量之前,该变量具有静态/线程存储持续时间,在与要初始化的变量相同的转换单元中定义

稍后它给出了延迟动态初始化的示例:

// - File 1 -
#include "a.h"
#include "b.h"
B b;
A::A(){ b.Use(); }
// - File 2 -
#include "a.h"
A a;
// - File 3 -
#include "a.h"
#include "b.h"
extern A a;
extern B b;
int main() {
a.Use();
b.Use();
}

评论说:

如果amain的第一条语句之后的某个时间点初始化(odr-使用File 1中定义的函数,强制其动态初始化运行),则b将在 A::A 中使用之前初始化

为什么会发生IF情况?a.Use()odr-usea因此a必须在此语句之前初始化吗?

我认为你被C++事物的顺序误导了。

翻译单元 (TU= 一个.cpp文件及其标头)在C++中没有顺序。它们可以按任何顺序编译,也可以并行编译。唯一特殊的TU是包含main()的TU的,但即使是那个也可以随时按顺序编译。

在每个翻译单元中,初始值设定项的显示顺序是有的。这也是初始化它们的时间顺序,但它可能与它们在内存中的顺序不同(如果甚至确定 - C++严格来说并不强制)。这不会导致跨翻译单元初始值设定项顺序。它确实在执行该转换单元的函数之前发生,因为这些函数可能依赖于初始化的对象。

翻译单元中的函数当然可以按任何顺序出现;它们的执行方式取决于你在它们中写了什么。

现在有一些事情施加了额外的排序约束。我知道您知道某些初始值设定项即使在main()启动后也可以运行。如果发生这种情况,普通规则仍然适用,即单个 TU 的初始值设定项必须执行该 TU 中的函数。

在这种情况下,TUfile1持有b的(默认)初始值设定项,该初始值设定项必须在同一 TU 中A::A之前运行。正如您正确指出的那样,a.Use必须在初始化后发生a.这需要A::A.

因此,我们有以下顺序关系(其中<表示precedes)

b < A::A
A::A < a
a < a.Use

因此是可传递的

b < a.Use

如您所见,在a.Use中使用a.c是安全的,因为A::A < a.Use顺序也保持不变。

如果你A::A依赖于bB::B依赖于a,你可能会遇到问题。如果引入循环依赖项,无论首先初始化哪个对象,它始终依赖于尚未初始化的对象。别这样。

简而言之,为什么要打扰ab的初始化顺序?

该示例未显示任何指示在b之前必须初始化a以使程序定义良好。

  • 的确,extern A a;extern B b;之前,但这与订单无关。

  • 同样,a.Use();中的求值在从文件 3 翻译的 TU 中的main函数中b.Use();求值之前进行排序,但这仍然与顺序无关。

使a.Use()被明确定义与此特定顺序无关,除非存在其他依赖关系(例如子对象初始化意味着顺序)。

OTOH,如果你想要额外的订单,你如何指定它?

附件:

"发生在主/线程函数的第一个语句之后"的措辞很奇怪。似乎有意的那个是"在主/线程函数的第一个语句之前不发生",编辑偶尔会错过在单个语句的评估中可以有多个适用于二进制关系的评估。这源于标准,但已被P0250R3纠正。

实际上,我发现该示例来自标准,引用自N4727 [basic.start.dynamic]/4:

3非初始化 odr-use是指不是由初始化 非本地静态或线程存储持续时间变量。

4 实现定义是否用静态非局部非内联变量进行动态初始化 存储持续时间在main的第一个语句之前排序或延迟。如果推迟,则强烈 发生在任何非初始化 ODR 使用任何非内联函数或非内联变量之前 与要初始化的变量相同的转换单元.55 它是实现定义的,其中线程和 在程序中的哪个点发生这种延迟的动态初始化。[ 注:这些要点应为 以允许程序员避免死锁的方式进行选择。—尾注 ] [ 示例:

// - File 1 -
#include "a.h"
#include "b.h"
B b;
A::A(){
b.Use();
}
// - File 2 -
#include "a.h"
A a;
// - File 3 -
#include "a.h"
#include "b.h"
extern A a;
extern B b;
int main() {
a.Use();
b.Use();
}

它是实现定义的,无论在输入a还是b之前初始化main初始化将延迟到a首次在main中使用 ODR 之前。特别是,如果在amain输入,不保证b在被a初始化ODR使用之前会被初始化,即 在调用A::A之前。但是,如果amain的第一个语句之后的某个时间点初始化,b将是 在A::A中使用之前已初始化。—结束示例 ]

55) 在这种情况下,初始化具有静态存储持续时间且具有副作用的初始化的非局部变量,即使它是 本身不是ODR使用的(6.2,6.6.4.1)。

我认为 cpppreferences 示例中的注释与示例试图传达的内容根本不匹配。它应该用b的初始化来表达,而不是a的初始化:

  • 如果b的初始化没有延迟,那么不能保证它在a的初始化之前发生。编译器可能会选择一些顺序,例如 (1) 初始化 TU 2,(2) 初始化 TU 1,(3) 执行main()。(请注意,这些步骤也可能是交错的。
  • 如果b的初始化推迟,那么它必须在任何 odr 使用任何非内联变量或非内联函数之前进行,这些函数在与b相同的翻译单元中定义。由于A::A的定义与b在同一个翻译单元中,这意味着b必须在调用A构造函数之前进行初始化。

但是,整个分析不再正确,因为P0250R3更改了规则,因此初始化只需要在同一翻译单元中定义的非内联变量和非内联函数的任何"非初始化odr-use"之前进行。A::A的 odr-use 只是由a的初始化引起的,所以它不是非初始化的 odr-use,所以不能保证b的初始化发生在它之前。

(我不完全确定他们为什么要进行此更改,但至少在这种情况下,结果是有意义的。与非延迟初始化相比,延迟初始化不应在初始化之间为您提供额外的排序保证,如果您依赖此类排序保证,则您的代码是不可移植的并且极难理解;希望您现在已经修复了它。