什么时候使用shared_ptr和什么时候使用原始指针?


77
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返回ashared_ptr<B>或a B*

通常,是否最好完全避免在C ++代码中使用原始指针来代替智能指针?

我认为,shared_ptr仅在显式转让或所有权共享时才应使用,在函数分配一些内存,用一些数据填充并返回它的情况下,我认为这种情况很少见,并且理解在呼叫者和被呼叫者之间,前者现在对该数据“负责”。


我假设它不可能返回引用而不是指针?
里奇博布2011年

这是个好主意...为了便于讨论,让我们假设必须返回一个指针。
TripShock 2011年

另外,如果我想问一个相关/非常相似的问题,是否可以编辑此问题并将其添加到此处,还是我应该将其作为另一个问题提出?
TripShock 2011年

Answers:


73

我认为您的分析是正确的。在这种情况下,如果该对象保证永远不会为null ,我也将返回裸值B*,甚至返回a [const] B&

在花了一些时间仔细研究智能指针之后,我得出了一些指导原则,这些指导原则告诉我在许多情况下该怎么做:

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

5
会延长寿命”“很少需要std::weak_ptr”听!听!
curiousguy

在最后一个项目符号中,您还假定呼叫者知道或可以控制被呼叫者的寿命。如果调用者没有,则您应该返回智能指针。
Shital Shah

@ShitalShah:如果呼叫者控制了生命周期,请使用std::unique_ptr(或也许我不太了解您的意思)对此进行明确说明。
Alexandre C.

1
我应该从工厂设计模式返回shared_ptr吗?
斯塔夫·阿菲

29

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

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

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


11

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

  • 当没有转移或共享所有权引用或普通指针就足够了。(普通指针比引用更灵活。)
  • 当所有权转移但没有共享所有权时,这std::unique_ptr<>是一个不错的选择。通常具有工厂功能。
  • 如果存在共享所有权,则这是std::shared_ptr<>or的一个很好的用例boost::intrusive_ptr<>

最好避免共享所有权,部分原因是它们在复制方面最昂贵,并且std::shared_ptr<>占用了普通指针的两倍存储,但最重要的是,因为它们有助于在没有明确所有者的情况下进行较差的设计,因此,反过来,会导致无法破坏的对象堆积如山,因为它们相互之间拥有共享的指针。

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


7
请注意,从C ++ 11开始不推荐使用std :: auto_ptr,并且从C ++ 17开始将其删除。
Sydius

“,因为它们彼此拥有共享的指针”“仅当您遇到严重的设计错误时
Curiousguy

6

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

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

最后,在返回的指针可以在A实例的生存期到期之后存在的情况下,但是您不希望指针本身延长B的生存期,那么您可以返回weak_ptr,可用于测试它是否仍然存在。

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


3

我同意你的意见 shared_ptr当发生显式资源共享时,最好使用此,但是还有其他类型的智能指针可用。

在您的确切情况下:为什么不返回引用?

指针表明数据可能为空,但这里总是会有一个BA,所以永远不会为空。该引用断言此行为。

话虽这么说,但我看到有人提倡shared_ptr甚至在非共享环境中使用它并提供weak_ptr句柄,以“保护”应用程序并避免过时的指针。不幸的是,因为你可以恢复shared_ptrweak_ptr(它是实际操作中的数据的唯一途径),这仍然是共同所有权,即使它并不意味着是。

注意:带有一个细微的错误,除非您显式编写一个复制构造函数和一个复制赋值运算符,否则默认情况下shared_ptr,的副本AB与原始副本共享相同的代码。当然,您不会使用原始指针A来容纳a B,您会:)吗?


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

您不得将手柄交还给您的内部零件(请参阅《德米特律》)。

因此,对您问题的真正答案可能是,不应放弃B对它的引用或指针,而应仅通过A的界面对其进行修改。


你的笔记是一个微妙的笑话吗?如果是这样,那么对我来说太微妙了,如果不是,那么它与原始指针的错误相同(除了两次删除或原始指针的内存泄漏)
stefaanv

@stefaanv:请注意,在OP问题中,数据成员不一定是动态分配的。仅当使用解决方案时,才必须动态分配它shared_ptr,但是,当返回引用时,它完全可以是常规属性,并且常规属性不会浅表复制。
Matthieu M.

@Matthieu,好的,那条纸条对我来说还不是很清楚。(OP的示例清楚地显示了动态分配,他的问题是关于共享或原始指针的)
stefaanv 2011年

@stefaanv:OP显示了一个动态指针,因此必然是一个动态分配,我觉得这个问题仅涉及返回的内容gimmeB(即使响应可能会影响内部表示)。我会尽力澄清说明。
Matthieu M.

顺便说一句:您的加法+1:问题1:我真的必须公开内部结构吗?
stefaanv 2011年

2

通常,我将尽可能避免使用原始指针,因为它们具有非常含糊的含义-您可能必须取消分配指针,但可能不会,并且只有人工阅读和编写的文档才能告诉您情况是什么。而且文档总是不好的,过时的或被误解的。

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


2
  1. 您在结构A处分配B。
  2. 您说B不应在As生存期之外继续存在。
    这些都指向B是A的成员,并且只是返回引用访问器。您是否对此工程过度了?

2

我发现《 C ++核心准则》针对此问题给出了一些非常有用的提示:

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

拥有 :

smart pointer, owner<T*>

不属于:

T*, T&, span<>

owner <>,span <>在Microsoft GSL库中定义

这是经验法则:

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

2)仅当打算使用所有权语义时才应使用智能指针

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

4)使用向量/数组/跨度数组

5)不知所措,shared_ptr通常在您不知道谁将释放obj的情况下使用,例如,多线程使用一个obj


1

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


返回aweak_ptr并不能真正解决问题“类的用户会认为可以延长B的生命周期”,因为他们可以将weak_ptr锁定在销毁A的位置,因此他们仍然可以延长B的生命。但是,这可能会让他们考虑,当然,如果在A被销毁时碰巧没有锁定它,那么B也可以走。
史蒂夫·杰索普

0

当您说:“假设B是在A的生存期之外在语义上不应存在的类”

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

当客户有直接指向A的指针时,您必须相信他们会适当地处理它。不要尝试将其存储等

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.