在现代C++中,单例模式仍然是解决“全局唯一实例”问题的一种常见手段。相比传统的懒加载实现,C++11之后提供的线程安全静态局部变量以及std::call_once/std::once_flag可以让我们轻松实现线程安全的单例。下面将从几种实现方式展开讨论,并说明它们各自的优缺点。
1. 静态局部变量(Meyer’s Singleton)
class Singleton {
public:
static Singleton& instance() {
static Singleton inst; // C++11保证线程安全
return inst;
}
// 禁止拷贝与移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
-
优点
- 代码最简洁;只需一个
static变量。 - 由于编译器在第一次调用
instance()时执行一次初始化,保证了线程安全。 - 对象在程序结束时自动销毁,无需手动管理生命周期。
- 代码最简洁;只需一个
-
缺点
- 若程序中存在“顺序销毁”问题(比如全局对象依赖单例),在程序退出时可能导致未定义行为。
- 不能在单例构造时抛异常而让程序安全退出;异常会导致全局析构顺序混乱。
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() {
instance_.reset(new Singleton);
});
return *instance_;
}
// 其余禁止拷贝/移动与构造/析构同上
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;
-
优点
- 可以在单例构造时执行复杂逻辑(如读取配置文件、打开网络连接)并处理异常。
- 对象的创建与销毁都受
unique_ptr控制,更容易与其他资源共同管理。 call_once在多线程场景下仍然保证一次性初始化。
-
缺点
- 代码稍微繁琐,需要手动维护
std::once_flag与std::unique_ptr。 - 依旧存在顺序销毁问题,但可以通过在单例内部使用
std::shared_ptr或显式销毁来缓解。
- 代码稍微繁琐,需要手动维护
3. 线程本地单例(TLS)
如果你需要在每个线程中拥有自己的单例实例,可使用线程局部存储(TLS):
class ThreadSingleton {
public:
static ThreadSingleton& instance() {
thread_local ThreadSingleton inst;
return inst;
}
// 同上禁拷贝/移动与构造/析构
private:
ThreadSingleton() = default;
~ThreadSingleton() = default;
};
- 适用场景
- 需要在多线程环境下隔离状态,避免竞争。
- 当单例维护线程相关信息(如日志文件句柄、线程ID等)时特别有用。
4. 对比与选择
| 实现方式 | 线程安全 | 延迟加载 | 析构顺序 | 代码简洁度 |
|---|---|---|---|---|
| 静态局部 | ✔ | ✔ | 受限 | 极简 |
call_once |
✔ | ✔ | 可控 | 中等 |
| TLS | ✔ | ✔ | 受限 | 极简 |
- 如果你只需要一个全局唯一实例,并且不在乎程序退出时的析构顺序,Meyer’s Singleton是最好的选择。
- 如果单例需要在构造时执行可能抛异常的逻辑,或想在退出时显式销毁,则建议使用
std::call_once方案。 - 如果单例需要在每个线程中独立存在,则使用TLS实现。
5. 进阶:多态单例与工厂模式
在某些大型项目中,单例往往需要实现不同的子类,例如日志系统可能有文件日志、网络日志等。可以在单例内部采用工厂模式:
class Logger {
public:
virtual void log(const std::string& msg) = 0;
virtual ~Logger() = default;
};
class FileLogger : public Logger { /* ... */ };
class NetworkLogger : public Logger { /* ... */ };
class LoggerFactory {
public:
static Logger* create(const std::string& type) {
if (type == "file") return new FileLogger;
if (type == "network") return new NetworkLogger;
return nullptr;
}
};
class LoggerSingleton {
public:
static LoggerSingleton& instance() {
static LoggerSingleton inst;
return inst;
}
Logger& getLogger() { return *logger_; }
private:
LoggerSingleton() : logger_(LoggerFactory::create("file")) {}
~LoggerSingleton() { delete logger_; }
Logger* logger_;
// 禁止拷贝/移动
LoggerSingleton(const LoggerSingleton&) = delete;
LoggerSingleton& operator=(const LoggerSingleton&) = delete;
};
这样既保持了单例的全局唯一性,又支持多态的实现细节。
6. 小结
C++11后,单例实现比以往更安全、更简洁。通过static局部变量即可保证线程安全,若需要更细粒度的控制,std::call_once/std::once_flag提供了更灵活的手段。理解不同实现的权衡点,才能在项目中选择最合适的单例方案。