Meyers单例通过函数内static局部对象实现线程安全、懒加载与自动析构,C++11起编译器保证其首次初始化原子性,无需手动加锁或call_once。

为什么直接用静态局部变量就能线程安全
因为 C++11 标准明确规定:static 局部变量的首次初始化是原子的、线程安全的——编译器会自动生成带锁或类似 pthread_once 的机制,你不用手写双重检查锁(DCLP),也无需依赖 std::call_once。
常见错误现象:自己实现 DCLP 时忘记加 volatile 或内存序,导致多线程下构造出多个实例,或者读到未完全构造的对象。
- 必须使用 C++11 或更高标准(
-std=c++11及以上) - 不能在函数外定义静态指针再手动 new(那又回到老式不安全单例)
- 析构时机由静态存储期决定:首次调用时构造,程序退出时自动销毁
怎么写一个真正靠谱的 Meyers 单例
核心就一行:在函数里声明 static 局部对象。它天然满足懒加载、线程安全、自动释放三要素。
class Logger {
public:
static Logger& instance() {
static Logger inst; // ✅ 这行就是全部
return inst;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() = default; // 构造函数私有
};
-
inst是对象本身,不是指针——避免空指针、内存泄漏和手动delete - 类必须禁用拷贝(
= delete),否则用户可能意外复制出第二个“单例” - 如果构造函数需要参数(比如配置路径),就不能用默认构造;此时需改用带参数的静态局部变量 + 函数重载或传参封装(见下一条)
构造函数要参数怎么办:用 std::call_once + 静态指针(慎用)
一旦构造逻辑依赖运行时参数(如 config_path),纯 Meyers 模式失效——因为 static Logger inst 无法接收参数。这时得退一步,但别退回原始 DCLP。
立即学习“C++免费学习笔记(深入)”;
- 优先考虑把参数提取成全局配置,让
Logger构造函数从那里读,保持 Meyers 形式 - 实在不行,用
static std::unique_ptr<Logger>+std::call_once,确保只初始化一次 - 绝对不要用裸指针配
new和手动delete——容易忘删、异常安全差 - 注意:
std::call_once本身线程安全,但你要保证传入的 lambda 不抛异常,否则后续调用会卡死(std::call_once对已触发异常的 once_flag 行为未定义)
容易被忽略的析构顺序问题
静态局部对象的析构顺序与构造顺序相反,且跨编译单元时顺序未定义。如果你的单例被其他静态对象(比如全局 std::ofstream)在析构时调用,就可能访问已销毁的单例。
- 典型报错:
Segmentation fault (core dumped)发生在 main 返回后,堆栈里出现对已析构对象的虚函数调用 - 解决方案:避免在其他静态对象的析构函数中访问单例;或改用“Phoenix Singleton”模式(销毁后允许重建),但代价是逻辑变复杂
- 更务实的做法:确认该单例是否真需要析构逻辑;多数日志、配置类只需构造期初始化,析构期什么都不做反而更安全
真正麻烦的从来不是怎么写出来,而是谁在什么时候、以什么顺序去访问它。








