考虑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。
我不明白为什么b
在A中使用之前被保证初始化::当在main的第一条语句之后的某个时刻初始化时的。根据规则:
基本启动动态#4
实现定义了具有静态存储持续时间的非本地非内联变量的动态初始化是在main的第一条语句之前排序还是推迟如果延迟,则强烈发生在任何非初始化或使用与要初始化的变量在同一转换单元中定义的任何非内联函数或非内联变量之前
basic.start.dynamic#3
非初始化odr使用是不是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的odr使用([basic.def.odr])。
我所能理解的是,当初始化被推迟时,变量a
应该在变量a
的odr使用(非初始化odr使用)之前初始化,该变量位于用#2
标记的位置。然而,我不能理解的是,评论说b在A中使用之前将被初始化::A。IIUC,函数A::A
的调用是变量a
初始化的一部分,因此在#1
对变量b
的odr使用不是非初始化odr使用,因为它是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的。我认为它只能说变量b
保证在#2
之前初始化,为什么注释说b在A中使用之前将被初始化::A?如何解释这个例子?
子句的演变
有问题的(非规范性)例子可以追溯到标准的C++98版本,但宿主条款中的(规范性)语言在C++17中发生了变化。
C++98:
3.6.2初始化非本地对象〔basic.start.init〕
3-它是实现定义的,是否在main的第一个语句之前完成命名空间范围的对象的动态初始化([交叉引用])。如果初始化被推迟到main的第一个语句之后的某个时间点,则应在首次使用与要初始化的对象在同一转换单元中定义的任何函数或对象之前进行[关于副作用的脚注][示例如下]
C++03具有相同的文本。C++11删除交叉引用;命名空间范围的对象";用";"具有静态存储持续时间的非局部变量"对象";用";变量";,以及";使用";用";odr使用";,但我认为该条款的含义没有改变。C++14没有改变。
P0250R3随后对该语言进行了更改,并于2017年3月发布并转录为标准草案,正好使其成为C++17。P0250R3添加了非初始化odr使用的定义,并修改了子句以引用该定义,同时还用线程感知术语表达了事件之间的关系(在之前排序,强烈发生在之前等等),并添加了关于避免死锁的注释。
此后,关于避免僵局的说明被修改为建议做法。
措辞变化的动机
幸运的是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::A
的调用,而b
的初始化需要调用B::B
。所以我们有一个循环回归。
P0250R3中的措辞更改(将odr use更改为非初始化odr use[/em>)打破了这种循环性,代价是使示例变得毫无意义。但后来它总是坏掉了。这就是SIOF,可以通过"第一次使用时构造"习惯用法或通过使用辅助对象(如ios_base::Init
)来避免。
实施实践
我将这个示例(带有循环性)编译成一个(Linux,ELF;CentOS 7.8)共享对象,在使用dlopen
输入main后加载到程序中。a
和b
中恰好有一个是在未初始化状态下使用的,这一状态取决于链路排序。
这表明,非初始化odr使用的措辞变化反映了实现实践。不幸的是,该标准现在包含了一个明显不正确的示例,但由于示例和注释是非规范性的,这是有问题的,但不是致命的。