在现代C++中,实现一个线程安全且懒加载(即仅在第一次使用时才创建实例)的单例模式最推荐的方法是利用函数静态局部变量的特性。自C++11起,编译器保证对函数内部静态变量的初始化是线程安全的。下面给出完整示例,并对比几种常见实现方式,帮助你了解它们的优缺点。
1. 基础懒加载单例(线程安全)
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 起保证线程安全
return instance;
}
void doSomething() { std::cout << "Hello from Singleton\n"; }
private:
Singleton() { std::cout << "Constructing Singleton\n"; }
~Singleton() { std::cout << "Destructing Singleton\n"; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
使用方式:
int main() {
Singleton::instance().doSomething();
Singleton::instance().doSomething(); // 只创建一次
}
为什么安全?
函数内部的静态对象在第一次调用时才会被构造。C++11 标准规定,对同一静态对象的并发访问会自动加锁,确保只有一个线程能够完成初始化,其他线程会等待,随后直接获得已经初始化好的对象。
2. 传统 std::call_once 实现
如果你想在更旧的编译器或更细粒度地控制初始化顺序,可以使用 std::call_once:
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){ instancePtr.reset(new Singleton); });
return *instancePtr;
}
void doSomething() { std::cout << "Hello from Singleton\n"; }
private:
Singleton() { std::cout << "Constructing Singleton\n"; }
~Singleton() { std::cout << "Destructing Singleton\n"; }
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 = nullptr;
std::once_flag Singleton::initFlag;
此实现与上面完全等价,但在某些极端场景下可以更清晰地表达“只初始化一次”。
3. 双重检查锁(不推荐)
class Singleton {
public:
static Singleton* instance() {
if (!ptr) { // 第一层检查
std::lock_guard<std::mutex> lock(mtx);
if (!ptr) { // 第二层检查
ptr = new Singleton();
}
}
return ptr;
}
private:
Singleton() {}
static Singleton* ptr;
static std::mutex mtx;
};
虽然逻辑上正确,但在某些编译器和平台上会因为内存模型导致指针可见性问题。现代 C++ 的静态局部变量或 std::call_once 已经解决了这个问题,双重检查锁已经不再推荐。
4. 用 std::shared_ptr 进行懒加载
如果你希望单例在程序结束时自动销毁,而不受函数返回顺序影响,可以使用 std::shared_ptr:
static std::shared_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag, [](){
instancePtr = std::make_shared <Singleton>();
});
return instancePtr;
}
std::shared_ptr 还能让你在需要时获得引用计数,避免手动管理对象生命周期。
5. 单例的常见误区
| 误区 | 正确做法 |
|---|---|
用 new 并手动 delete |
使用局部静态对象或智能指针,避免手动销毁 |
| 只关心线程安全,忽略销毁顺序 | 静态局部变量在程序结束时按逆序销毁,保证资源释放 |
认为 Meyers Singleton(函数静态)不安全 |
C++11 之后已保证线程安全 |
| 通过宏或全局变量实现 | 宏会导致名字冲突,建议使用类封装 |
6. 小结
- 推荐:使用函数内部静态局部变量(Meyers Singleton),因为代码最简洁,且 C++11 起已保证线程安全。
- 备选:若需要更细粒度的控制或兼容老编译器,使用
std::call_once。 - 避免:双重检查锁,手动
new/delete,宏实现。
单例模式是 C++ 中经常被讨论的设计模式之一,但在实际项目中,建议先评估是否真的需要全局共享实例。若仅是想共享某个资源,考虑使用依赖注入或模块化设计,以保持代码的可测试性和可维护性。