在现代 C++ 开发中,单例模式仍然是一种常用的设计模式,尤其是在需要共享资源(如日志系统、配置管理器、数据库连接池等)时。虽然传统的单例实现可以通过双重检查锁定(Double-Check Locking)等方式完成,但这些实现往往不够直观,且在多线程环境下容易出现细微的 race 条件。自 C++11 起,标准库提供了线程安全的 std::call_once 以及 std::once_flag,再配合 std::unique_ptr 或直接返回静态对象,便可以实现极为简洁且线程安全的懒汉式单例。下面给出一个完整的实现示例,并演示如何在 C++17 环境下使用。
1. 单例接口与实现
// Singleton.h
#pragma once
#include <memory>
#include <mutex>
template <typename T>
class Singleton {
public:
// 获取单例实例的引用
static T& instance() {
std::call_once(initFlag_, []() {
instance_.reset(new T());
});
return *instance_;
}
// 禁止拷贝与移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
Singleton() = default;
virtual ~Singleton() = default;
private:
static std::once_flag initFlag_;
static std::unique_ptr <T> instance_;
};
// Singleton.cpp
#include "Singleton.h"
template <typename T>
std::once_flag Singleton <T>::initFlag_;
template <typename T>
std::unique_ptr <T> Singleton<T>::instance_;
说明
std::call_once只会在第一次调用时执行传入的 lambda,后续调用不再触发。std::once_flag用来保证call_once的一次性执行。std::unique_ptr保证了单例对象在程序退出时的正确析构。- 通过把
instance_声明为static,可以实现跨线程共享。
2. 业务类继承单例
// Logger.h
#pragma once
#include "Singleton.h"
#include <iostream>
#include <string>
#include <chrono>
#include <iomanip>
class Logger : public Singleton <Logger> {
friend class Singleton <Logger>; // 让 Singleton 能够访问 Logger 的构造函数
public:
void log(const std::string& message) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::cout << "[" << std::put_time(std::localtime(&time), "%F %T") << "] " << message << std::endl;
}
private:
Logger() = default;
~Logger() = default;
};
说明
Logger的构造函数是私有的,只有 `Singleton ` 通过 `friend` 能够访问。Singleton的instance()函数返回Logger&,可以在任何地方直接使用。
3. 在多线程环境中使用单例
// main.cpp
#include "Logger.h"
#include <thread>
#include <vector>
void worker(int id) {
Logger::instance().log("Thread " + std::to_string(id) + " started.");
// 业务逻辑...
Logger::instance().log("Thread " + std::to_string(id) + " finished.");
}
int main() {
const int threadCount = 8;
std::vector<std::thread> threads;
for (int i = 0; i < threadCount; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
Logger::instance().log("All threads completed.");
return 0;
}
运行上述程序时,可以看到所有线程都共享同一个 Logger 实例,且日志输出顺序不受线程调度的影响(虽然消息的时序可能因调度而异,但单例的初始化只会在第一次调用 instance() 时执行一次,且不会产生任何 race 条件)。
4. 优点与适用场景
- 线程安全 –
std::call_once确保了懒汉式初始化的原子性。 - 延迟加载 – 只有在第一次使用时才创建实例,避免不必要的资源占用。
- 易于维护 – 单例实现被封装在
Singleton模板中,业务类只需继承即可。 - 自动销毁 –
std::unique_ptr保证在程序结束时自动析构,避免内存泄漏。
常见的适用场景包括:
- 日志系统(如上例)
- 配置管理器
- 线程池
- 数据库连接池
- 缓存管理器
5. 可能的改进
虽然上述实现已经满足大多数需求,但在一些特殊场景下可能需要做进一步优化:
| 场景 | 解决方案 |
|---|---|
| 单例需要延迟销毁 | 使用 std::shared_ptr 并手动 reset() |
| 单例初始化需要额外参数 | 在 instance() 前提供 init() 函数,或使用工厂模式 |
| 单例需要在多进程共享 | 需要使用进程间共享内存或 mmap 进行映射,C++ 标准库并不直接支持 |
6. 结语
通过结合 std::call_once、std::once_flag 与 std::unique_ptr,C++17 可以轻松实现线程安全的懒汉式单例模式。相比传统的双重检查锁定(Double-Check Locking),这种方式更为简洁、可读且安全。只需将业务类继承自 `Singleton