【问题标题】:When to use shared_ptr and when to use raw pointers?何时使用 shared_ptr 以及何时使用原始指针?
【发布时间】:2011-10-05 06:56:47
【问题描述】:
class B;

class A
{
public:
    A ()
        : m_b(new B())
    {
    }

    shared_ptr<B> GimmeB ()
    {
        return m_b;
    }

private:
    shared_ptr<B> m_b;
};

假设 B 是一个在语义上不应该存在于 A 生命周期之外的类,即 B 本身存在绝对没有意义。 GimmeB 应该返回 shared_ptr&lt;B&gt; 还是 B*

一般来说,完全避免在 C++ 代码中使用原始指针来代替智能指针是一种好习惯吗?

我认为 shared_ptr 应该只在明确转让或共享所有权时使用,我认为在函数分配一些内存、填充一些数据并返回的情况之外,这种情况非常罕见它,并且调用者和被调用者之间的理解是前者现在对该数据“负责”。

【问题讨论】:

  • 我假设它不可能返回引用而不是指针?
  • 这是一个很好的观点......为了讨论,让我们假设必须返回一个指针。
  • 另外,如果我想问一个相关/非常相似的问题,是否可以编辑这个并添加到这里,或者我应该作为另一个问题提出?

标签: c++ smart-pointers


【解决方案1】:

我认为你的分析很正确。在这种情况下,如果保证对象永远不会为空,我也会返回一个裸露的B*,甚至返回一个[const] B&amp;

在仔细阅读智能指针之后,我得出了一些指导方针,这些指导方针告诉我在许多情况下应该做什么:

  • 如果您返回一个其生命周期将由调用者管理的对象,则返回std::unique_ptr。调用者可以根据需要将其分配给std::shared_ptr
  • 返回std::shared_ptr实际上是非常罕见的,当它有意义时,它通常是显而易见的:你向调用者表明它将延长指向对象的生命周期超过最初维护的对象的生命周期资源。从工厂返回共享指针也不例外:您必须这样做,例如。当你使用std::enable_shared_from_this
  • 您很少需要std::weak_ptr,除非您想了解lock 方法。这有一些用途,但很少见。在您的示例中,如果从调用者的角度来看,A 对象的生命周期不是确定性的,则需要考虑这一点。
  • 如果返回对调用者无法控制其生命周期的现有对象的引用,则返回裸指针或引用。通过这样做,你告诉调用者一个对象存在并且她不必关心它的生命周期。如果您不使用 nullptr 值,则应返回参考。

【讨论】:

  • "会延长寿命" "很少需要std::weak_ptr" 听说!听!
  • 在最后一个项目符号中,您还假设调用者知道或可以控制被调用者的生命周期。如果调用者没有或不能,那么你应该返回智能指针。
  • @ShitalShah:如果调用者控制寿命,请使用std::unique_ptr 明确说明(或者我可能不太明白您的意思)。
  • 我应该从工厂设计模式返回 shared_ptr 吗?
  • @AlexandreC。 “如果您返回一个其生命周期将由调用者管理的对象,则返回 std::unique_ptr。” 您能否更详细地解释一下?一个简单的例子可能就清楚了。谢谢。
【解决方案2】:

问题“我什么时候应该使用shared_ptr,什么时候应该使用原始指针?”有一个非常简单的答案:

  • 当您不想将任何所有权附加到指针时,请使用原始指针。这项工作通常也可以通过参考来完成。原始指针也可用于一些低级代码(例如用于实现智能指针或实现容器)。
  • 如果您想要对象的唯一所有权,请使用unique_ptrscope_ptr。这是最有用的选项,应该在大多数情况下使用。唯一所有权也可以通过简单地直接创建对象来表达,而不是使用指针(如果可以的话,这甚至比使用unique_ptr 更好)。
  • 如果您希望共享指针所有权,请使用shared_ptrintrusive_ptr。这可能会令人困惑且效率低下,并且通常不是一个好的选择。共享所有权在一些复杂的设计中可能很有用,但一般应避免,因为它会导致代码难以理解。

shared_ptrs 执行与原始指针完全不同的任务,shared_ptrs 和原始指针都不是大多数代码的最佳选择。

【讨论】:

    【解决方案3】:

    以下是一个很好的经验法则:

    • 当没有共享所有权的转移引用或纯指针就足够了。 (普通指针比引用更灵活。)
    • 当所有权转移但没有共享所有权时,std::unique_ptr&lt;&gt; 是一个不错的选择。工厂函数通常是这种情况。
    • 当存在共享所有权时,std::shared_ptr&lt;&gt;boost::intrusive_ptr&lt;&gt; 是一个很好的用例。

    最好避免共享所有权,部分原因是它们在复制方面最昂贵,std::shared_ptr&lt;&gt; 占用了普通指针的两倍存储空间,但最重要的是,它们有利于糟糕的设计没有明确的所有者,这反过来又导致无法销毁的对象的毛球,因为它们彼此持有共享指针。

    最好的设计是建立明确的所有权并且是分层的,因此,理想情况下,根本不需要智能指针。例如,如果有一个工厂创建唯一对象或返回现有对象,那么工厂拥有它创建的对象并将它们按值保存在关联容器中(例如std::unordered_map)是有意义的,这样它可以返回简单的指针或引用给它的用户。该工厂的生命周期必须在其第一个用户之前开始并在其最后一个用户之后结束(分层属性),因此用户不可能拥有指向已销毁对象的指针。

    【讨论】:

    • 请注意,std::auto_ptr 自 C++11 起已弃用,自 C++17 起将被删除。
    • "因为它们彼此持有共享指针" 仅当您有严重的设计错误时
    【解决方案4】:

    如果您不希望 GimmeB() 的被调用者能够通过在 A 的实例死亡后保留 ptr 的副本来延长指针的生命周期,那么您绝对不应该返回 shared_ptr。

    如果被调用者不应该长时间保留返回的指针,即不存在 A 的实例在指针的生命周期之前到期的风险,那么原始指针会更好。但更好的选择是简单地使用引用,除非有充分的理由使用实际的原始指针。

    最后如果返回的指针在A实例的生命周期结束后还能存在,但又不希望指针本身延长B的生命周期,那么你可以返回一个weak_ptr,你可以用于测试是否还存在。

    最重要的是,通常有比使用原始指针更好的解决方案。

    【讨论】:

      【解决方案5】:

      我同意您的观点,即在发生显式共享资源时最好使用 shared_ptr,但是还有其他类型的智能指针可用。

      在您的具体情况下:为什么不返回参考?

      一个指针表明数据可能为空,但是在您的A 中总会有一个B,因此它永远不会为空。引用断言了这种行为。

      话虽如此,我看到有人提倡即使在非共享环境中使用shared_ptr,并给予weak_ptr 句柄,以“保护”应用程序并避免过时的指针。不幸的是,由于您可以从 weak_ptr 中恢复 shared_ptr(这是实际操作数据的唯一方法),因此即使不是有意的,这仍然是共享所有权。

      注意:shared_ptr 有一个细微的错误,A 的副本将默认与原始共享相同的B,除非您明确编写复制构造函数和复制赋值运算符。当然,您不会在A 中使用原始指针来保存B,对吗:)?


      当然,另一个问题是您是否真的需要这样做。良好设计的原则之一是封装。实现封装:

      您不得将句柄返回到您的内部(请参阅Law of Demeter)。

      所以也许对您的问题的真正答案是,与其放弃对B 的引用或指针,不如只通过A 的界面对其进行修改。

      【讨论】:

      • 你的笔记是个微妙的玩笑吗?如果是这样,对我来说太微妙了,如果不是,那么它与原始指针的错误相同(除了双重删除或原始指针的内存泄漏)
      • @stefaanv:请注意,在OP问题中,数据成员不一定是动态分配的。它必须仅在使用shared_ptr 解决方案时动态分配,但是当返回引用时,它可以完美地成为常规属性并且常规属性不会被浅拷贝。
      • @Matthieu,好的,那么我对这个注释并不是很清楚。 (OP 的示例清楚地显示了动态分配,他的问题是关于共享或原始指针)
      • @stefaanv:OP 显示了一个动态指针,因此必然是一个动态分配和问题,我觉得,我只关心从 gimmeB 返回什么(即使响应可能会影响内部表示)。我会尽量澄清说明。
      • 顺便说一句:为您的补充 +1:问题 1:我真的必须公开内部吗?
      【解决方案6】:

      一般来说,我会尽可能避免使用原始指针,因为它们的含义非常模糊 - 您可能必须解除指针对象的分配,但也可能不需要,只有人工阅读和编写的文档会告诉您具体情况。而且文档总是不好的、过时的或被误解的。

      如果所有权是一个问题,请使用智能指针。如果没有,我会在可行的情况下使用参考。

      【讨论】:

        【解决方案7】:
        1. 您在构建 A 时分配 B。
        2. 你说 B 不应该在 As 生命周期之外持续存在。
          这两个都指向 B 是 A 的成员并且 a 只是返回一个引用访问器。您是否对此进行了过度设计?

        【讨论】:

          【解决方案8】:

          我发现 C++ 核心指南为这个问题提供了一些非常有用的提示:

          使用原始指针(T*)还是更智能的指针取决于谁拥有对象(谁负责释放 obj 的内存)。

          自己的:

          smart pointer, owner<T*>
          

          不拥有:

          T*, T&, span<>
          

          owner, span 在 Microsoft GSL 库中定义

          这里是经验法则:

          1) 永远不要使用原始指针(或不是自己的类型)来传递所有权

          2) 智能指针只应在使用所有权语义时使用

          3) T* 或所有者指定单个对象(仅)

          4) 对数组使用vector/array/span

          5)我不理解,shared_ptr通常用在你不知道谁会释放obj的时候,比如一个obj被多线程使用

          【讨论】:

            【解决方案9】:

            避免使用原始指针是一种很好的做法,但您不能只用shared_ptr 替换所有内容。在示例中,您的类的用户会假设可以将 B 的生命周期延长到 A 的生命周期之外,并且可能出于自己的原因决定将返回的 B 对象保留一段时间。您应该返回一个weak_ptr,或者,如果在 A 被销毁时 B 绝对不存在,则返回一个对 B 的引用或只是一个原始指针。

            【讨论】:

            • 返回weak_ptr 并不能真正解决“您的班级的用户会认为可以延长 B 的生命周期”的问题,因为他们可以将 weak_ptr 锁定在 A 被销毁的位置,所以他们仍然可以延长 B 的寿命。不过,这可能会让他们考虑一下,当然,如果他们在 A 被销毁时碰巧没有锁定它,那么 B 也可以去。
            【解决方案10】:

            当你说:“假设 B 是一个在语义上不应该存在于 A 生命周期之外的类”

            这告诉我,如果没有 A,B逻辑上不应该存在,但是物理存在呢? 如果您可以确定没有人会尝试在 A dtors 之后使用 *B ,那么原始指针可能会很好。否则,更智能的指针可能是合适的。

            当客户有一个指向 A 的直接指针时,您必须相信他们会妥善处理它;不要尝试删除它等。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-01-19
              • 1970-01-01
              • 1970-01-01
              • 2021-11-15
              • 2017-11-03
              • 2011-07-01
              • 2015-02-18
              • 1970-01-01
              相关资源
              最近更新 更多