【问题标题】:shared_ptr<> is to weak_ptr<> as unique_ptr<> is to... what?shared_ptr<> 对 weak_ptr<> 就像 unique_ptr<> 对...什么?
【发布时间】:2013-07-06 09:08:56
【问题描述】:

在 C++11 中,您可以使用 shared_ptr&lt;&gt; 与对象或变量建立所有权关系,并使用 weak_ptr&lt;&gt; 以非拥有方式安全地引用该对象。

您还可以使用unique_ptr&lt;&gt; 与对象或变量建立所有权关系。但是,如果其他非拥有对象也想引用该对象怎么办? weak_ptr&lt;&gt; 在这种情况下没有帮助。原始指针很有帮助,但也带来了各种缺点(例如,它们可以是 automatically initialized to nullptr,但这是通过与 std::*_ptr&lt;&gt; 类型不一致的技术来实现的)。

对于通过unique_ptr&lt;&gt; 拥有的对象的非拥有引用,weak_ptr&lt;&gt; 的等价物是什么?

这是一个清晰的示例,类似于我正在开发的游戏中的某些内容。

class World
{
public:

    Trebuchet* trebuchet() const { return m_trebuchet.get(); }

private:
    std::unique_ptr< Trebuchet > m_trebuchet;
};

class Victim
{
public:
    Victim( Trebuchet* theTrebuchet ) : m_trebuchet( theTrebuchet ) {}

    ~Victim()
    {
        delete m_trebuchet;     // Duh. Oops. Dumb error. Nice if the compiler helped prevent this.
    }

private:

    Trebuchet* m_trebuchet;    // Non-owning.
};

shared_ptr< Victim > createVictim( World& world )
{
    return make_shared< Victim >( world.trebuchet() );
}

这里我们使用原始指针来维持与通过unique_ptr&lt;&gt; 在其他地方拥有的对象的非拥有关系。但是,我们能做到最好吗?

希望是一种指针:

  • 看起来像其他现代指针类型。例如。 std::raw_ptr&lt;T&gt;
  • 替换原始指针,以便使用现代指针类型的代码库可以通过搜索_ptr&lt;(大致)找到所有指针。
  • 自动初始化为 nullptr。

因此:

int* p;                  // Unknown value.
std::raw_ptr< int > p;   // null.

这种类型现在是否已经存在于 C++ 中,它是为未来提出的,还是在例如另一个广泛可用的实现中?提升?

【问题讨论】:

  • unique_ptr::get 如果你想访问底层指针。没有weak_ptr 等价物,因为那样unique_ptr 就不会很独特
  • 没有类似的东西,因为unique_ptr 被设计为对原始指针没有开销。如果它必须保留所有弱指针的引用计数,那将是不可能的。
  • @Xeo,这很公平。 unique_ptrweak_ptr 等价物可能成本过高。但是可能没有某种中间功能:一种在语法上类似于其他现代指针类型的指针类型,在构造时自动取消,防止直接调用删除等,但没有额外的开销?这些可能仍会受到悬空指针问题的影响,但不会比原始指针更差,而且确实比原始指针更好。有没有这样的类型存在?
  • @OldPeculier 您混淆了所有权语义。 unique_ptr 是一个拥有指针。如果您想要其他人可以拥有非拥有引用的指针,请使用shared_ptr。介于两者之间的另一个智能指针有什么用途?
  • 您的“Duh. Oops. Dumb error”不是很有说服力。您不应该在所有权句柄之外使用delete(另请参阅:Rule of Zero)。防止这样的错误是防止马基雅维利,而不是墨菲。

标签: c++ c++11 unique-ptr


【解决方案1】:

shared_ptr 的“通知”行为需要对引用计数控制块进行引用计数。 shared_ptr 的引用计数控制块为此使用单独的引用计数。 weak_ptr 实例维护对此块的引用,weak_ptrs 本身阻止引用计数控制块被 deleteed。当强计数变为零时(这可能会也可能不会导致存储该对象的内存的deleteion),指向对象的析构函数被调用,并且控制块只有在弱引用计数归零。

unique_ptr 的宗旨是它比普通指针的开销为零。分配和维护引用计数控制块(以支持weak_ptr-ish 语义)打破了这一原则。如果您需要该描述的行为,那么您确实需要共享语义,即使对该对象的其他引用是非拥有的。在这种情况下,共享仍然在进行——共享对象是否已被销毁的状态。

如果您需要一个通用的非拥有引用并且不需要通知,请使用纯指针或纯引用 unique_ptr 中的项目。


编辑:

在您的示例中,看起来Victim 应该要求Trebuchet&amp; 而不是Trebuchet*。然后很清楚谁拥有该对象。

class World
{
public:

    Trebuchet& trebuchet() const { return *m_trebuchet.get(); }

private:
    std::unique_ptr< Trebuchet > m_trebuchet;
};

class Victim
{
public:
    Victim( Trebuchet& theTrebuchet ) : m_trebuchet( theTrebuchet ) {}

    ~Victim()
    {
        delete m_trebuchet;     // Compiler error. :)
    }

private:

    Trebuchet& m_trebuchet;    // Non-owning.
};

shared_ptr< Victim > createVictim( World& world )
{
    return make_shared< Victim >( world.trebuchet() );
}

【讨论】:

  • @Potatoswatter:谢谢。固定!
  • 在我的示例中,引用的构造函数内初始化是公平的,但我的示例在允许此解决方法方面存在不足。更一般的情况(以及我在代码中实际处理的各种情况下的情况)允许在构造后设置指针。
  • @OldPeculier:我在答案的第一部分给出了“一般答案”,在第二部分给出了具体答案。
  • 在可能的情况下使用参考的绝妙建议。如果您可以先设计要创建的“所有者”容器,然后再设计“窥视”它的所有内容,则通常可以在辅助容器和索引中使用引用,并且一切都“恰到好处”。当原始容器中发生大量更改时,如果它导致重新分配,它会变得更加困难。坚持使用 [unordered_][set|map] 的! :-)
  • 这是一个很好的解释,但不幸的是没有回答这个问题。实际上需要弱引​​用无法复制或具有多个所有者的指针。提议的 non_owning_ptr 将解决这个问题。
【解决方案2】:

确实需要一种标准指针类型来充当std::unique_ptr&lt;&gt; 的非拥有、廉价且行为良好的对位。目前还没有这样的指针被标准化,但是 standard has been proposed 并且正在由 C++ 标准委员会讨论。 “世界上最愚蠢的智能指针”,又名 std::exempt_ptr&lt;&gt; 将具有其他现代 C++ 指针类的一般语义,但对于拥有指向对象(如 shared_ptrunique_ptr 做)或正确响应该对象的删除(就像weak_ptr 所做的那样)。

假设此功能最终得到委员会的批准,它将完全满足此问题中强调的需求。即使没有得到委员会的批准,上述链接文件也充分表达了需求并描述了一个完整的解决方案。

【讨论】:

  • 请注意,建议的 exempt_ptr 不会为您提供所需的通知语义。没有标准的指针可以做到这一点,但写一个是微不足道的。
  • @BillyONeal 请理解:我从未要求通知语义。我暗示这会很好,但这不是问题的本质。
  • 提案的 v3 可在此处获得:http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3840.pdf exempt_ptr 已重命名为 observer_ptr
  • 我喜欢它,如果没有别的,因为它会导致更好的文档化代码。在这种情况下,它类似于使用 static_cast 而不是在你必须这样做的那些讨厌的时候进行强制转换 - 至少,它让我可以搜索我知道我行为不端的地方。 :-)
  • @user1754322 weak_ptr 具有在它指向的对象被销毁时“被通知”的能力。我认为这就是@Billy ONeal 所引用的“通知语义”。
【解决方案3】:

unique_ptr 的 non-owing 模拟是一个普通的 C 指针。有什么不同 - C 指针不知道指向的数据是否仍然可以访问。另一方面,weak_ptr 可以。但是不可能在没有额外开销的情况下用知道数据有效性的指针替换raw 指针(并且weak_ptr 确实有这种开销)。这意味着 C 风格的指针在速度方面是最好的,你可以作为 unique_ptr 的非 owing 模拟。

【讨论】:

  • 不完全,接近,但不完全。
  • 在 C++ 世界中,一个普通的 C 指针是一个“我不会告诉你所有权”的指针。
【解决方案4】:

虽然您不能免费获得指向唯一拥有的对象的“弱”指针,但这个概念很有用,并且在几个系统中使用。有关实现,请参阅 Chromium's WeakPtrQT's QPointer

Chromium 的 WeakPtr 是通过在弱引用对象中存储一个 shared_ptr 并在对象被销毁时将其标记为无效来实现的。 WeakPtrs 然后引用该 ControlBlock 并在分发原始指针之前检查它是否有效。我假设 QT 的 QPointer 实现类似。因为所有权不是共享的,所以原始对象会被确定性地销毁。

然而,这意味着取消引用 WeakUniquePtr 不是线程安全的:

线程 1:

unique_ptr<MyObject> obj(new MyObject);
thread2.send(obj->AsWeakPtr());
...
obj.reset();  // A

线程2:

void receive(WeakUniquePtr<MyObject> weak_obj) {
  if (MyObject* obj = weak_obj.get()) {
    // B
    obj->use();
  }
}

如果A 行恰好与B 行同时运行,线程2 将使用悬空指针结束。 std::weak_ptr 将通过atomically taking a shared owning reference to the object before letting thread 2 use it 阻止这个问题,但这违反了上面的假设,即对象是唯一拥有的。这意味着WeakUniquePtr 的任何使用都需要与真实对象的销毁同步,最简单的方法是要求它们在同一线程上的消息循环中完成。 (请注意,在使用之前在线程间来回复制WeakUniquePtr 仍然是完全安全的。)

可以想象在std::unique_ptr 中使用自定义删除器来使用标准库类型来实现这一点,但这留给读者作为练习。

【讨论】:

    【解决方案5】:
    boost::optional<Trebuchet&>
    

    正如 Billy ONeal 在他的回答中指出的那样,您可能希望传递 Trebuchet&amp; 而不是指针。引用的问题是您不能传递nullptrboost::optional 提供了一种等效于nullptr 的方法。关于 boost::optional 的更多细节在这里:http://www.boost.org/doc/libs/1_54_0/libs/optional/doc/html/boost_optional/detailed_semantics.html

    另请参阅此问题:boost::optional<T&> vs T*

    注意:std::optional&lt;T&gt; 有望进入 C++14,但 std::optional&lt;T&amp;&gt; 是一个单独的提案,不在当前的 C++14 草案中。更多细节在这里:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3672.html

    【讨论】:

      【解决方案6】:

      在带有 shared_ptr、weak_ptr 和 unique_ptr 的新 C++ 世界中,您不应该使用原始指针或引用来存储对对象的长期引用,例如投石机。相反,World 应该有一个 shared_ptr 到投石机,而 Victim 应该存储 shared_ptr 或 weak_ptr,这取决于如果世界消失了,投石机是否应该留在受害者身边。使用 weak_ptr 可以让你判断指针是否仍然有效(即世界仍然存在),没有办法用原始指针或引用来做到这一点。

      当您使用 unique_ptr 时,您声明只有 World 实例拥有投石机。 World 类的客户可以通过调用“get”方法来使用 World 对象的投石机,但在使用完之后不应持有该方法返回的引用或指针。相反,他们应该在每次想要使用投石机时通过调用“get”方法“借用”投石机。

      上面说过,在某些情况下,您可能希望存储引用或原始指针以供将来使用,以避免 shared_ptr 的开销。但是这些实例很少而且相差甚远,您需要完全确保在拥有投石机的 World 对象消失后您不会使用指针或引用。

      【讨论】:

      • 我不知道是否所有的使用都是在主 unique_ptr 之外的临时“借用”——考虑需要在 unique_ptr 的主容器中创建多个二级索引的情况。
      • 对我来说,这似乎是“少之又少”类别的有效成员。
      • 您不维护大型对象集?如果你这样做了,你几乎肯定会需要多个索引。我猜这是 YMMV 的经典案例。
      • 是的,我同意你的看法。将二级索引放入 unique_ptrs 数组似乎是对我的原始指针的有效使用。 “很少和很远”可能是使用错误的描述,请参阅bit.ly/1F4jEQK,了解我对裸指针的更详细感受。
      • 我们的想法肯定是一致的。我现在正在使用 unordered_multiset 索引进入 unordered_set 的 unique_ptr 来解决这个问题。我发现使用多组原始指针相当容易(我永远不会分配或解除分配,这是 unique_ptr 的工作)。但理想情况下,我会对 unique_ptr 使用 const ref。为此设置散列和等于函数很困难......但我还没有放弃。我想我会就此提出另一个问题。
      【解决方案7】:

      otn::raw::weak(来自C++ Object Token Library)是std::unique_ptr 的无主、廉价且行为良好的对立面。在库中还有otn::safe::unique,这是一个唯一的所有者,可以“通知”非所有者otn::safe::weak 删除对象。

      #include <otn/all.hpp>
      #include <iostream>
      
      int main()
      {
          using namespace std;
          using namespace otn;
      
          raw::weak_optional<int> raw_weak;
          if (!raw_weak)
              cout << "raw_weak is empty" << endl;
      
          cout << "--- create object in std_unique..." << endl;
          auto std_unique = std::make_unique<int>(42);
          raw_weak = std_unique;
          if (std_unique)
              cout << "std_unique is not empty" << endl;
          if (raw_weak)
              cout << "raw_weak is not empty" << endl;
      
          cout << "--- move std_unique to safe_unique..." << endl;
          safe::unique_optional<int> safe_unique = std::move(std_unique);
      
          if (!std_unique)
              cout << "std_unique is empty" << endl;
          if (raw_weak)
              cout << "raw_weak is not empty, it is observs safe_unique" << endl;
      
          safe::weak_optional<int> safe_weak = safe_unique;
          if (safe_unique)
              cout << "safe_unique is not empty" << endl;
          if (!safe_weak.expired())
              cout << "safe_weak is not expired" << endl;
      
          cout << "--- destroy object in safe_unique..." << endl;
          utilize(std::move(safe_unique));
          if (!safe_unique)
              cout << "safe_unique is empty" << endl;
          if (safe_weak.expired())
              cout << "safe_weak is expired, it is not dangling" << endl;
          if (raw_weak)
              cout << "raw_weak is not empty, it is dangling!!!" << endl;
      }
      

      输出:

      raw_weak is empty
      --- create object in std_unique...
      std_unique is not empty
      raw_weak is not empty
      --- move std_unique to safe_unique...
      std_unique is empty
      raw_weak is not empty, it is observs safe_unique
      safe_unique is not empty
      safe_weak is not expired
      --- destroy object in safe_unique...
      safe_unique is empty
      safe_weak is expired, it is not dangling
      raw_weak is not empty, it is dangling!!!
      

      【讨论】:

        【解决方案8】:

        采用原始指针或引用的函数隐含地承诺在函数返回后不会保留该指针的副本。作为回报,调用者承诺在被调用者返回之前指针是有效的(或nullptr)。

        如果你想保留指针,你正在共享它(并且应该使用shared_ptr)。 unique_ptr 管理指针的单个 副本。您使用原始指针(或引用)来引用涉及该对象的调用函数。

        shared_ptr 对象也是如此。 weak_ptr 仅在您想要对指向的对象进行额外引用时才起作用,该对象的寿命超过所涉及的函数。 weak_ptr 的主要目的是打破两个对象相互持有引用(因此永远不会被释放)的引用循环。

        但是请记住,采用shared_ptrweak_ptr 意味着采用该参数的函数将(可选地)修改一些其他对象以保留对指向对象的引用,该引用比函数调用的寿命更长。在绝大多数情况下,您使用原始指针(如果 nullptr 是有效值)或 ref(当值被保证时),即使是 shared_ptr 或 weak_ptr。

        【讨论】:

          猜你喜欢
          • 2021-06-20
          • 2011-01-03
          • 2018-05-22
          • 2013-08-18
          • 1970-01-01
          • 2015-07-23
          • 2016-03-06
          • 2017-12-25
          • 2011-06-26
          相关资源
          最近更新 更多