在 C++20 之前,单例模式的线程安全实现常常需要手动加锁或使用双重检查锁(double‑checked locking)。C++11 起引入了 std::call_once 和 std::once_flag,以及对函数静态变量的线程安全初始化,极大简化了实现。
下面演示两种推荐方案,说明它们的原理、优点和适用场景。
1. 使用 std::call_once
#include <iostream>
#include <mutex>
#include <memory>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() {
instancePtr.reset(new Singleton());
});
return *instancePtr;
}
void sayHello() const { std::cout << "Hello from Singleton!\n"; }
private:
Singleton() { std::cout << "Singleton ctor\n"; }
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instancePtr;
std::once_flag Singleton::initFlag;
原理
std::call_once接收一个std::once_flag和一个可调用对象(这里是 lambda)。- 第一次调用
instance()时,call_once会执行 lambda,并将once_flag标记为已初始化。 - 后续调用
call_once时会直接跳过 lambda,避免重复初始化。
优点
- 显式控制:你可以在需要的地方决定初始化时机。
- 延迟初始化:只有第一次访问
instance()时才创建对象,省去无用构造。
缺点
- 多次调用:如果
instance()被多次并发调用,仍会多次检查once_flag,虽然效率高,但略显冗余。
2. 直接使用函数内部静态变量
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
void sayHello() const { std::cout << "Hello from Singleton!\n"; }
private:
Singleton() { std::cout << "Singleton ctor\n"; }
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
原理
- C++11 起,函数内部的
static变量在第一次执行该函数时初始化,并且该初始化是 线程安全 的。 - 后续调用直接使用已存在的实例。
优点
- 代码最简洁:不需要额外的
once_flag或指针。 - 天然延迟:实例只在第一次调用时创建。
- 编译器优化:编译器可以将
static变量的访问视为只读,进一步提升性能。
缺点
- 不可延迟销毁:实例会在程序终止时按逆序销毁,如果你需要在程序结束前手动销毁,需额外实现。
- 构造错误:如果构造函数抛异常,后续调用仍会尝试重新初始化。
3. 何时选择哪种方式?
| 场景 | 推荐方案 |
|---|---|
| 需要在单例内部做复杂的资源初始化或异常处理 | std::call_once + unique_ptr |
| 只需要最简单的懒加载,且构造不抛异常 | 静态局部变量 |
| 需要自定义销毁顺序 | std::call_once + 自定义 destroy() |
4. 线程安全与 std::atomic 的误区
有人尝试使用 std::atomic<Singleton*> 作为单例指针,并用 load / store 进行原子访问。虽然技术上可行,但在实际使用中会出现未初始化指针读取的问题,除非再配合 std::call_once 或 std::mutex,否则难以保证安全。
小结
C++20 及其之前版本都已提供完备的工具来实现线程安全的懒加载单例。最简洁的方案是使用函数内部的静态变量,而如果需要更细粒度的控制,std::call_once则是最安全、最易维护的选择。无论哪种实现,都建议配合delete关键字禁用拷贝构造和赋值,以确保单例唯一性。