您可以将任何shared_ptr<foo> 转换为shared_ptr<void> 而不会损失与make_shared 相关的效率:
#include <memory>
struct foo {};
int main()
{
std::shared_ptr<void> p = std::make_shared<foo>();
}
转换将foo 和引用计数保持在相同的内存分配中,即使您现在通过void* 引用它。
更新
这是如何工作的?
std::shared_ptr<foo> 的一般结构是两个指针:
+------> foo
| ^
p1 ---------> (refcount, +) |
p2 --- foo* -----------------------+
p1 指向一个控制块,其中包含一个引用计数(实际上是两个引用计数:一个用于强所有者,一个用于弱所有者)、一个删除器、一个分配器和一个指向对象“动态”类型的指针. “动态”类型是shared_ptr<T> 构造函数看到的对象的类型,比如Y(可能与T 相同也可能不同)。
p2 具有 T* 类型,其中 T 与 shared_ptr<T> 中的 T 相同。将此视为存储对象的“静态”类型。当您取消引用 shared_ptr<T> 时,将取消引用 p2。当您销毁 shared_ptr<T> 时,如果引用计数变为零,则控制块中的指针有助于销毁 foo。
在上图中,控制块和foo都是动态分配的。 p1是一个拥有指针,控制块中的指针是一个拥有指针。 p2 是一个非拥有指针。 p2 的 only 函数是取消引用(箭头运算符、get() 等)。
当您使用make_shared<foo>() 时,实现有机会将foo 与引用计数和其他数据一起放在控制块中:
p1 ---------> (refcount, foo)
p2 --- foo* --------------^
这里的优化是现在只有一个分配:现在嵌入foo的控制块。
当上述内容转换为shared_ptr<void> 时,会发生以下情况:
p1 ---------> (refcount, foo)
p2 --- void* -------------^
即p2 的类型从 foo* 更改为 void*。而已。 (除了增加/减少引用计数以解释临时的副本和销毁——这可以通过从右值构造来省略)。当引用计数变为零时,仍然是控制块破坏了通过p1 找到的foo。 p2 不参与销毁操作。
p1 实际上指向控制块的通用基类。这个基类不知道存储在派生控制块中的foo 类型。当实际对象类型Y 已知时,派生控制块在shared_ptr 的构造函数中构造。但从那时起shared_ptr 只能通过control_block_base* 与控制块通信。所以像销毁这样的事情是通过虚函数调用发生的。
在 C++11 中从右值 shared_ptr<foo> 中“移动构造”shared_ptr<void> 只需复制两个内部指针,而不必操作引用计数。这是因为右值 shared_ptr<foo> 无论如何都会消失:
// shared_ptr<foo> constructed and destructed within this statement
std::shared_ptr<void> p = std::make_shared<foo>();
这在shared_ptr构造函数源代码中可以最清楚地看到:
template<class _Tp>
template<class _Yp>
inline _LIBCPP_INLINE_VISIBILITY
shared_ptr<_Tp>::shared_ptr(shared_ptr<_Yp>&& __r,
typename enable_if<is_convertible<_Yp*, _Tp*>::value, __nat>::type)
_NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
__r.__ptr_ = 0;
__r.__cntrl_ = 0;
}
在转换构造之前,引用计数只有 1。在转换构造之后,引用计数仍然是 1,而在其析构函数运行之前,源指向空。简而言之,这就是移动语义的乐趣! :-)