懒汉模式是“用到时才创建”的单例实现方式,需用std::call_once和std::once_flag保证线程安全的仅一次初始化,避免裸指针多线程重复构造;推荐结合std::unique_ptr自动管理生命周期。

什么是懒汉模式,为什么不能直接用普通指针
懒汉模式的核心是“用到时才创建”,但 C++ 类型对象在栈上声明会立即构造,在堆上用 new 又得手动管理生命周期——这既容易漏删,又无法保证线程安全。直接用裸指针 + if (p == nullptr) 判断,在多线程下可能触发多次构造,导致未定义行为。
关键点在于:必须原子性地完成“检查 + 构造 + 赋值”三步,且只执行一次。C++11 提供了 std::once_flag 和 std::call_once 来解决这个问题。
std::call_once + std::once_flag 的标准写法
这是目前最推荐、最轻量的懒汉实现方式,不依赖智能指针,也不需要锁对象显式管理。它底层由编译器和标准库协同保障“仅执行一次”,且支持异常安全(若初始化函数抛异常,后续调用仍会重试)。
常见写法如下:
class ResourceManager {
public:
static ResourceManager& instance() {
std::call_once(init_flag_, []{
instance_ = new ResourceManager();
});
return *instance_;
}
<p>private:
ResourceManager() = default;
static ResourceManager* instance_;
static std::once_flag init<em>flag</em>;
};</p><p>ResourceManager* ResourceManager::instance_ = nullptr;
std::once_flag ResourceManager::init<em>flag</em>;
</p>
-
std::once_flag必须是静态变量(或全局/静态局部),否则每次调用都新建一个 flag,失去“once”语义 - lambda 中用
new是为了绕过栈对象生命周期限制;若需自动释放,应配合static std::unique_ptr,并在 lambda 中reset(new ResourceManager()) - 不要在
instance()里加if (!instance_) return *instance_;——std::call_once已隐含该逻辑,重复判断反而干扰可读性
std::unique_ptr + std::call_once 组合更安全
裸指针容易忘记 delete,而 std::unique_ptr 能自动析构。但要注意:静态 std::unique_ptr 的初始化本身不是线程安全的,所以仍要靠 std::call_once 控制构造时机。
正确写法:
class Logger {
public:
static Logger& instance() {
std::call_once(init_flag_, []{
instance_.reset(new Logger());
});
return *instance_;
}
<p>private:
Logger() = default;
static std::unique<em>ptr<logger> instance</logger></em>;
static std::once_flag init<em>flag</em>;
};</p><p>std::unique<em>ptr<logger> Logger::instance</logger></em>;
std::once_flag Logger::init<em>flag</em>;
</p>
-
instance_.reset(...)是安全的,即使之前为nullptr也无副作用 - 不能写成
instance_ = std::make_unique,因为() std::make_unique在 lambda 外部调用时可能提前构造对象(取决于编译器优化),破坏懒加载语义 - 如果类有非默认构造参数,直接在
new Logger(arg1, arg2)中传入即可,std::make_unique在此处反而受限
为什么不用 std::call_once 的静态局部变量替代方案
C++11 允许静态局部变量的初始化是线程安全的,比如:
static T& get() {
static T obj;
return obj;
}
这看起来更简洁,但它属于“饿汉变体”——首次调用 get() 时就构造,且无法传递构造参数(除非用工厂函数封装)。更重要的是:它不满足“延迟到真正使用成员前才初始化”的场景。例如,某类的构造函数内部会打开文件或连接数据库,但你只想在调用 log() 时才触发,而不是一进 get() 就执行。
此时 std::call_once + 显式 new 或 reset() 是唯一可控的方式。它的延迟粒度更细,错误处理也更明确——比如构造失败可抛异常并由上层决定是否重试,而静态局部变量一旦抛异常,下次调用仍会再次尝试,但状态已不可预测。
实际项目中,只要初始化逻辑稍复杂(涉及 IO、配置加载、依赖注入),就别贪图语法糖,老实用 std::once_flag。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










