单例模式是一种常见的软件设计模式,用于确保一个类只有一个实例,并提供全局访问点。在C++中实现线程安全的单例模式有多种方法,下面介绍几种常用且易于理解的实现方式。
1. 经典Meyers单例(C++11及以后)
C++11引入了对局部静态变量初始化的线程安全保证。最简单、最安全的单例实现就是使用局部静态对象:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 线程安全初始化
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点:
- 代码简洁,易于维护。
- 线程安全且延迟初始化。
缺点:
- 不能在C++11之前的编译器上使用。
- 如果需要在程序结束前手动销毁实例,默认实现不支持。
2. 带锁的双重检查锁定(Double-Checked Locking)
如果你在旧编译器(C++03)环境中,需要手动实现线程安全的单例,可以使用双重检查锁定结合互斥量:
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
if (!ptr_) { // 第一检查
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) { // 第二检查
ptr_ = new Singleton();
}
}
return ptr_;
}
~Singleton() {
delete ptr_;
}
private:
Singleton() = default;
static Singleton* ptr_;
static std::mutex mutex_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;
优点:
- 支持在C++03中使用。
- 只在第一次实例化时加锁,后续访问更快。
缺点:
- 需要手动管理内存,容易出现泄漏或双删。
- 代码相对复杂。
3. 使用std::call_once(C++11)
std::call_once是C++11提供的一种一次性调用机制,可保证某个函数仅执行一次,常用于单例初始化:
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){ ptr_ = new Singleton(); });
return *ptr_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* ptr_;
static std::once_flag initFlag_;
};
Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::initFlag_;
优点:
- 线程安全,代码相对简洁。
- 只在第一次调用时执行初始化。
缺点:
- 仍需要手动管理内存(可改为智能指针)。
4. 智能指针与std::shared_ptr
为了避免手动内存管理,可以结合std::shared_ptr和std::call_once:
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag_, [](){ ptr_ = std::make_shared <Singleton>(); });
return ptr_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::shared_ptr <Singleton> ptr_;
static std::once_flag initFlag_;
};
std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::initFlag_;
优点:
- 自动管理生命周期,避免泄漏。
- 线程安全且延迟初始化。
5. 静态局部对象+C++14[[nodiscard]]
C++14提供[[nodiscard]]属性,可强制编译器警告如果忽略返回值,确保单例被正确使用:
class Singleton {
public:
[[nodiscard]] static Singleton& instance() {
static Singleton instance; // 线程安全
return instance;
}
// ...
};
6. 使用模块化编译(C++20)
C++20引入模块化,可将单例定义在模块中,进一步提高编译速度和安全性。示例略。
何时选择哪种实现?
| 实现方式 | 适用场景 | 优缺点 |
|---|---|---|
| Meyers单例(局部静态) | C++11+ | 简洁、线程安全 |
| 双重检查锁定 | C++03 | 兼容旧编译器,易出错 |
std::call_once |
C++11+ | 线程安全,易于理解 |
std::shared_ptr+call_once |
需要自动销毁 | 资源管理安全 |
[[nodiscard]] |
防止误用 | 语义明确 |
小结
在现代C++中,最推荐使用Meyers单例或std::call_once配合std::shared_ptr的实现。它们既简洁又可靠,充分利用了语言的线程安全特性。若你必须在旧编译器环境中工作,则双重检查锁定是可行但需谨慎的备选方案。通过合适的单例实现,你可以让代码既安全又易于维护。