std::shared_ptr可从std::unique_ptr初始化,通过移动语义实现所有权的单向降级:独占转共享,资源安全转移,删除器被类型擦除后存入控制块。该设计允许接口解耦与延迟共享开销,提升灵活性与性能,是标准刻意支持的机制。
一、标准规定了什么
C++ 标准为 std::shared_ptr提供了一个构造函数:
template<class Y, class D>
shared_ptr(std::unique_ptr<Y, D>&& r);
关键点:
-
参数类型是 右值引用
unique_ptr&& -
调用后,
r被置空,不再拥有资源 -
资源的所有权从
unique_ptr转移给shared_ptr
这意味着:只有 unique_ptr可以被"移动"给 shared_ptr,拷贝是不允许的。
二、为什么允许:所有权的单向降级
所有权模型决定了这种转换的合法性:
| 模型 | 语义 |
|---|---|
unique_ptr |
我是唯一所有者,我销毁资源 |
shared_ptr |
我们是共同所有者,最后一个销毁资源 |
从独占到共享,是所有权的降级:
原来一个人说了算,现在交给一个群体共同管理。
这在逻辑上完全自洽------资源依然存在,只是释放时机从"当前作用域结束"变成了"最后一个 shared_ptr析构"。
反过来则不行:从 shared_ptr到 unique_ptr意味着强行剥夺其他共享者的权利,会导致悬垂指针和重复释放,因此标准禁止。
三、底层发生了什么
std::unique_ptr<Foo> u = std::make_unique<Foo>();
std::shared_ptr<Foo> s = std::move(u);
执行步骤:
-
接管指针 :
s获得u内部管理的原始指针 -
复制删除器 :
u的删除器被复制到s的控制块中 -
置空源对象 :
u内部指针变为nullptr,析构时不再做任何事 -
初始化控制块 :
s的强引用计数设为 1
结果:u变为空壳,s成为资源的唯一管理者(尽管它是共享指针,但当前引用计数为 1)。
四、删除器如何被保留
unique_ptr<T, D>的删除器是类型的一部分,编译期确定。
shared_ptr<T>的删除器是类型擦除的,存储在控制块中。
转换时,shared_ptr把 unique_ptr的删除器包装成一个类型无关的可调用对象,存入控制块。因此,即使 shared_ptr的模板参数只有 T,它也能在引用计数归零时调用正确的删除逻辑。
前提只有一个:删除器必须可复制。
五、为什么标准要提供这个能力
1. 接口解耦
工厂函数可以统一返回 unique_ptr,调用方按需决定是否共享:
std::unique_ptr<Foo> create(); // 库只负责创建
// 调用方 A:不需要共享,零开销
auto u = create();
// 调用方 B:需要共享
auto s = std::shared_ptr<Foo>(create());
如果只能返回 shared_ptr,所有调用方都要承担原子引用计数的开销,即使他们不需要共享。
2. 延迟共享开销
局部作用域内用 unique_ptr管理资源(零开销),直到真正需要跨作用域共享时,才转为 shared_ptr:
void f() {
auto u = std::make_unique<Foo>(); // 零开销
u->init();
if (need_share) {
std::shared_ptr<Foo> s = std::move(u); // 此时才引入共享开销
pass_to_other_thread(s);
}
}
六、一句话总结
shared_ptr可以从unique_ptr初始化,是因为独占所有权可以通过移动语义安全地降级为共享所有权,且删除器被完整保留在shared_ptr的控制块中。这是标准刻意设计的能力,用于解耦资源创建与生命周期管理策略。
不需要 std::move也能编译,是因为临时对象本身就是右值,自动匹配移动构造函数。