下面的代码重现了我遇到的问题,MSVC 2022:
#include <iostream>
struct A {
static void message()
{
std::cout << "A::message()\n";
}
struct B {
B() {}
~B()
{
A::message();
}
};
static inline B b;
};
int main()
{
}
通过上面的代码,我看到如预期的那样出现了消息。然而:
- 如果我删除 constructor
B()
,那么析构函数~B()
似乎被删除,因为消息消失了。 - 如果我随后
int dummy;
向结构添加一个虚拟属性B
,则该消息会再次出现。
我确实找不到答案的问题:
- 在我看来,如果没有构造函数
B()
,也没有属性B
,编译器只是将类标记B
为空,然后完全跳过它。它是否正确? - 是否有记录表明这种情况可能发生?鉴于析构函数的内容,我认为这永远不会发生。
- 我怎样才能保证它始终正常工作,而不仅仅是我现在使用的编译器。定义构造函数是否
B()
足够,或者我还应该使用B
虚拟属性使其非空int dummy;
?
区别在于是否
b
有静态初始化。如果静态存储持续时间变量具有静态初始化,则它在任何其他没有静态初始化(即动态初始化)的静态存储持续时间变量之前被初始化。
特别是, 中定义了另一个重要的静态存储持续时间对象
<iostream>
,即 的实例std::ios_base::Init
。该类型对象的初始化会导致标准 IO 流的初始化(例如std::cout
)。当它的最后一个实例被销毁时,它还负责刷新所有这些流。因此,为了查看您的输出,您必须确保该
std::ios_base::Init
对象在您的b
. 然而,销毁的顺序与初始化的顺序相反。因此,如果b
具有静态初始化但std::ios_base::Init
对象具有动态初始化,那么这是错误的方法。如果b
没有静态初始化,则仅当b
的定义存在于包含(直接或间接) 的每个<iostream>
翻译单元中(在第一次包含之后)时,它将被正确排序。这是因为您使用了,这使得仅inline
动态初始化部分排序的。b
一般来说,如果静态存储持续时间变量的初始化是常量表达式,则该变量具有静态初始化。如果您没有声明任何构造函数,就会出现这种情况。
如果初始化不是常量表达式,那么它通常具有动态初始化,但只要在初始化后不会更改静态存储持续时间变量的结果值,实现就可以自由选择静态初始化来代替任何这样的变量。如果您使用非
constexpr
构造函数或未初始化成员,则会遇到这种情况。因此,为了可靠地获得正确的顺序,您需要确保
b
没有静态初始化。由于实施上有余地,这有点棘手。在 的构造函数中做一些B
显然只能在运行时发生的事情应该足够了(例如调用一些 IO),但标准当前指定的方式很难完全确定,因为没有考虑副作用的差异。请参阅开放的CWG 问题 1294。更好的选择可能是将一个
std::ios_base::Init
对象添加为 的成员B
,以便在b
的析构函数运行时确保一个这样的对象保持活动状态。另外,正如您在上面看到的,C++ 中静态存储持续时间变量的构造和销毁非常复杂。
b
如果可以的话,在 中简单地声明为非static
局部变量会更直接main
。通常无法保证static inline
变量会在第一次非初始化 odr 使用之前被构造main
(这是实现定义的)。请注意,我没有验证这实际上是 MSVC 的原因。答案基于对标准的理论考虑以及实施可以做什么。
另外,我使用我对[basic.start.term]/3的解释,我认为这意味着静态初始化变量的销毁顺序是根据动态初始化变量的实际初始化顺序来排序的。我在措辞中看到的另一种解释是,销毁的顺序就像所有变量都根据动态初始化的反向初始化顺序规则进行了动态初始化一样。不幸的是,我觉得文本并不清楚,至少 GCC 和 Clang 似乎遵循后一种解释。在那种情况下,你不会看到你的问题。