std::call_once和std::once_flag是最稳妥的线程安全单次初始化方案,需静态存储期、禁止抛异常、避免递归调用;std::shared_ptr+自定义deleter适合多持有懒加载;std::optional仅适用于单线程栈上轻量延迟构造。

std::call_once 和 std::once_flag 是最稳妥的选择
多线程环境下,手动检查 + 双重检查锁(DCLP)极易出错,std::call_once 由标准库保证“只执行一次”且内存序安全,不用自己写 std::atomic 或 std::mutex 配合判断逻辑。
常见错误是把 std::once_flag 声明为局部静态变量却误以为它自动初始化 —— 实际上必须显式定义(不能只声明),否则链接失败或未定义行为。
-
std::once_flag必须是静态存储期(全局、类静态成员、局部静态变量),不能是栈上临时对象 - 回调函数不能抛异常;若抛了,
std::call_once行为未定义(多数实现会终止程序) - 初始化函数里避免调用可能再次触发同个
std::call_once的代码,否则死锁
class Config {
static std::once_flag init_flag;
static std::unique_ptr<Config> instance_;
static void init() {
instance_ = std::make_unique<Config>();
// 不要在这里 throw
}
public:
static Config& get() {
std::call_once(init_flag, init);
return *instance_;
}
};std::shared_ptr + 自定义 deleter 实现线程安全的懒加载单例
如果需要延迟创建 + 自动释放 + 多处持有,std::shared_ptr 比裸指针 + 手动 delete 更可靠。关键是用自定义 deleter 把销毁逻辑和初始化解耦,避免析构时又触发初始化。
容易踩的坑是把 std::shared_ptr 存在非静态位置(比如普通成员变量),导致每次调用都新建一个实例 —— 懒加载失效。
立即学习“C++免费学习笔记(深入)”;
- 必须用静态
std::shared_ptr存储实例,否则无法跨调用共享 - deleter 里不要访问该
std::shared_ptr本身(循环引用或提前释放) - 初始化函数返回
std::shared_ptr时,确保构造过程不抛异常,否则std::shared_ptr构造失败,资源泄漏
static std::shared_ptr<Database> get_db() {
static std::shared_ptr<Database> instance;
static std::once_flag flag;
std::call_once(flag, []{
instance = std::shared_ptr<Database>(
new Database("config.json"),
[](Database* p) { delete p; } // 自定义 deleter
);
});
return instance;
}std::optional 适合栈上延迟构造,但不适用于大对象或需多线程保护的场景
std::optional 在 C++17 后支持就地构造,能避免堆分配,对轻量级类型(如 int、小结构体)很合适。但它不提供线程同步机制,多个线程同时调用 emplace() 会引发数据竞争。
典型误用是把它当单例容器:声明为局部静态 std::optional,却没加锁就直接 emplace() —— 这不是懒加载,是竞态加载。
- 仅用于单线程环境,或明确由外部同步(比如整个函数已加锁)
-
std::optional的has_value()判断开销极小,但emplace()可能触发移动/拷贝,注意 T 的构造成本 - 不能用于需要动态生命周期管理的对象(比如依赖全局状态的资源句柄)
int get_default_timeout() {
static std::optional<int> value;
if (!value.has_value()) {
value.emplace(read_from_env("TIMEOUT") ?: 30);
}
return *value;
}宏 + 函数指针模拟“惰性函数调用”,慎用于复杂初始化
有些老项目用宏包装函数指针,在首次调用时跳转到初始化函数,之后再跳转到实际逻辑。这种方式零运行时开销,但破坏调试体验、无法捕获异常、难以测试。
问题常出现在宏展开后符号名冲突,或初始化函数返回值类型与后续调用不一致,编译器不报错但运行时崩溃。
- 宏参数必须加括号包裹,防止运算符优先级问题(如
MACRO(x + y)) - 函数指针类型必须严格匹配,C++ 中建议用
using InitFunc = void(*)();显式声明 - 初始化函数不能依赖尚未完成初始化的其他模块(比如初始化 A 时调用了 B::get(),而 B 尚未初始化)
这种方案现在基本被 std::call_once 取代,除非在嵌入式等极端性能敏感且无标准库的环境。










