利用`std::weak_ptr`实现自杀对象

13

我正在考虑在游戏中使用“自杀对象”来模拟实体,即可以删除自身的对象。通常的C++03实现(简单地delete this)对于可能引用自杀对象的其他对象不起作用,这就是为什么我要使用std::shared_ptrstd::weak_ptr

现在是代码转储:

#include <memory>
#include <iostream>
#include <cassert>

struct SuObj {
    SuObj() { std::cout << __func__ << '\n'; }
    ~SuObj() { std::cout << __func__ << '\n'; }

    void die() {
        ptr.reset();
    }

    static std::weak_ptr<SuObj> create() {
        std::shared_ptr<SuObj> obj = std::make_shared<SuObj>();
        return (obj->ptr = std::move(obj));
    }

private:

    std::shared_ptr<SuObj> ptr;
};

int main() {
    std::weak_ptr<SuObj> obj = SuObj::create();

    assert(!obj.expired());
    std::cout << "Still alive\n";

    obj.lock()->die();

    assert(obj.expired());
    std::cout << "Deleted\n";

    return 0;
}

问题

这段代码似乎运行良好。然而,我希望有其他人的眼睛来评估它。这段代码是否有意义?我是否盲目地进入了未知的领域?我应该放下键盘立即开始艺术学习吗?

我希望这个问题对于 Stack Overflow 已经足够窄化了。对于 CR 来说,似乎有点微小和低级。

小精度

我不打算在多线程代码中使用它。如果需要,我一定会重新考虑整个事情。


对我来说看起来没问题。你可能想通过将obj.lock分配给一个shared_ptr并确保在那个时刻它没有过期,来添加另一个测试,但我不认为这会有任何区别。 - Mark Ransom
我认为die()方法指的是游戏角色实际上“死亡”,比如飞船被炸成碎片之类的。例如:if (spaceship->hitpoints() <= 0) spaceship->die() - Jason
Chromium 代码库中有一种名为 WeakPointerFactory 的对象,正好是您想实现的内容。他们使用自己的 shared_ptrweak_ptr,所以您无法直接使用该代码。然而,该代码可能会给您在设计方面提供一些启示。 - Mateusz Kubuszok
@MikaelPersson 对象应该几乎立即死亡,因为不应该有任何被遗忘的锁定指针(否则就是一个bug)。 shared_ptr确实过于复杂了,因为我仅使用其功能的一半,但我现在不确定是否想要自己实现它(肯定不是为了一个POc)谢谢您的文章,它肯定会很有用! - Quentin
@MikaelPersson 我认为我是第三种情况,因为任何人都可以销毁对象。因此它不是唯一的所有权,但也不是共享所有权,因为没有人可以将其保持活动状态。对于那些流浪的 shared_ptr,将锁定的 shared_ptr 包装在一个不可复制、不可移动、不可操作的对象中怎么样? - Quentin
显示剩余6条评论
3个回答

3
当您使用基于shared_ptr的对象生命周期时,您的对象的生命周期是共同拥有它的shared_ptr联合体的“生命周期”。
在您的情况下,您有一个内部shared_ptr,只要该内部shared_ptr过期,您的对象就不会死亡。
然而,这并不意味着您可以自杀。如果您删除了最后一个引用,则如果任何人已经将weak_ptr上的.lock()结果存储起来,您的对象将继续存在。由于这是您唯一可以从外部访问对象的方式,因此可能会发生。
简而言之,die()可能无法杀死对象。更好的做法可能是称其为remove_life_support(),因为在撤销生命支持后,其他东西可能会使对象继续存在。
除此之外,您的设计很好。
考虑将这些混乱的细节隐藏在具有pImpl解决内存管理问题的常规类型(或几乎常规类型)后面。那个pImpl可以是具有上述语义的weak_ptr
然后,您的代码用户只需要与常规(或伪常规)包装器交互。
如果您不希望克隆变得容易,禁用复制构造/赋值并仅公开移动。
现在,您的混乱内存管理隐藏在外观后面,如果您决定做错了一切,外部伪常规接口可以有不同的实现。
参考链接:C++11中的Regular类型

我认为pImpl解决方案是一个非常好的建议。它解决了我在答案中提出的许多问题,并给你带来了很多灵活性。这得到了我的支持。 - Jason

3

虽然不是直接的答案,但以下信息可能会有用。

在Chromium代码库中,有一个与您尝试实现的概念非常相似的东西。他们称之为WeakPtrFactory。虽然从设计上它可能对您有用,但由于他们自己实现了例如shared_ptrweak_ptr等内容,因此他们的解决方案不能直接应用到您的代码中。

我尝试实现它,并发现通过将自定义空删除器传递到内部的shared_ptr中可以解决双重删除的问题。从此时起,既从weak_ptr创建的shared_ptrs,也不会从内部的shared_ptr再次调用您对象的析构函数。

唯一需要解决的问题是,如果您的对象被删除,而其他地方仍保留着对它的shared_ptr引用怎么办?但是从我看到的情况来看,这并不能简单地通过任何魔法手段解决,而需要通过设计整个项目的方式,使其永远不会发生,例如仅在本地范围内使用shared_ptr,并确保某些操作(创建自杀对象,使用它,对其进行排序)仅在同一线程中执行。


2

我知道您想要为SO创建一个最小化的示例,但我看到了一些挑战需要考虑:

  1. 您有一个公共构造函数和析构函数,因此从技术上讲,不能保证始终使用create()方法。
  2. 您可以将它们设置为受保护或私有,但这个决定会干扰std算法和容器的使用。
  3. 这并不保证对象实际上会被销毁,因为只要有人有一个shared_ptr,它就会存在。这可能对您的用例有利也可能有害,但由于这个原因,我认为这不会像您预期的那样有价值。
  4. 这很可能会让其他开发者感到困惑和反直觉。即使您的意图是使其更易于维护,这也可能使维护变得更加困难。这是一个价值判断,但我鼓励您考虑它是否真的更容易管理。

我赞扬您事先考虑内存管理的思路。纪律性地使用shared_ptr和weak_ptr将有助于解决内存管理问题--我建议不要尝试让实例尝试管理自己的生命周期。

至于艺术研究......我只会建议如果这确实是您的热情所在!祝你好运!


你的第一个观点是因为我很蠢,编辑了原始帖子 ;) — 我认为通过独占使用 weak_ptr 来传达“不要持有它”的意思,如果这是正确的,那么同时只应该有一个外部 shared_ptr。编辑:等等,它不能直接编译,因为有一个私有构造函数。这是一个非常好的观点。 - Quentin
是的,但现在您已经被取消使用大部分标准库的资格了。我喜欢Yakk提出的pImpl方法建议。 - Jason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接