在 C++17 之前,单例模式实现往往需要手动处理同步,容易出现错误。自 C++11 起,标准库提供了线程安全的静态局部变量初始化,简化了单例实现。下面给出几种常见实现方式,并讨论它们的优缺点。
1. 局部静态变量(最推荐)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 业务初始化 */ }
};
优点
- 代码简洁,几行即可完成。
- 依赖标准库实现,线程安全性保证。
- 对象延迟加载,只有真正访问
instance()时才创建。
缺点
- 只能在函数内部使用局部静态,若想让单例在程序结束前被显式销毁,需自己手动实现。
- 如果单例构造时抛异常,后续访问会再次尝试构造。
2. std::call_once + std::unique_ptr
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []{
ptr.reset(new Singleton);
});
return *ptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 初始化 */ }
static std::once_flag initFlag;
static std::unique_ptr <Singleton> ptr;
};
std::once_flag Singleton::initFlag;
std::unique_ptr <Singleton> Singleton::ptr;
优点
- 能在程序结束前主动销毁单例。
- 兼容 C++11 之前的编译器(使用
std::call_once也需要 C++11,但构造器不需要 C++11 静态初始化)。
缺点
- 代码量略多,维护成本增加。
std::unique_ptr的析构顺序可能与其他全局对象产生竞争。
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;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
缺点
- 需要对
ptr进行原子读写,存在可见性问题。 - 若编译器不采用合适的内存屏障,可能导致多线程下实例不完整。
- 代码更易出错,建议使用前两种方式。
4. Meyers 单例(静态局部变量)与全局对象
如果单例本身不需要动态销毁,或者不需要在 main 之前使用,Meyers 单例是最简单且最安全的方式。它利用 C++11 对局部静态变量初始化的线程安全保证,避免了手动加锁。
如何在单例中使用依赖注入
在复杂项目中,单例往往需要依赖其他组件。可以将依赖以构造参数形式传入,并通过 std::function 或者 std::unique_ptr 存储。
class Logger {
public:
void log(const std::string& msg) { /* ... */ }
};
class Config {
public:
std::string get(const std::string& key) const { /* ... */ }
};
class Singleton {
public:
static Singleton& instance(Logger* logger = nullptr, Config* cfg = nullptr) {
static Singleton s;
if (logger) s.setLogger(logger);
if (cfg) s.setConfig(cfg);
return s;
}
// 业务接口...
private:
Singleton() = default;
void setLogger(Logger* l) { logger = l; }
void setConfig(Config* c) { config = c; }
Logger* logger = nullptr;
Config* config = nullptr;
};
这样可以在第一次访问时注入依赖,后续访问不受影响。
小结
- 推荐:使用 C++11 的局部静态变量实现(Meyers 单例)。
- 需要显式销毁:使用
std::call_once+std::unique_ptr。 - 避免:手写双重检查锁,除非有特殊需求且你非常了解内存模型。
- 依赖注入:可通过可选参数或单独的
set方法实现。
通过上述方案,你可以在 C++ 项目中安全、可靠地实现单例模式,既兼顾了性能,又遵循了现代 C++ 的最佳实践。