在现代 C++(尤其是 C++17 之后)中,实现线程安全且惰性初始化的单例模式已经不再需要复杂的锁机制。标准库提供的 std::call_once、std::once_flag 以及 constexpr 初始化器,结合 std::unique_ptr 或 std::shared_ptr,可以让代码既简洁又高效。下面将从几个关键点展开说明,并给出完整可编译的示例。
1. 单例的核心需求
- 全局唯一实例:保证同一进程内只能有一个对象实例。
- 懒加载:第一次访问时才创建实例,避免不必要的资源占用。
- 线程安全:多线程环境下,实例化过程不会出现竞态条件。
- 易于使用:调用者不需要关心底层实现细节。
2. C++20 里最简洁的实现方式
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
class Singleton
{
public:
// 禁止复制构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 提供全局访问入口
static Singleton& instance()
{
// std::call_once 保证只有一个线程会执行初始化代码
std::call_once(initFlag_, [] {
// 这里使用 make_unique,构造函数默认调用
instance_.reset(new Singleton);
});
return *instance_;
}
void sayHello() const
{
std::cout << "Hello from Singleton! Thread ID: " << std::this_thread::get_id() << '\n';
}
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destroyed\n"; }
static std::once_flag initFlag_;
static std::unique_ptr <Singleton> instance_;
};
// 静态成员定义
std::once_flag Singleton::initFlag_;
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
关键点说明
-
std::once_flag+std::call_once
这对组合是实现单例懒加载的最标准方式。call_once会在多线程环境下确保内部 lambda 只被执行一次,无论多少线程同时调用。 -
std::unique_ptr
用于管理单例对象的生命周期,保证在程序结束时自动销毁。相比裸指针,避免了内存泄漏。 -
删除拷贝构造与赋值
防止外部误用导致多实例。 -
线程 ID 输出
在sayHello中打印线程 ID,便于验证多线程访问的安全性。
3. 如何使用
void worker()
{
Singleton::instance().sayHello();
}
int main()
{
std::thread t1(worker);
std::thread t2(worker);
std::thread t3(worker);
t1.join(); t2.join(); t3.join();
// 程序结束时,单例会自动析构
return 0;
}
运行结果类似:
Singleton constructed
Hello from Singleton! Thread ID: 140353219892288
Hello from Singleton! Thread ID: 140353211499584
Hello from Singleton! Thread ID: 140353203106880
Singleton destroyed
可以看到,构造函数只被调用一次,且所有线程都共享同一个实例。
4. 常见误区与注意事项
| 误区 | 正确做法 |
|---|---|
| 直接使用静态局部变量实现单例 | 仍然可行,但 call_once 更显式,易于阅读。 |
在 main 里手动销毁单例 |
让 unique_ptr 自动析构,避免手动调用可能导致顺序错误。 |
| 忽略拷贝/移动构造 | 通过 delete 明确禁止,以防止意外拷贝。 |
5. 小结
在 C++20 及以上版本中,借助 std::call_once、std::once_flag 与智能指针,可以用极简的代码实现线程安全、懒加载的单例模式。与传统的 static 局部变量或双重检查锁模式相比,现代实现更易读、易维护,且不需要手动处理锁或计数器。只需几行代码即可满足大多数项目的需求。