【问题标题】:Returning Smart Pointers返回智能指针
【发布时间】:2014-05-30 01:35:33
【问题描述】:

假设我有一个管理器类,其中包含某个对象的向量:

class SomeObjectManager
{
private:
    std::vector<SomeObject> _heldObjects;
};

在那个类中,我有一些函数可以遍历所述向量以返回请求的对象。

SomeObject getSomeObjectByName(std::string nameToFind);

我需要知道什么时候适合使用智能指针。我真的应该返回类似下面的东西吗?

std::shared_ptr<SomeObject> getSomeObjectByName(std::string nameToFind);

或者我应该使用其他类似 unique_ptr 或 weak_ptr 的东西吗?我希望 SomeObjectManager 类拥有返回的实际对象,并且永远不会说 SomeObject 被删除,除非 Manager 这样做。

我在 C# 模式下呆了一段时间后,最近才回到 C++ 世界;感谢您的帮助并消除了我的困惑。

我已经阅读了很多关于此问题的信息,但从未真正找到针对我的具体情况的直接答案。


编辑#1

我想用这个来改写我最后几句话:

我希望 SomeObjectManager 类拥有正在返回的实际对象,并且从未说过 SomeObject 从向量中删除并随后被删除,超出范围,直到 Manager 强制它这样做。例如:

void SomeObjectManager::removeSomeObjectByName(const std::string& objectToRemove);

这只会遍历向量,找到SomeObject,然后将其从向量中删除。

【问题讨论】:

  • 阅读const 并通过引用传递的好主意(&amp; 运算符)
  • 是的,我一直在阅读const correctness。缓慢而坚定地试图重新站稳脚跟。

标签: c++ shared-ptr smart-pointers unique-ptr resource-management


【解决方案1】:

由于SomeObjectManagerSomeObject 实例(存储在其std::vector 数据成员中)的所有者,我只返回原始 指针,因为它们实际上是在观察指针。

std::vector<SomeObject> _heldObjects;

SomeObject* getSomeObjectByName(const std::string& nameToFind) {
    ... find index of object corresponding to 'nameToFind'
    return &_heldObjects[foundIndex];
}

(请注意,我使用 引用 const 传递了 nameToFind,因为我假设 nameToFind 是输入字符串,所以如果在方法内部,您只是 观察 那个字符串,你可以避免使用const &amp; 进行深度复制。

拥有原始指针时必须注意(它们应该被包裹在安全的 RAII 边界内),但是观察原始指针是可以的。

只需确保SomeObjectManager 的生命周期超过其客户端的生命周期,以确保客户端引用的是有效对象。

还请注意,如果您将新项目添加到向量数据成员(例如,使用 std::vector::push_back()),则存储在向量中的先前 SomeObject 实例的 地址 可能会更改。所以,如果你把指针指向外面的那些,它们就会失效。

因此,在将指向其元素的指针指向外部客户端代码之前,请确保矢量大小和矢量内容未更改。

另一种方法是将std::vector&lt;std::unique_ptr&lt;SomeObject&gt;&gt; 作为数据成员。在这种情况下,即使调整了向量的大小,您使用智能指针(尤其是使用std::unique_ptr::get())返回的地址仍然有效:

std::vector<std::unique_ptr<SomeObject>> _heldObjects;

SomeObject* getSomeObjectByName(const std::string& nameToFind) {
    ... find index of object corresponding to 'nameToFind'
    return _heldObjects[foundIndex].get();
}

PS
另一种选择可能是将references返回到const SomeObject(假设const 的这种使用在您的设计中有意义):

std::vector<SomeObject> _heldObjects;

const SomeObject& getSomeObjectByName(const std::string& nameToFind) const {
    ... find index of object corresponding to 'nameToFind'
    return _heldObjects[foundIndex];
}

【讨论】:

  • 好吧,我需要在const 上阅读更多内容,但我还是会问的;不会返回 const SomeObject 使返回的对象不可变?防止修改对象的任何/所有方面?例如; someObjInst.setTitle("something new")。如果我错了,请纠正我。
  • @Mister:按值返回的问题(const SomeObjectSomeObject 会类似)是这会生成一个 copy(深拷贝) SomeObject 您返回的实例。相反,我认为您想给调用者一个 reference 到存储在 std::vector 数据成员中的 original SomeObject 实例。为此,您可以使用 pointersreferences
  • 好吧,这是有道理的,最后一个问题是const SomeObject&amp; 不是对const SomeObject 的引用吗?根据我的需要,我只想要SomeObject&amp; 不是吗?
  • 当您返回const SomeObject&amp; 时,您正在返回一个引用,但引用的对象无法在调用站点进行修改。如果您传递SomeObject&amp;,您将在std::vector 数据成员中为原始SomeObject 实例添加别名,因此它甚至可以重新分配给其他值。我不知道你的设计,但这真的是你想要的吗?
  • 不,这不是我想要的效果,但我认为SomeObject&amp; 也会返回一个参考;我这样想错了吗?另外,我认为引用不能重新分配。
【解决方案2】:

如果您的程序在单线程中运行,那么如果您有足够的纪律,您最擅长返回原始指针或对存储在vector 中的对象的引用。

由于管理器私有地拥有vector 和其中的对象,因此可以控制何时删除对象,您可以确保不存在指向已删除对象的无效指针(这不是自动保证的!) .
基本上,管理器只有在知道没有人持有对该对象的引用时才必须删除对象,例如仅在明确定义的不同时间执行此操作(例如在程序结束时,或者当它知道没有消费者存在时,等等)。
引用计数是这样做的一种方式,这也是shared_ptr 在内部所做的(嗯,不...严格按照规范的字母,不需要引用计数,只是可见行为已定义,但实际上所有实现都这样做)。

“删除”对象的过程因此只会减少引用计数器(很像托管语言),并且当引用计数器达到零时,对象将真的不复存在。 很可能,但当您“删除”它时不一定立即发生这种情况。在对象实际被销毁之前可能还需要一些时间。
这种方法“自动”工作,无需大量勤奋和严格的纪律,只需将对象的shared_ptrs 存储在向量中并返回shared_ptrs 或weak_ptrs 即可实现。

等一下!如果你也可以返回一个shared_ptr,那为什么还有weak_ptrs?他们在逻辑上和实践上做不同的事情。 shared_ptrs 拥有(至少部分),weak_ptrs 没有。此外,weak_ptrs 的复制成本更低。

多线程程序中,存储shared_ptrs 并返回weak_ptr 是唯一安全的方法。 weak_ptr 不拥有该资源,因此无法阻止管理器删除该对象,但它为持有者提供了一种可靠且明确的方式来了解资源是否有效并且资源将保持有效 当你使用它时。

您返回 weak_ptr,当消费者真正想要使用该对象时,它会将 weak_ptr 转换为临时的 shared_ptr。这将失败(给出一个空指针),因此消费者知道该对象已被删除,并且它可能不会使用它。或者,它会成功,现在消费者有一个 valid 指针,该指针具有一个对象的共享所有权,现在保证在使用它时保持有效

在“有效”和“无效”之间没有任何东西,没有猜测,也没有什么会失败。如果您成功转换为有效的临时 shared_ptr,那么您就可以开始了。否则,对象就消失了,但你知道
就安全性而言,这是一个很大的优势。即使管理器在您使用它时“删除”了该对象,您的程序也不会崩溃或产生垃圾,该对象在您停止使用之前一直有效!

可以说,这有点模糊了“管理器在选择删除对象时删除对象”的范式,但它确实是安全执行此操作的唯一方法。管理器仍然控制要删除哪些对象,它只能在对象正在使用时立即删除它(这可能会导致可怕的灾难)。但是,它可以随时通过删除其shared_ptr 并因此减少引用计数来安排下一次可能的删除。

唯一明显的阻碍是对象必须立即销毁(因为析构函数具有必须立即立即发生的副作用)。但是在这种情况下,要获得并发访问权是非常困难的(一场噩梦!)。幸运的是,这也是一种非常罕见的情况。

【讨论】:

  • 读完后我觉得我的设计可能有缺陷。我当然不想强制删除一个对象,如果某事应该要求管理器按名称删除一个对象,只需将其从向量中删除即可。
  • 删除没有错,只要你不删除对象,而另一部分代码希望能够通过它持有的指针使用该对象。引用计数(或分发弱指针)在这方面非常好,因为您不必担心。而且由于您明确表示您不想“强制删除”,因此您对这些非常满意。他们就是这样做的,一种“软删除”或“删除计划”。
【解决方案3】:

从您的函数返回对 SomeObject 的引用(或常规指针)。只要引用保留在向量中,并且向量未重新分配(注意这一点,可能使用列表代替或 unique_ptr 的向量),该引用就有效。当从向量中移除时,该对象已失效,并且对它的所有引用都不再有效。 (再次小心删除中间的元素)

【讨论】:

  • 如果他来自 C#,这听起来很危险。悬空指针有人吗?
  • @Bgie 这只是 C++。
【解决方案4】:

如果您没有将对象存储为 std::shared_ptr,则返回 std::shared_ptr 没有任何意义。甚至不知道你将如何做到这一点。我认为没有办法将已经存在的指针包装在智能指针中。如果那里已经有数据,则只需返回一个常规的 const 指针即可。这样您就可以避免复制对象内容所需的开销。

【讨论】:

    【解决方案5】:

    在这种情况下,您可以选择使用 shared_ptrweak_ptr。您使用哪个取决于您想要的对象的生命周期。

    如果您只希望对象有效,而 SomeObjectManager 具有对它的引用并且客户端当时正在使用它,则使用 weak_ptr。如果您希望在 SomeObjectManager 有引用并且客户端存储对它的引用时引用保持有效。

    这是一个带有weak_ptr 的示例。

    std::weak_ptr<SomeObject> weakref = getSomeObject();   
    // weakref will not keep the object alive if it is removed from the object manager.
    
    auto strongref = weakref.lock();
    if ( strongref ) {
         // strongref is a shared_ptr and will keep the object alive until it 
         // goes out of scope.
    }
    

    这在多线程环境中很有用,因为shared_ptr 引用计数访问是线程安全的。但是,这确实意味着客户端可以将对象的生命周期延长得比您希望的要长。

    【讨论】:

      【解决方案6】:

      如果你想使用智能共享指针,向量本身应该使用智能指针。

      class SomeObjectManager
      {
      private:
          std::vector<std::shared_ptr<SomeObject> > _heldObjects;
      };
      

      但是你是安全的。

      【讨论】:

      • “我希望 SomeObjectManager 类拥有被返回的实际对象,并且永远不会说 SomeObject 被删除,除非 Manager 这样做。”使用共享指针,管理器将失去在需要时删除对象的所有能力。
      • @juanchopanza:嗯,不,它没有。它失去了以不安全的方式立即删除对象的能力,但它保留了安全地删除对象并使用并发阅读器的能力。实际上,在一个多线程程序中,这个分发weak_ptrs 的shared_ptr 构造将是优选的实现(实际上是唯一安全的实现)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-13
      • 2017-04-29
      相关资源
      最近更新 更多