【问题标题】:The issue about the deferred dynamic initialization关于延迟动态初始化的问题
【发布时间】:2020-10-26 11:30:18
【问题描述】:

考虑basic.start.dynamic部分中的示例,即:

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

// - 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();  //#2
  b.Use();
}

下面的 cmets 是:

但是,如果 a 在 main 的第一条语句之后的某个时间点被初始化,则 b 将在其用于 A​::​A 之前被初始化

我不明白为什么 b 在使用 A​::​A 之前会被保证初始化,而当 a 在 main 的第一条语句之后的某个时间点被初始化时。按照规定:

basic.start.dynamic#4

具有静态存储持续时间的非局部非内联变量的动态初始化是在main的第一条语句之前排序还是延迟,由实现定义。 如果它被延迟,它强烈发生在任何非初始化之前 - 使用与要初始化的变量在同一翻译单元中定义的任何非内联函数或非内联变量。

basic.start.dynamic#3

非初始化 odr-use 是 odr-use ([basic.def.odr]) 不是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的 .

我能理解的是,当初始化被推迟时,变量a应该在变量a的odr-use(non-initialization odr-use)之前初始化,在标记的地方与#2。但是我无法理解的是,评论说 b 将在其用于 A​::​A 之前被初始化。 IIUC,函数A::A 的调用是变量a 初始化的一部分,因此变量b#1 的odr 使用不是非初始化odr 使用,因为它是由非局部静态或线程存储持续时间变量的初始化直接或间接引起。我认为它只能说变量b保证在#2之前被初始化,为什么评论说b将在其用于A之前被初始化​::​A ?如何解读这个例子?

【问题讨论】:

  • 为什么会出现“非本地静态存储时长变量的初始化直接或间接引起的”异常呢?为了避免解决循环初始化问题?
  • @Peter-ReinstateMonica 我认为这句话是用来限制 odr-use 的情况,即什么样的 odr-use 可以触发这些被延迟的动态初始化的发生。回到我的问题,b#1 的 odr 使用不是句子所说的例外,因此它不应该触发动态初始化的发生。但是评论说它触发了b的初始化,我只是不知道为什么这么说。
  • 我们同意;我想知道这个限制被纳入标准的原因。
  • @cigien 好吧,我删除了c++17 标签。感谢您的建议。
  • @aschepler A::A() 的 odr-use 是由 a 在选择 A::A() 时的初始化直接引起的,因此导致 bA::A() 内的 odr-use间接通过a的初始化。因为说明不延迟的情况的注释表明,即“特别是,如果 a 在进入 main 之前初始化,则不能保证 b 将在它被 a 的初始化 odr 使用之前被初始化,也就是说,在调用 A​::​A 之前。”

标签: c++ language-lawyer


【解决方案1】:

从句的演变

有问题的(非规范)示例可以追溯到标准的 C++98 版本,但托管子句中的(规范)语言在 C++17 中发生了更改。

C++98:

3.6.2 非本地对象的初始化[basic.start.init]

3 - 命名空间范围对象的动态初始化([交叉引用])是否在 main 的第一条语句之前完成是实现定义的。如果初始化延迟到 main 的第一个语句之后的某个时间点,它应该发生在第一次使用与要初始化的对象相同的翻译单元中定义的任何函数或对象之前。 [关于副作用的脚注] [示例如下]

C++03 有同样的文字。 C ++ 11删除了交叉引用并将“命名空间范围的对象”替换为“具有静态存储持续时间的非局部变量”,“对象”替换为“变量”,以及“使用”替换为“odr-use”,但我会提出该条款的含义没有改变。 C++14 没有改变。

该语言随后由P0250R3 更改,于 2017 年 3 月发布并转录为标准草案,正好赶上 C++17。 P0250R3 增加了non-initialization odr-use 的定义,并修改了子句以引用该定义,同时还用线程感知术语(sequenced before)表达了事件之间的关系, 强烈发生在等之前),并添加了关于避免死锁的注释。

从那时起,关于避免死锁的说明已被修改为推荐做法

措辞改变的动机

幸运的是,P0250R3 包含了动机的讨论。在顺序程序的并行初始化部分,我们读到:

目前,我们非常明确地允许静态构造函数在 main 启动后运行,而不管其他线程是否启动。这似乎是出于支持的意图,例如引用函数符号时延迟加载动态库,如 Posix 系统上的 RTLD_LAZY。即使静态命名空间范围的构造函数在库加载时立即运行,库也可能在 main 启动后隐式加载。

还有:

SG1 通常认为应避免使用静态命名空间范围的构造函数 [...] 我们决定将此类构造函数限制在现有线程中,这似乎与已知实现一致。

例子的正确性。

我认为这个例子一直是不正确的。

在 C++98 中,该示例是不正确的,因为该版本标准中的规范性措辞导致了循环性。假设我们扩充示例以在与 a 的定义相同的 TU 中定义构造函数 B::B

// - File 2 -
#include "a.h"
A a;
B::B() {
   a.Use();
}

现在根据 C++98,a 的(动态)初始化发生在第一次调用 B::B 之前,并且b 的初始化发生在第一次调用 A::A 之前。但是a的初始化需要调用A::Ab的初始化需要调用B::B。所以我们有一个循环回归。

P0250R3 中的措辞更改(将 odr-use 更改为 non-initialization odr-use)打破了这种循环,代价是使示例变得荒谬。但后来它总是坏掉。这就是 SIOF,可以通过 Construct on First Use 习惯用法或使用辅助对象(例如 ios_base::Init)来避免。

实施实践

我将示例(带有循环性)编译成一个(Linux,ELF;CentOS 7.8)共享对象,在使用dlopen 输入main 后加载到程序中。恰好ab 之一在未初始化状态下被odr 使用,该状态取决于链接顺序。

这表明对non-initialization odr-use的措辞变化反映了实施实践。不幸的是,该标准现在包含一个明显不正确的示例,但由于示例和注释是非规范性的,这有问题但不是致命的。

【讨论】:

  • 如果我正确理解当前标准,正如它所说,变量 b 应该在 a.Use(); 之前的某个时间点初始化,而不是在 A::A() 内的 b.Use(); 之前的某个时间点初始化?因为A::A()b的odr-use是由变量a的初始化引起的。
  • 但是,该标准并未说明在a.Use() 之前首先初始化哪个变量,此外,根据当前标准,变量b 应在A::A() 内保持未初始化状态。
  • a.Use(从 main 调用)是 a 的非初始化 odr 使用。在定义 a 的 TU 中定义的唯一变量是 a 本身,因此仅在从 main 调用 a.Use 之前初始化 aa 的初始化调用(是 odr-use)A::A,它在与 b 相同的 TU 中定义,但这不是非初始化 odr-use(因为它是由初始化间接引起的) 所以在调用A::A之前不需要初始化b
  • 此外,我认为函数A::Use应该是inline function,只有在类中定义了A::Use的定义,代码才会遵守odr规则。因此,A::Use 的使用不会导致 a 的初始化,因为它是一个内联函数。所以,我认为变量a本身的odr-use会触发a的初始化,你同意吗?
  • 是的,这就是我的理解,包括标准的规范性文本和示例的行为方式。 A::Use 必须是一个内联函数(因为它没有显式定义,并且每个标头都包含在多个 TU 中)所以它是 a 本身的 odr 使用触发了它自己的初始化。
猜你喜欢
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多