为什么 std::enable_shared_from_this 允许多个 std::shared_ptr 实例?

Why does std::enable_shared_from_this allow multiple std::shared_ptr instances?

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

std::enable_shared_from_this 继承的

类 带有一个 std::weak_ptr 成员。当应用程序创建一个指向[=13=的子类的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

库的工作方式似乎很奇怪,因为它在很容易成功的情况下失败了。我想知道是否有我不明白的设计considerations/limitations。

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

为同一个指针创建两个 shared_ptr 是未定义的行为,与 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);

如果我正确理解你的问题,你会假设第二次调用构造函数 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

突然间,完全有效的代码在没有任何编译器的情况下变得无效 error/warning,因为升级了你的库。应尽可能避免这种情况。

同时,也会造成很多混乱。原因取决于您在 shared_ptr 中输入的 class 的实现,它是已定义或未定义的行为。每次都将其设置为 undefined 会更容易混淆。

如果您没有 shared_ptr

enable_shared_from_this 是获得 shared_ptr 的标准解决方法。一个 classic 示例:

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

     NonCopyable nc;
 };

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

这不是问题的答案,而是基于用户 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<C> p3(p); 现在是一个错误。根据 cppreference:shared_ptr 他们特别声明:

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object.

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

另一方面,shared_from_this 的存在强烈表明只有一个人拥有 shared_ptr,因此当有多组 [=13] 时,可能应该避免拥有 shared_from_this =] 是预期的。与 std::enable_shared_from_this.

的隐式行为不同,自引用非拥有指针的显式管理更安全,因为它使此类问题在用户代码中显而易见