CPP make_shared用于void指针

cpp make_shared for void pointers

本文关键字:void 指针 用于 shared make CPP      更新时间:2023-10-16

我想使用std::make_shared来创建一个void指针。由于make_shared应该比shared_ptr(新T)更快,并且例外保存,我想知道是否有一个库函数可以在make_shared方式中创建shared_ptr(新foo)。

您可以将任何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*,其中Tshared_ptr<T>中的T相同。可以将其视为存储对象的"静态"类型。当对shared_ptr<T>解引用时,被解引用的是p2。当您销毁shared_ptr<T>时,如果引用计数变为零,则是控制块中的指针帮助销毁foo

在上图中,控制块和foo都是动态分配的。p1是一个归属指针,控制块中的指针也是一个归属指针。p2是非归属指针。p2只有函数是解引用的(箭头操作符,get()等)。

当您使用make_shared<foo>()时,实现有机会将foo放在控制块中,与引用计数和其他数据一起:

p1  ---------> (refcount, foo)
p2  --- foo* --------------^

这里的优化是现在只有一个分配:现在嵌入foo的控制块。

当上面的转换为shared_ptr<void>时,所发生的就是:

p1  ---------> (refcount, foo)
p2  --- void* -------------^

p2的类型由foo*变为void*。就是这样。(除了增加/减少引用计数来解释临时对象的复制和销毁——这可以通过右值构造来省略)。当引用计数变为零时,仍然是控制块破坏通过p1找到的foop2不参与销毁操作。

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,源代码在析构函数运行之前没有指向任何对象。简而言之,这就是移动语义的乐趣!: -)