何时使用shared_ptr以及何时使用原始指针

When to use shared_ptr and when to use raw pointers?

本文关键字:何时使 原始 指针 ptr shared      更新时间:2023-10-16
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<B>还是B*

通常,完全避免在C++代码中使用原始指针代替智能指针是否是一种好的做法?

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

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

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

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

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

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

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

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

  • 当没有共享所有权的转让时,引用或普通指针就足够了。(纯指针比引用更灵活。
  • 当所有权转让但没有共享所有权时,std::unique_ptr<>是一个不错的选择。工厂功能通常就是这种情况。
  • 当有共享所有权时,它是std::shared_ptr<>boost::intrusive_ptr<>的一个很好的用例。

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

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

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

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

最后,如果返回的指针可以在 A 实例的生存期到期后存在,但您不希望指针本身延长 B 的生存期,则可以返回一个 weak_ptr,您可以使用它来测试它是否仍然存在。

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

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

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

指针表明数据可能为空,但是此处A中始终存在B,因此它永远不会为空。引用断言此行为。

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

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


当然,另一个问题是你是否真的需要这样做。良好设计的原则之一是封装。要实现封装,请执行以下操作:

您不得将手柄归还给您的内部(参见得墨忒耳定律(。

所以也许你的问题的真正答案是,与其给出一个引用或指向B的指针,不如只通过A的界面进行修改。

一般来说,我会尽可能避免使用原始指针,因为它们的含义非常模糊 - 你可能不得不释放点,但可能不是,只有人类阅读和编写的文档告诉你情况是什么。文档总是不好、过时或被误解。

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

  1. 您在构造 A 时分配 B。
  2. 你说B不应该在外面坚持一辈子。
    这两者都指向 B 是 A 的成员,而 A 只是返回一个引用访问器。你是否过度设计了这个?

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

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

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

有:

smart pointer, owner<T*>

不拥有:

T*, T&, span<>

所有者<>、span<> 在 GSL 库中定义Microsoft

以下是经验法则:

1(切勿使用原始指针(或不是自己的类型(来传递所有权

2( 智能指针应仅在需要所有权语义时使用

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

4( 使用向量/数组/跨度作为数组

5(在我看来,shared_ptr通常在你不知道谁会发布obj时使用,例如,一个obj被多线程

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

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