如何避免静态存储期变量的初始化被延迟?
问题背景
我所使用的类的构造函数带有副作用,这些类的对象是具有静态存储期的全局对象。初始化过程中,这些对象会将所属类注册至一个特殊映射表,且这些注册操作必须早于该映射表被用于其他用途。
这些类及其全局对象定义在不同的编译单元中,根据非块变量的动态初始化规则,其初始化可能会被延迟到该编译单元中的其他函数或变量被使用时,甚至可能被无限期延迟。我需要一种避免这些全局对象初始化被延迟的方法,要求对象的构造函数要么在main函数执行前调用,要么在main函数启动后立即调用,且所有修改仅需在这些对象所在的编译单元内完成——新增包含此类全局对象的编译单元时,无需修改其他编译单元。
示例代码
struct Base { }; extern void regFactoryMethod(std::function<std::shared_ptr<Base>()>); struct Object : Base { struct Registrator { Registrator() { regFactoryMethod([](){ return std::make_shared<Object>(); }); } }; static Registrator registrator; }; Object::Registrator Object::registrator;
原本预期Object::Registrator::Registrator()会被自动调用,但这无法得到保证,因为该调用可能被延迟:
非块非内联静态存储期变量的动态初始化,是在main函数第一条语句之前执行还是被延迟,由实现定义。若被延迟,则其会在对同一编译单元中定义的任何非内联函数或非内联变量进行非初始化odr-use之前严格执行。此类延迟动态初始化发生的线程和程序中的具体点由实现定义。
可行解决方案
方法1:函数内静态对象 + 构造器属性(跨编译器推荐)
将全局静态对象改为函数内的静态局部对象,再通过编译器的构造器属性强制触发初始化,确保在main执行前完成注册。该方案仅需修改当前编译单元,无需改动其他文件:
struct Object : Base { struct Registrator { Registrator() { regFactoryMethod([](){ return std::make_shared<Object>(); }); } }; // 用函数内静态对象替代全局静态对象 static Registrator& getRegistrator() { static Registrator instance; return instance; } }; // 触发初始化的函数,标记为构造器(GCC/Clang用[[gnu::constructor]],MSVC用[[msvc::constructor]]) [[gnu::constructor]] void initObjectRegistrator() { Object::getRegistrator(); }
编译器会在main执行前调用initObjectRegistrator,进而触发getRegistrator内的静态对象初始化,保证注册操作执行。新增编译单元时,只需复制此模式即可。
方法2:Nifty Counter 技巧(标准C++兼容)
该技巧通过全局计数器保证所有编译单元的注册对象在main前完成初始化,仅需一个公共头文件框架,新增编译单元时只需引入头文件并定义注册类:
- 公共头文件(
registration.h):
struct Base { }; extern void regFactoryMethod(std::function<std::shared_ptr<Base>()>); // Nifty Counter 核心框架 class RegistrationCounter { public: RegistrationCounter(); ~RegistrationCounter(); static inline int counter = 0; }; // 简化注册的宏 #define REGISTER_CLASS(Class) \ struct Class##Registrator { \ Class##Registrator() { \ regFactoryMethod([](){ return std::make_shared<Class>(); }); \ } \ }; \ static RegistrationCounter reg_counter_##Class; \ static Class##Registrator reg_instance_##Class;
- 实现文件(
registration.cpp):
#include "registration.h" RegistrationCounter::RegistrationCounter() { // 第一个计数器初始化时可做全局映射表的准备工作 ++counter; } RegistrationCounter::~RegistrationCounter() { // 最后一个计数器销毁时可做全局映射表的清理工作 --counter; }
- 目标编译单元中使用:
#include "registration.h" struct Object : Base { // 类定义 }; REGISTER_CLASS(Object)
同一编译单元内的静态变量按定义顺序初始化,reg_counter_##Class会先于reg_instance_##Class初始化,确保注册操作在main前执行。
方法3:MSVC 特定初始化段
针对MSVC编译器,可使用#pragma init_seg将全局静态对象指定到lib初始化段,该段的初始化会在main前完成:
#pragma init_seg(lib) Object::Registrator Object::registrator;
仅需在当前编译单元的全局对象定义前添加该指令,无需修改其他文件,但仅适用于MSVC环境。
注意事项
- 多线程环境下需确保
regFactoryMethod是线程安全的,main前的初始化通常为单线程,但部分编译器可能支持多线程初始化,需额外处理。 - 构造器属性的语法因编译器而异:GCC/Clang用
[[gnu::constructor]],MSVC在C++20后支持[[msvc::constructor]],旧版本可使用__declspec(allocate(".CRT$XCU"))。
内容的提问来源于stack exchange,提问作者Dmitry Kuzminov

