【问题标题】:Why are two raw pointers to the managed object needed in std::shared_ptr implementation?为什么在 std::shared_ptr 实现中需要两个指向托管对象的原始指针?
【发布时间】:2016-03-06 21:22:45
【问题描述】:

这是cppreference的std::shared_ptr的实现注释部分的引用,其中提到有两个不同的指针(如粗体所示):一个可以由get()返回,一个包含实际数据控制块。

在典型的实现中,std::shared_ptr 只包含两个指针:

  1. 存储的指针(get() 返回的指针)
  2. 指向控制块的指针

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

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

shared_ptr直接持有的指针是get()返回的指针,而控制块持有的指针或对象是共享所有者数为零时删除的指针或对象。这些指针不一定相等。

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

【问题讨论】:

  • IIUC 这意味着实际上可能至少涉及 三个 指针: 1. get() 返回的东西; 2. 可能是控制块指向最终将被删除的对象的指针; 3. 指向控制块的指针。其中两个由共享的 ptr 持有;第三个位于控制块内。
  • 这是一个不同的问题,正如我在之前的评论中指出的那样。它不关心控制块与对象,而是有 two 指向“对象”的指针,这些指针甚至可能不同(或者不需要持有其中两个) . 加上 ptr 到控制块。 (答案显然与别名 shared_ptr 构造函数有关。)
  • 据我了解,stackoverflow.com/a/26351926 的答案是关于两种构造方法的区别,但仍然没有解释为什么首先需要两种可能不同的分配。
  • @PeterA.Schneider 好的,我发现这个问题令人困惑和模棱两可。它肯定可能问一些与我想的不同的东西。

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


【解决方案1】:

这样做的原因是您可以拥有一个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 的真正实现具有所有隐式转换,因此creator 中的p-and-q 舞蹈是不必要的,但我将其保留在那里以类似于第一个例子。

【讨论】:

  • 从派生类到基类的转换是一个不太人为的例子。
  • 我认为这个例子已经足够好了。关键是我们指向作为原子实体管理的事物的一部分,一对可能是最微不足道和最明确的例子。这里的继承只是“成为某事的一部分”的一种特殊形式。
  • @Angew:不喜欢你。 C++ 的 Ew。这几乎是可能形成的最简洁的例子,这一事实支持了我的观点!
  • 我同意@LightnessRacesinOrbit 的观点,如果你创建了一对,你应该将这个所有权作为一对来处理,而不是试图在所有者之间分割对象。一开始似乎是一个糟糕的设计。但又一次,委员会有时无法交付
  • @LightnessRacesinOrbit 我同意“ew”部分。剩下的就是我的补充:)
【解决方案2】:

@Angew 答案的附加链接:

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

boost smart pointer history

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

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

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

【讨论】:

    【解决方案3】:

    对控制块的一个不可避免的需求是支持弱指针。在对象销毁时通知所有弱指针并不总是可行的(事实上,几乎总是不可行)。因此,弱指针需要指向一些东西,直到它们全部消失。因此,一些内存块必须挂起。那块内存就是控制块。有时它们可​​能被一起分配,但单独分配它们可以让您回收一个可能昂贵的对象,同时保留便宜的控制块。

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

    这也允许对象在分配后进入共享所有权的情况。 make_shared 或许可以将这两个概念捆绑到一块内存中,但shared_ptr&lt;T&gt;(new T) 必须先分配 T,然后再想办法共享它。如果不希望这样做,boost 有一个相关的概念 intrusive_ptr,它直接在对象内部而不是使用控制块进行引用计数(您必须自己编写递增和递减运算符才能使其工作)。

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

    高性能实现还可以池化分配控制块,从而将使用它们的成本几乎为零。

    【讨论】:

    • 问题是“为什么控制块包含存储指针的副本”,而不是“为什么存在控制块?”
    【解决方案4】:

    让我们看看std::shared_ptr&lt;int&gt; 这是一个指向int* 的引用计数智能指针。现在int* 不保存引用计数信息,shared_ptr 对象本身也不能保存引用计数信息,因为它很可能在引用计数下降到零之前就被破坏了。

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

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

    【讨论】:

    • 您没有回答问题,因为从您的解释来看,仅指向控制块的指针就足够了,get() 可以只返回指向控制块内实际对象的指针。
    • 据我了解(并试图澄清它),问题不在于为什么存在“指向控制块的指针”和“指向对象的指针”。问题是为什么有“一个指向控制块的指针”,然后是“一个指向对象的指针,存储在shared_ptr 中”,然后是“一个指向对象的指针,存储在控制块中”。
    • @Angew 问题是why there are TWO raw pointers 一个就足够了
    • @Slava 是的;我是在向多伦发表评论。
    • @Slava 我认为最初的问题是模棱两可的。是的,它问为什么有两个指针,但它问为什么我们不能只存储要由get() 返回的指针。也就是说,OP 似乎认为控制块要么是不必要的,要么可以被get() 指针引用。在此答案所描述的情况下,这些都不是一个选项。
    猜你喜欢
    • 1970-01-01
    • 2021-11-15
    • 2012-09-07
    • 2021-11-05
    • 2021-07-15
    • 1970-01-01
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    相关资源
    最近更新 更多