【问题标题】:Why does std::enable_shared_from_this allow multiple std::shared_ptr instances?为什么 std::enable_shared_from_this 允许多个 std::shared_ptr 实例?
【发布时间】:2019-06-05 17:44:12
【问题描述】:

有几个问题涵盖了std::enable_shared_from_this 的行为,但我不认为这是重复的。

std::enable_shared_from_this 继承的类带有std::weak_ptr 成员。当应用程序创建一个指向std::enable_shared_from_this 子类的std::shared_ptr 时,std::shared_ptr 构造函数会检查std::weak_ptr,如果它没有被初始化,则初始化它并使用std::weak_ptr 控制块来控制std::shared_ptr .但是,如果 std::weak_ptr 已经初始化,构造函数只会创建一个带有新控制块的新 std::shared_ptr。当两个std::shared_ptr 实例之一的引用计数变为零并删除底层对象时,这会使应用程序崩溃。

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);

// Okay, p1 and p2 both have ref count = 2
std::shared_ptr<C> p2 = p->shared_from_this();

// Bad: p3 has ref count 1, and C will be deleted twice
std::shared_ptr<C> p3(p);

我的问题是:为什么图书馆会这样?如果std::shared_ptr 构造函数知道该对象是std::enable_shared_from_this 子类并费心检查std::weak_ptr 字段,为什么它不总是为新的std::shared_ptr 使用相同的控制块,从而避免潜在的崩溃?

那么,为什么 shared_from_this 方法在 std::weak_ptr 成员未初始化时会失败,而不是仅仅初始化它并返回 std::shared_ptr

这个库的工作方式似乎很奇怪,因为它在很容易成功的情况下失败了。我想知道是否有我不理解的设计注意事项/限制。

我在 C++17 模式下使用 Clang 8.0.0。

【问题讨论】:

  • 对于第二个问题,初始化一个新的std::shared_ptr 而不是在shared_from_this 上失败,您不能假设当前是如何管理对象的生命周期的。除非它由用户不会delete 的原始指针拥有,否则实际上不会有任何好的结果。例如,如果我尝试使用自动存储持续时间的 shared_from_this 会发生什么?
  • the std::shared_ptr constructor checks the std::weak_ptr, and if it is not initialized, initializes it and uses the std::weak_ptr control block for the std::shared_ptr 我不认为这是准确的。 cppreference 声明 weak_ptr 已初始化值。我认为这意味着没有进行检查。因此,添加检查会增加每个不需要检查的构造的成本。
  • @FrançoisAndrieux 在关于对象的观点上足够公平,没有假设 this 是一个可以/应该从中创建共享指针的指针。
  • 拥有多个指向“同一个对象”的智能指针(无论这意味着什么)本质上并不是一个坏主意。

标签: c++ c++17 shared-ptr weak-ptr enable-shared-from-this


【解决方案1】:

如果我正确理解您的问题,您会认为第二次调用构造函数shared_ptr 会在逻辑上重用存储在 shared_from_this 中的控制块。

从您的角度来看,这看起来合乎逻辑。让我们暂时假设C 是您正在维护的库的一部分,而C 的使用是您库用户的一部分。

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Valid given your assumption

现在,您找到了一种不再需要 enable_shared_from_this 的方法,并且在您的库的下一个版本中,它会更新为:

struct C {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Now a bug

突然,由于升级了库,完全有效的代码在没有任何编译器错误/警告的情况下变得无效。应尽可能避免这种情况。

同时,它会引起很多混乱。原因取决于您放入 shared_ptr 的类的实现,它是已定义或未定义的行为。每次都将其设为 undefined 会更容易混淆。

enable_shared_from_this 是在没有shared_ptr 的情况下获取shared_ptr 的标准解决方法。一个经典的例子:

 struct C : std::enable_shared_from_this<C>
 {
     auto func()
     {
         return std::thread{[c = this->shared_from_this()]{ /*Do something*/ }};
     }

     NonCopyable nc;
 };

添加您提到的额外功能确实会在您不需要时添加额外代码,仅用于检查。不过,这并不重要,零开销抽象并不是几乎零开销抽象。

【讨论】:

  • 谢谢,这很清楚。您是说从同一个原始指针创建两个 std::shared_ptr 实例通常是不良/未定​​义的行为,因此它不应该仅仅因为底层对象恰好属于某个魔术类而突然开始工作。有道理。
  • 是的,确实如此。就其本身而言并不是什么大问题,但是,如果您需要维护一个大型代码库,其中一个基类会更改语义,这将成为一个令人头疼的问题。
  • 许多小的更改可能会悄悄地破坏使用enable_shared_from_this 的功能,特别是更改为私有继承(许多人认为这是一个实现细节),如果您让 stdlib 函数的朋友来修复上一期,把一个对象(用make_shared创建)变成一个成员子对象...
  • @curiousguy 正是我的观点。让标准库成为朋友不是一个好主意,所以这是我最不害怕的情况之一
【解决方案2】:

这不是对问题的回答,而是对用户 jvapen 的回复,基于他对这个问题的回答。

您在回答中已经说明了这一点:

struct C {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Now a bug

我在这里没有看到第 5 行 std::shared_ptr&lt;C&gt; p3(p); 现在是一个错误。根据cppreference:shared_ptr,他们明确指出:

std::shared_ptr 是一个智能指针,它通过指针保持对象的共享所有权。多个shared_ptr 对象可能拥有同一个对象。

【讨论】:

  • 如果您继续阅读,您还会看到:“对象的所有权只能通过复制构造或将其值复制分配给另一个 shared_ptr 与另一个 shared_ptr 共享。使用另一个 shared_ptr 拥有的原始底层指针会导致未定义的行为。”
  • @WillisBlackburn 谢谢你需要澄清一下!
  • 权重:您只能转让一次所有权。这在使用 unique_ptr 作为所有权时会更加明显,因为您必须移动它。这就是为什么普遍的共识是尽可能用make_unique 替换new/delete。
【解决方案3】:

为同一个指针创建两个shared_ptrs 是未定义的行为,与std::enable_shared_from_this 无关。你的代码应该是:

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);

std::shared_ptr<C> p2 = p->shared_from_this();

std::shared_ptr<C> p3(p1);

【讨论】:

  • 问题是为什么该语言没有可以让 OP 代码工作的功能,基于 C 知道它已经拥有一个 std::shared_ptr。 OP 明确表示他们知道他们的代码有问题。
  • @FrançoisAndrieux:“OP 明确表示他们知道他们的代码有问题。”我不同意。您通常不会问为什么您的有问题的代码没有变得没有问题。问题的存在至少表明 OP 认为他们的代码应该特别有问题。
  • @FrançoisAndrieux:但是......代码是错误的。这就是答案:不允许这样做,因为用户这样做是在做错事,所以我们不会让用户做错事。即使它可以被“修复”,“修复”就是说不连贯的代码不是不连贯的。
  • @NicolBolas 问题是为什么 enable_shared_from_this 会这样。问题中解释了代码不好的事实以及原因。重申这几乎不是答案。
  • @FrançoisAndrieux “类似的人会回答“我的手机掉了摔坏了,为什么公司不生产更坚固的手机?””我会这么说一个更好的类比是“我把手机放在我的汽车轮胎前面,然后加速,它坏了。为什么公司不生产可以开车的手机?”这里的答案的重点是您不应该能够做这些事情。他们不被允许,因为他们应该被允许。
【解决方案4】:

创建一个实际上不拥有的辅助智能指针(当最后一个副本被重置/销毁时它什么都不做),或者在控制块中(在删除器对象中)携带原始智能指针的副本,这样当次要引用计数变为零主要引用计数减少,这是一种非常罕见的情况,可能会让大多数程序员感到困惑,但它本质上并不是非法的。 (而且我认为在特殊情况下可以为这种模式提供强有力的理由。)

另一方面,shared_from_this 的存在强烈表明只有一个拥有shared_ptr,因此当预计会有多组std::shared_ptr 时,可能应该避免拥有shared_from_this。与 std::enable_shared_from_this 的隐式行为不同,对自引用非拥有指针的显式管理更安全,因为它使此类问题在用户代码中显而易见。

【讨论】:

  • “我认为在特殊情况下可以为这种模式提供强有力的理由。”你能举一些例子吗?
  • 一个简单的例子是当一个对象永远不会被销毁(或在程序终止之前)并且您想要创建指向它的非拥有智能指针;创建多个独立的shared_ptr 显然并不比只创建一个差。
  • 答案的第 1 段实际上包含一些:“智能指针,它要么实际上是非拥有的(当最后一个副本被重置/销毁时它什么都不做),要么携带控制块中的原始智能指针(在删除器对象中),这样当辅助引用计数变为零时,主引用计数就会减少“(现在你可以完全找到这些想法 bizarre 和甚至认为它们是反模式。这是主观的。)
  • 我正在寻找使用 shared-pointers-to-shared-pointers 或“非拥有”共享指针的代码示例,以及为什么需要这样使用的一些解释。你认为这样的使用可能会令人困惑和/或奇怪,但我可以为使用它们提供一个强有力的案例。我想知道那个强有力的案例会是什么。
  • @WillisBlackburn 当您使用采用const shared_ptr&lt;T&gt;&amp;(或shared_ptr&lt;T&gt;)并按顺序复制它的库组件时,需要非拥有共享拥有指针以延长托管的生命周期对象。一个非拥有对象当然不能延长任何东西的生命周期,但对于一个永远不会被破坏(或仅在程序终止时)的全局对象是可以的。你告诉库它可以延长对象的生命周期,而实际上它不能,但这没关系。
猜你喜欢
  • 2017-06-11
  • 1970-01-01
  • 2015-10-06
  • 1970-01-01
  • 2015-07-23
  • 2011-08-20
  • 2017-08-15
相关资源
最近更新 更多