
std::shared_ptr 本身不支持延迟初始化,必须手动封装
标准库的 std::shared_ptr 是一个裸指针包装器,构造时就完成资源获取(如调用 new 或工厂函数),它不内置“首次访问才创建”的逻辑。所谓“延迟初始化”,本质是业务逻辑层的责任,不是智能指针的职责。
常见误用是试图在多线程中直接对 std::shared_ptr 成员变量做“判空 + 构造”操作,这会引发竞态——两个线程同时看到空指针、同时执行 new,导致资源重复创建或泄漏。
正确做法是把延迟逻辑抽出来,配合同步机制(如 std::call_once 或互斥锁)控制初始化时机。
用 std::call_once 实现线程安全的延迟初始化(推荐)
std::call_once 是 C++11 提供的轻量级一次性初始化原语,比手写双重检查锁(DCLP)更简洁、更不易出错,且无内存序陷阱。
立即学习“C++免费学习笔记(深入)”;
- 它内部已处理了内存屏障和原子状态,无需手动写
std::atomic或std::memory_order - 初始化函数只执行一次,即使多个线程同时进入,也只有一个成功执行,其余阻塞等待完成
- 配合
std::shared_ptr的线程安全赋值(operator=对shared_ptr对象本身是线程安全的),可安全发布共享对象
示例:
class ResourceManager {
mutable std::shared_ptr resource_;
mutable std::once_flag init_flag_;
public:
std::shared_ptr get_resource() const {
std::call_once(init_flag_, [this] {
resource_ = std::make_shared();
});
return resource_;
}
};
为什么不该手写双重检查锁(DCLP)?
虽然 DCLP 在 Java 中常见,但在 C++ 中极易写出有缺陷的版本,尤其涉及 std::shared_ptr 时:
- 漏掉内存序约束:未用
std::memory_order_acquire/std::memory_order_release,可能导致指针可见性问题(一个线程看到非空指针,但指向未完全构造的对象) -
std::shared_ptr的赋值不是原子操作:它涉及引用计数更新和指针写入,不能仅靠if (ptr == nullptr)判断就跳过锁 - 编译器重排风险:若未用
std::atomic包装指针或显式 barrier,构造函数内联、指令重排都可能破坏初始化顺序
即便写对,代码复杂度远高于 std::call_once,且无性能优势——现代实现中 std::call_once 在首次调用后几乎无开销。
std::shared_ptr 的线程安全边界必须分清
很多人混淆了“shared_ptr 对象本身”和“它所管理的对象”的线程安全性:
-
std::shared_ptr对象的拷贝/赋值/析构是线程安全的(引用计数增减原子) - 但多个
shared_ptr实例指向的同一对象,其内部数据访问仍需用户自行加锁 - 延迟初始化场景下,你保护的是“谁来执行构造 + 谁来首次赋值”,不是“谁来读这个指针”
所以 get_resource() 返回拷贝是安全的;但若返回引用或原始指针(.get()),后续对该对象的读写仍需额外同步。
最易被忽略的一点:延迟初始化的“延迟”是有代价的——每次调用都要走一次 std::call_once 的路径判断(尽管首次后极快)。如果该函数被高频调用(如内层循环),且初始化早已完成,应考虑缓存结果或改用静态局部变量(C++11 起保证线程安全初始化)。










