有关延迟动态初始化的问题

时间:2020-10-26 11:30:18

标签: c++ language-lawyer

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

示例后面的评论是:

但是,如果a在main的第一条语句之后的某个时刻被初始化,则b将被初始化,然后再在A :: A 中使用它。

我不明白为什么在{main}的第一个语句之后的某个时刻初始化a时,为什么保证b在A :: A中使用之前会被初始化。根据规则说:

basic.start.dynamic#4

由实现定义的是,具有静态存储持续时间的非局部非内联变量的动态初始化是在main的第一条语句之前进行排序还是推迟。 如果推迟,则很可能在未初始化odr使用与要初始化的变量相同转换单元中定义的任何非内联函数或非内联变量之前发生。

basic.start.dynamic#3

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

我能理解的是,当延迟初始化时,变量a应该在变量a的odr-use(非初始化odr-use)之前被初始化。在标有#2的地方。但是我不明白的是,评论说 b将在A :: A 中使用之前进行初始化。 IIUC,函数A::A的调用是变量a初始化的一部分,因此在b上对变量#1的使用并非非由于初始化odr-use是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的。我认为只能说保证变量b会在#2之前初始化,为什么注释说b将在A中使用之前先初始化b :: A ?如何解释这个例子?

1 个答案:

答案 0 :(得分:6)

子句的演变

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

C ++ 98:

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

3-是否定义空间范围对象的动态初始化([cross-references])是否在main的第一条语句之前进行,由实现定义。如果将初始化推迟到main的第一条语句之后的某个时间点,则应在首次使用与要初始化的对象相同的转换单元中定义的任何函数或对象之前进行初始化。 [关于副作用的脚注] [示例如下]

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

然后P0250R3对语言进行了更改,并于2017年3月发布并转录为标准草案,正好将其转换为C ++ 17。 P0250R3添加了非初始化odr-use 的定义,并修改了该子句以引用该定义,同时还以线程感知术语(之前强烈发生在之前),并添加了有关避免死锁的说明。

从那时起,有关避免死锁的注释已被修订为推荐做法

措辞变化的动机

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

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

还有:

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

示例的正确性。

我认为该示例始终都是错误的。

在C ++ 98中,该示例是错误的,因为该版本标准中的规范性措词导致了不完整。假设我们扩大了示例,以在与B::B的定义相同的TU中定义构造函数a

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

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

P0250R3中的措词更改(将 odr-use 更改为非初始化odr-use )破坏了这种循环性,但代价是使示例变得毫无意义。但是后来总被打破了。这就是SIOF,可以通过“首次使用时构造”惯用语或使用辅助对象(例如ios_base::Init)来避免。

实施实践

我将示例(具有循环性)编译为一个(Linux,ELF; CentOS 7.8)共享对象,并在使用B::B输入main之后将其加载到程序中。恰好dlopena中的一个是在未初始化状态下使用的,其中一个取决于链接顺序。

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