【问题标题】:Why doesn't calling member function invoke the ODR-USE of that object?为什么调用成员函数不调用该对象的 ODR-USE?
【发布时间】:2023-04-08 07:14:01
【问题描述】:

cppref 说,

如果非内联变量(C++17 起)的初始化延迟发生在 main/thread 函数的第一个语句之后,它发生在任何第一个 odr-use 之前在与要初始化的变量相同的翻译单元中定义的具有静态/线程存储持续时间的变量。

后面给出了一个延迟动态初始化的例子:

// - 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 的第一条语句之后的某个时刻被初始化(它使用File 1 中定义的函数,强制其动态初始化运行),那么@ 987654326@ 将在其用于 A::A 之前被初始化

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

【问题讨论】:

  • 我很困惑。为什么你认为a 没有初始化?
  • @LightnessRacesinOrbit 因为在a的构造函数中它调用b.Use()。如果a被构造,那么b也将被构造。但是cppref说b被初始化为a.Use(),不是A::A()
  • @LightnessRacesinOrbit 并且引用的段落说:“如果amain的第一条语句之后的某个时刻被初始化”,这意味着aa.Use()之后被初始化。
  • a.Use() 强制“加载”File2(初始化a)。创建a 强制“加载”File3(然后初始化b)。
  • @LightnessRacesinOrbit 我认为这个测试用例实际上与 cppref 所说的和原始问题无关。这只是一个未定义的行为,因为未指定不同编译单元中全局变量的初始化顺序。所以在这种情况下,“一个对象将在你调用函数之前被初始化”是不正确的。

标签: c++ initialization language-lawyer one-definition-rule


【解决方案1】:

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

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

在每个翻译单元中,初始化器的出现顺序是有顺序的。这也是它们被初始化的时间顺序,但它可能与它们在内存中的顺序不同(如果这是确定的——严格来说,C++ 不会强制执行)。这不会导致初始化器的顺序翻译单元。它确实在执行该翻译单元的函数之前发生,因为这些函数可能依赖于初始化的对象。

翻译单元中的功能当然可以以任何顺序出现;它们的执行方式取决于您在其中写入的内容。

现在有一些东西会施加额外的排序限制。我了解到您知道即使在 main() 启动之后,某些初始化程序也可以运行。如果发生这种情况,通常的规则仍然适用,即单个 TU 的初始化程序必须在该 TU 中的函数之前执行。

在这种情况下,TU file1 包含 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 &lt; a.Use 的顺序也成立。

如果你让A::A 依赖于bB::B 依赖于a,你可能会遇到问题。如果引入循环依赖,无论先初始化哪个对象,它总是依赖于一个尚未初始化的对象。不要那样做。

【讨论】:

  • 我以为你误解了这个问题,我在这里感到困惑的是odr-use,而不是初始化顺序。
  • @bigxiao:我一点也不困惑。 aodr-use 是我给定序关系a &lt; a.Use 的原因,a 的初始化肯定发生在a.Use 之前。还有一个命令b&lt;b.Use,但被b&lt;A::A&lt;a&lt;a.Use&lt;b.Use 击败。
  • 为什么是a &lt; a.Usea.Use 可以在构造 a 之前调用。
  • @bigxiao:不,它不能,因为你在问题开始时给出的确切原因。
  • this question。不保证。
【解决方案2】:

总之,为什么要打扰ab的初始化顺序呢?

该示例没有表明a 应该必须在b 之前初始化以使程序定义明确。

  • 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 是一个 odr-use (6.2) 不是由初始化直接或间接引起的 非本地静态或线程存储持续时间变量。

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();
}

在输入 main 之前是否初始化 ab 或者是否 初始化会延迟到 a 第一次在 main 中使用。特别是,如果 amain 之前初始化 输入,不保证b会在被a的初始化odr使用之前被初始化,即 在调用 A::A 之前。然而,如果amain 的第一条语句之后的某个时刻被初始化,b 将是 在A::A 中使用之前初始化。 ——结束示例]

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

【讨论】:

  • 那么,是否正确: 1. 调用成员函数是对该对象的 odr-use。 2.一个TU中的所有对象在该TU中的任何对象发生odr-use之前都被初始化,即如果某个对象的odr-use即将发生,那么该TU中的所有对象都需要被初始化,无论它们是否会被 ODR 使用?
  • 1.当它不平凡时。正如我所知,每个用户定义的函数都应该是这样。 2. 不。 (a) 你会排除局部静态变量和线程局部变量,对吗? (b) 程序启动期间使用 odr 和存在非局部静态变量的初始化都是程序的静态属性。初始化要求不受影响,只允许改变执行的顺序。如果程序在初始化期间没有退出,那么所有这些都将完成。另请注意,odr-use 仅在动态初始化规则中引用,而非传统规则。
  • 好的,从你的引用看来,我可以得出两个结论: 1.如果动态初始化没有延迟,那么b可能在它的odr-use之前没有被初始化,这可能导致未定义的行为。 2.如果延迟了,那么b保证在A:A()之前初始化,因为A::A()b在同一个TU。我说的对吗?
猜你喜欢
  • 2014-06-14
  • 1970-01-01
  • 2012-01-02
  • 1970-01-01
  • 2017-12-29
  • 1970-01-01
相关资源
最近更新 更多