C++ 静态初始化顺序

最后更新: 2023-12-26 14:30:46 +0800 +0800
原文以英文撰写。您正在阅读AI翻译版本。

C++ 独立于作用域分配存储期。静态存储期和线程存储期的初始化与销毁规则,决定了对象是在 main 之前可用、首次使用时可用,还是每个线程各自初始化一次。

存储期与初始化阶段 #

命名空间作用域对象、静态数据成员以及块作用域的 static 对象具有静态存储期。thread_local 对象具有线程存储期:每个线程都有独立的实例,并根据声明和实现规则,在该线程启动时或首次使用时初始化。

当静态存储期或线程存储期变量满足条件时,会进行常量初始化;否则,该变量会进行零初始化。常量初始化和零初始化合称静态初始化,并且会在动态初始化之前完成。因此,constinit 变量必须进行常量初始化,而 constexpr 对象是明确表达初始化要求的一种有效方式。

cpp
constinit int process_limit = 256;
constexpr int cache_line_bytes = 64;

constinit 不会使对象变为不可变;它只会拒绝需要动态初始化的初始化器。

顺序边界 #

同一翻译单元中普通的非内联命名空间作用域变量,其动态初始化按照定义顺序进行。跨翻译单元的动态初始化没有可靠的源代码顺序:它可能以不确定顺序进行,或者受到延迟初始化规则的影响。因此,读取另一个非局部对象的非局部初始化器,可能只能看到零初始化状态,而不是该对象预期的动态状态。

函数局部静态对象会将构造延迟到控制流首次经过其声明时:

cpp
Logger& logger() {
    static Logger instance;
    return instance;
}

从 C++11 开始,对块作用域静态对象的并发初始化尝试只会成功初始化一次;其他线程会等待初始化成功。如果初始化器抛出异常,则会在后续进入时重试初始化。首次使用时构造消除了 instance 的跨翻译单元构造依赖,但调用方仍必须制定更广泛的关闭策略。

销毁是独立的设计问题 #

静态对象和线程对象分别在程序终止或线程退出时销毁。不能将跨翻译单元的非局部对象销毁顺序作为安全的依赖机制,因为析构函数可能访问已经销毁的对象。函数局部静态对象按照构造完成顺序的逆序销毁;当其析构函数调用其他访问器时,也可能产生类似的风险。

对于在清理阶段相互作用的资源,应优先采用显式所有权和关闭顺序。对于进程生命周期内存在的服务,如果选择由操作系统回收资源的契约,可以有意不销毁这些服务;应记录这一选择,而不要依赖偶然的销毁顺序。

评论

在 GitHub 上阅读或评论(发表评论需要登录)