Singleton - 单例模式¶
cf 命名空间提供了两种单例实现:Singleton 和 SimpleSingleton。两者的区别在于初始化方式——Singleton 需要显式调用 init() 并支持参数化构造,而 SimpleSingleton 利用 Meyer's Singleton 模式自动初始化,要求目标类有默认构造函数。
为什么需要两种单例¶
单例模式看似简单,实际上有几个关键的设计选择:
- 何时初始化:启动时显式初始化 vs 首次访问时自动初始化
- 如何初始化:需要传参数 vs 默认构造就行
- 能否重置:测试时需要重置单例状态
Singleton 适用于需要参数化构造和精确控制初始化时机的场景。SimpleSingleton 适用于构造不需要参数、希望自动初始化的场景。
SimpleSingleton - 简单单例¶
最简实现,利用 C++11 标准保证的函数局部静态变量线程安全初始化。
基本用法¶
#include "base/singleton/simple_singleton.hpp"
class Logger {
public:
void log(const std::string& msg) { /* ... */ }
};
using LoggerSingleton = cf::SimpleSingleton<Logger>;
// 在任何地方
LoggerSingleton::instance().log("Hello");
不需要手动初始化,第一次调用 instance() 时自动构造。后续调用返回同一个实例。
继承使用¶
更常见的用法是让类自身继承 SimpleSingleton:
class WindowManager : public cf::SimpleSingleton<WindowManager> {
public:
void create_window(const std::string& title) { /* ... */ }
private:
WindowManager() = default; // 构造函数保持 private
friend class SimpleSingleton<WindowManager>;
};
// 使用
auto& mgr = WindowManager::instance();
mgr.create_window("Main");
注意把构造函数设为 private 或 protected,并声明 SimpleSingleton 为友元。
特点¶
- 线程安全:C++11 标准保证函数局部静态变量的初始化是线程安全的。
- 零开销:没有互斥锁、没有
std::call_once,只有一个静态局部变量。 - 不可重置:实例一旦创建就存在到程序结束,没有
reset()方法。 - 要求默认构造:目标类必须有可访问的默认构造函数。
Singleton - 显式初始化单例¶
需要调用 init() 来创建实例,支持传参构造。使用 std::call_once 保证线程安全。
基本用法¶
#include "base/singleton/singleton.hpp"
class Database {
public:
Database(const std::string& conn_str, int pool_size)
: conn_str_(conn_str), pool_size_(pool_size) {}
void query(const std::string& sql) { /* ... */ }
private:
std::string conn_str_;
int pool_size_;
};
using DBSingleton = cf::Singleton<Database>;
// 初始化(通常在 main 函数或启动流程中)
DBSingleton::init("host=localhost port=5432", 10);
// 使用
DBSingleton::instance().query("SELECT * FROM users");
初始化时机控制¶
Singleton 的一个重要特性是初始化时机完全可控:
// main.cpp
int main() {
// 阶段 1:初始化基础设施
cf::Singleton<Config>::init("config.json");
cf::Singleton<Logger>::init(cf::Singleton<Config>::instance().log_path());
// 阶段 2:初始化业务模块
cf::Singleton<Database>::init(
cf::Singleton<Config>::instance().db_connection_string(),
cf::Singleton<Config>::instance().db_pool_size()
);
// 阶段 3:运行
run_app();
}
如果调用 instance() 之前没有调用 init(),会抛出 std::logic_error:
// 忘记初始化
try {
DBSingleton::instance().query("SELECT 1");
} catch (const std::logic_error& e) {
// "Singleton not initialized. Call init() first."
}
重置单例¶
Singleton 提供 reset() 方法,主要用于测试:
// 测试中重置单例状态
void test_something() {
cf::Singleton<Config>::init("test_config.json");
// 运行测试...
cf::Singleton<Config>::reset();
// 下次使用前需要重新 init
cf::Singleton<Config>::init("production_config.json");
}
reset() 之后必须重新调用 init() 才能使用 instance()。
重复初始化¶
多次调用 init() 是安全的——只有第一次调用生效,后续调用被 std::call_once 忽略:
cf::Singleton<Config>::init("config1.json"); // 生效
cf::Singleton<Config>::init("config2.json"); // 被忽略,仍使用 config1
两种单例对比¶
| 特性 | SimpleSingleton | Singleton |
|---|---|---|
| 初始化方式 | 自动(首次访问) | 显式调用 init() |
| 构造参数 | 不支持 | 支持 |
| 线程安全 | 是(C++11 保证) | 是(std::call_once) |
| 重置 | 不支持 | 支持(reset()) |
| 未初始化访问 | N/A | 抛出 std::logic_error |
| 性能开销 | 极低(静态变量检查) | 稍高(指针判空 + call_once) |
常见陷阱¶
1. 析构顺序¶
单例的析构顺序和构造顺序相反。如果单例 A 的析构依赖单例 B,而 B 先于 A 析构,就会出问题。SimpleSingleton 更容易遇到这个问题,因为它的构造/析构时机不容易控制。
解决方案:让析构函数不依赖其他单例,或者用 atexit 显式控制析构顺序。
2. 循环依赖¶
如果单例 A 的初始化依赖单例 B,而 B 的初始化又依赖 A,就会死锁。这在 Singleton 中更容易发现(因为 init() 调用链是显式的),在 SimpleSingleton 中可能表现为首次访问时的递归初始化。
3. 生命周期¶
SimpleSingleton 的实例是函数局部静态变量,在 main 结束后的静态析构阶段销毁。如果你的单例持有需要在 main 之前或之后特殊管理的资源(比如线程、文件句柄),需要注意析构时机。
Singleton 的实例通过 unique_ptr 管理,也在静态析构阶段销毁,但可以通过 reset() 提前释放。
4. 不可复制、不可移动¶
两种单例都禁止复制和移动。这是有意为之的——单例意味着全局唯一,复制或移动会破坏这个语义。
线程安全¶
| 操作 | SimpleSingleton | Singleton |
|---|---|---|
instance() |
线程安全(C++11 保证) | 线程安全(call_once 保护构造,但访问需要判空) |
init() |
N/A | 线程安全(call_once) |
reset() |
N/A | 不安全,需要外部同步 |
注意:Singleton::reset() 不是线程安全的。如果你需要在多线程环境下重置单例,需要自己的同步机制。通常重置只在测试中使用,单线程环境下没有问题。