为什么在std :: shared_ptr实现中需要两个指向托管对象的原始指针?

时间:2015-12-02 15:07:53

标签: c++ pointers c++11 shared-ptr

这里引用了std::shared_ptr的cppreference的实现说明部分,其中提到有两个不同的指针(如粗体所示):{{1}可以返回的指针在控制块中保存实际数据的那个。

  

在典型的实现中,get()只包含两个指针:

     
      
  1. 存储的指针(std::shared_ptr返回的指针)
  2.   
  3. 指向控制块的指针
  4.         

    控制块是一个动态分配的对象,它包含:

         
        
    1. 指向托管对象的指针或托管对象本身
    2.   
    3. 删除者(类型删除)
    4.   
    5. 分配器(类型擦除)
    6.   
    7. 拥有托管对象的get()的数量
    8.   
    9. 引用托管对象的shared_ptrs的数量
    10.         

      weak_ptrs直接持有的指针是由shared_ptr返回的指针,而控制块保存的指针或对象是当共享所有者数量达到零时将被删除的指针或对象。这些指针不一定相等。

我的问题是,为什么托管对象需要两个不同的指针(两个粗体)(除了指向控制块的指针)?那个get()返回的那个不够吗?为什么这些指针不一定相等?

4 个答案:

答案 0 :(得分:46)

这样做的原因是你可以拥有一个shared_ptr,而不是它所拥有的东西,这是设计的。这是使用列为nr的构造函数实现的。 8 cppreference

template< class Y >
shared_ptr( const shared_ptr<Y>& r, T *ptr );

使用此构造函数创建的shared_ptrr共享所有权,但指向ptr。考虑这个(设计的,但是说明性的)代码:

std::shared_ptr<int> creator()
{
  using Pair = std::pair<int, double>;

  std::shared_ptr<Pair> p(new Pair(42, 3.14));
  std::shared_ptr<int> q(p, &(p->first));
  return q;
}

一旦此函数退出,只有指向该对的int子对象的指针可用于客户端代码。但由于qp之间的共享所有权,指针q“保持”整个Pair对象。

一旦发生了交易,必须将整个Pair对象的指针传递给删除器。因此,指向Pair对象的指针必须存储在删除器旁边的某个位置 - 换句话说,在控制块中。

对于一个不那么做作的例子(可能甚至更接近该特征的原始动机),考虑指向基类的情况。像这样:

struct Base1
{
  // :::
};

struct Base2
{
  // :::
};

struct Derived : Base1, Base2
{
 // :::
};

std::shared_ptr<Base2> creator()
{
  std::shared_ptr<Derived> p(new Derived());
  std::shared_ptr<Base2> q(p, static_cast<Base2*>(p.get()));
  return q;
}

当然,std::shared_ptr的真实实现具有所有隐式转换,因此p中的q - 和 - creator舞蹈不是必需的,但是我把它放在那里就像第一个例子。

答案 1 :(得分:0)

控制块的一个不可避免的需求是支持弱指针。通知所有关于对象破坏的弱指针并不总是可行的(事实上,它几乎总是不可行)。因此,弱指针需要指出一些东西,直到它们全部消失为止。因此,一些内存块必须徘徊。该内存块是控制块。有时它们可​​能被分配在一起,但是单独分配它们可以让你回收一个可能很昂贵的对象,同时保留廉价的控制块。

一般规则是只要存在单个共享指针或引用它的弱指针,控制块就会持续存在,而在没有指向它的共享指针的瞬间允许回收对象。

这也允许在分配后将对象置于共享所有权的情况。 make_shared可能能够将这两个概念捆绑到一个内存块中,但shared_ptr<T>(new T)必须先分配T,然后弄清楚如何在事后分享它。当这是不合需要的时候,boost有一个相关的intrusive_ptr概念,它直接在对象内部进行引用计数而不是控制块(你必须自己编写递增和递减运算符才能使它工作)。

我见过没有控制块的共享指针实现。相反,共享指针在它们之间开发了一个链表。只要链表包含1个或更多shared_ptrs,该对象仍然存在。但是,这种方法在多线程场景中更复杂,因为您必须维护链表而不仅仅是简单的引用计数。在您重复分配和重新分配shared_ptrs的许多情况下,它的运行时间也可能更糟,因为链表更加重量级。

高性能实现也可以对控制块进行分配,从而将使用它们的成本提高到接近零。

答案 2 :(得分:0)

@Angew 答案的附加链接:

<块引用>

Peter Dimov、Beman Dawes 和 Greg Colvin 通过第一个库技术报告(称为 TR1)提议将 shared_ptr 和 weak_ptr 包含在标准库中。该提案被接受并最终在 2011 年的迭代中成为 C++ 标准的一部分。

boost smart pointer history

在这个proposal中,作者指出了“共享指针别名”的用法:

<块引用>

高级用户通常需要能够创建一个 shared_ptr 实例 p,该实例 p 与另一个(主)shared_ptr q 共享所有权,但指向一个不是 *q 基础的对象。例如,*p 可以是 *q 的成员或元素。本节提出了一个可用于此目的的附加构造函数。

所以他们在控制块中添加了一个额外的指针。

答案 3 :(得分:-1)

让我们看一下std::shared_ptr<int> 这是指向int*的引用计数智能指针。现在int*没有引用计数信息,shared_ptr对象本身不能保存引用计数信息,因为它可能在引用计数下降到零之前很好地被破坏。

这意味着我们必须有一个中间对象来保存控制信息,保证在参考计数降至零之前保持持久。

话虽如此,如果使用shared_ptr创建make_sharedint和控制块都将在连续内存中创建,从而使解除引用更加高效。