关于延迟动态初始化的问题
The issue about the deferred dynamic initialization
考虑 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();
}
范例后面的评论是:
If, however, a is initialized at some point after the first statement of main, b will be initialized prior to its use in A::A.
我不明白为什么 b
保证在 A :: A 中使用之前初始化,而 a 在 main 的第一条语句之后的某个时间点初始化。按照规则说的:
It is implementation-defined whether the dynamic initialization of a non-local non-inline variable with static storage duration is sequenced before the first statement of main or is deferred. If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized.
A non-initialization odr-use is an odr-use ([basic.def.odr]) not caused directly or indirectly by the initialization of a non-local static or thread storage duration variable.
我能理解的是,当延迟初始化时,变量a
应该在变量a
的odr-use(非初始化odr-use)之前被初始化在标有#2
的地方。然而我无法理解的是,评论说 b 将在 A :: A 中使用之前进行初始化。 IIUC,函数 A::A
的调用是变量 a
初始化的一部分,因此变量 b
在 #1
的 ODR 使用不是非initialization odr-use 由于它是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的。我认为它只能说变量 b
保证在 #2
之前被初始化,为什么评论说 b 将在其在 A 中使用之前被 初始化 :: A?如何解读这个例子?
条款的演变
有问题的 (non-normative) 示例可以追溯到标准的 C++98 版本,但是托管子句中的(规范)语言在 C++17 中发生了变化。
C++98:
3.6.2 Initialization of non-local objects [basic.start.init]
3 - It is implementation-defined whether or not the dynamic initialization ([cross-references]) of an object of namespace scope is done before the first statement of main. If the initialization is deferred to some point in time after the first statement of main, it shall occur before the first use of any function or object defined in the same translation unit as the object to be initialized. [footnote regarding side-effects] [Example follows]
C++03 有相同的文本。 C++11 删除了 cross-references 并将“命名空间范围的对象”替换为“non-local 具有静态存储持续时间的变量”,“对象”替换为“变量”,“使用”替换为“odr-use”,但我认为该条款的含义没有改变。 C++14 没有改变。
该语言随后被 P0250R3 更改,于 2017 年 3 月发布并转录为标准草案,正好赶上将其纳入 C++17。 P0250R3 添加了 non-initialization odr-use 的定义并修改了子句以引用该定义,同时还用 threading-aware 术语表达了事件之间的关系( 顺序在之前,强烈发生在之前等等),并添加了关于避免死锁的注释。
从那时起,关于避免死锁的注释被修改为推荐做法。
措辞变化的动机
幸运的是,P0250R3 包含了对动机的讨论。在顺序程序的并行初始化一节中,我们读到:
Currently, we very explicitly allow static constructors to run after the start of main, whether or not other threads are started. This appears to be motivated by the intent to support e.g. lazily loading a dynamic library when a function symbols is referenced, as with RTLD_LAZY on Posix systems. Even if static namespace-scope constructors are run immediately in library loading, the library may be implicitly loaded after the start of main.
还有:
SG1 generally feels that static namespace-scope constructors should be avoided [...] we decided to restrict such constructors to existing threads, which appears to be consistent with known implementations.
例子的正确性。
我认为这个例子一直都是错误的。
在 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 更改为 non-initialization odr-use)这种循环,以让这个例子变得荒谬为代价。但后来总是坏掉。这是 SIOF, which can be avoided via the Construct on First Use idiom or via the use of helper objects such as ios_base::Init
.
实施实践
我将示例(带循环)编译成一个(Linux,ELF;CentOS 7.8)共享对象,在使用dlopen
进入main后加载到程序中。恰好 a
和 b
之一处于未初始化状态 odr-used,这取决于 link 排序。
这表明 non-initialization odr-use 的措辞变化反映了实施实践。不幸的是,该标准现在包含一个明显不正确的示例,但由于示例和注释是 non-normative,这是有问题的,但不是致命的。
考虑 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();
}
范例后面的评论是:
If, however, a is initialized at some point after the first statement of main, b will be initialized prior to its use in A::A.
我不明白为什么 b
保证在 A :: A 中使用之前初始化,而 a 在 main 的第一条语句之后的某个时间点初始化。按照规则说的:
It is implementation-defined whether the dynamic initialization of a non-local non-inline variable with static storage duration is sequenced before the first statement of main or is deferred. If it is deferred, it strongly happens before any non-initialization odr-use of any non-inline function or non-inline variable defined in the same translation unit as the variable to be initialized.
A non-initialization odr-use is an odr-use ([basic.def.odr]) not caused directly or indirectly by the initialization of a non-local static or thread storage duration variable.
我能理解的是,当延迟初始化时,变量a
应该在变量a
的odr-use(非初始化odr-use)之前被初始化在标有#2
的地方。然而我无法理解的是,评论说 b 将在 A :: A 中使用之前进行初始化。 IIUC,函数 A::A
的调用是变量 a
初始化的一部分,因此变量 b
在 #1
的 ODR 使用不是非initialization odr-use 由于它是由非本地静态或线程存储持续时间变量的初始化直接或间接引起的。我认为它只能说变量 b
保证在 #2
之前被初始化,为什么评论说 b 将在其在 A 中使用之前被 初始化 :: A?如何解读这个例子?
条款的演变
有问题的 (non-normative) 示例可以追溯到标准的 C++98 版本,但是托管子句中的(规范)语言在 C++17 中发生了变化。
C++98:
3.6.2 Initialization of non-local objects [basic.start.init]
3 - It is implementation-defined whether or not the dynamic initialization ([cross-references]) of an object of namespace scope is done before the first statement of main. If the initialization is deferred to some point in time after the first statement of main, it shall occur before the first use of any function or object defined in the same translation unit as the object to be initialized. [footnote regarding side-effects] [Example follows]
C++03 有相同的文本。 C++11 删除了 cross-references 并将“命名空间范围的对象”替换为“non-local 具有静态存储持续时间的变量”,“对象”替换为“变量”,“使用”替换为“odr-use”,但我认为该条款的含义没有改变。 C++14 没有改变。
该语言随后被 P0250R3 更改,于 2017 年 3 月发布并转录为标准草案,正好赶上将其纳入 C++17。 P0250R3 添加了 non-initialization odr-use 的定义并修改了子句以引用该定义,同时还用 threading-aware 术语表达了事件之间的关系( 顺序在之前,强烈发生在之前等等),并添加了关于避免死锁的注释。
从那时起,关于避免死锁的注释被修改为推荐做法。
措辞变化的动机
幸运的是,P0250R3 包含了对动机的讨论。在顺序程序的并行初始化一节中,我们读到:
Currently, we very explicitly allow static constructors to run after the start of main, whether or not other threads are started. This appears to be motivated by the intent to support e.g. lazily loading a dynamic library when a function symbols is referenced, as with RTLD_LAZY on Posix systems. Even if static namespace-scope constructors are run immediately in library loading, the library may be implicitly loaded after the start of main.
还有:
SG1 generally feels that static namespace-scope constructors should be avoided [...] we decided to restrict such constructors to existing threads, which appears to be consistent with known implementations.
例子的正确性。
我认为这个例子一直都是错误的。
在 C++98 中,该示例不正确,因为该版本标准中的规范性措辞导致循环。假设我们扩充示例以在与 a
:
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 更改为 non-initialization odr-use)这种循环,以让这个例子变得荒谬为代价。但后来总是坏掉。这是 SIOF, which can be avoided via the Construct on First Use idiom or via the use of helper objects such as ios_base::Init
.
实施实践
我将示例(带循环)编译成一个(Linux,ELF;CentOS 7.8)共享对象,在使用dlopen
进入main后加载到程序中。恰好 a
和 b
之一处于未初始化状态 odr-used,这取决于 link 排序。
这表明 non-initialization odr-use 的措辞变化反映了实施实践。不幸的是,该标准现在包含一个明显不正确的示例,但由于示例和注释是 non-normative,这是有问题的,但不是致命的。