24

我在 C# 中度过了过去 4 年,因此我对 C++ 中当前的最佳实践和常见设计模式很感兴趣。考虑以下部分示例:

class World
{
public:
    void Add(Object *object);
    void Remove(Object *object);
    void Update();
}

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this;
        }
    }
}

在这里,我们有一个负责管理一组对象并定期更新它们的世界。火是一个可以在许多不同情况下添加到世界的对象,但通常是由世界上已经存在的另一个对象添加的。火是唯一知道它何时烧毁的对象,所以目前我让它自行删除。造成火灾的物体可能不再存在或不再相关。

这是明智的做法还是有更好的设计可以帮助清理这些物体?

4

11 回答 11

30

你做了Update一个虚函数,暗示派生类可以覆盖Update. 这带来了两大风险。

1.) 被覆盖的实现可能记得做 a World.Remove,但可能忘记delete this. 内存泄露了。

2.) 重写的实现调用基类Update,它执行 a delete this,但随后继续进行更多工作,但使用无效的 this 指针。

考虑这个例子:

class WizardsFire: public Fire
{
public:
    virtual void Update()
    {
        Fire::Update();  // May delete the this-object!
        RegenerateMana(this.ManaCost); // this is now invaild!  GPF!
    }
}
于 2009-02-06T23:42:04.357 回答
20

这样做的问题是,您实际上是在对象和 World 类之间创建隐式耦合。

如果我尝试在 World 类之外调用 Update(),会发生什么?我可能最终会删除该对象,但我不知道为什么。职责似乎严重混淆。当您在编写此代码时没有想到的新情况下使用 Fire 类时,这将导致问题。如果对象应该从多个位置删除,会发生什么?也许它应该从世界、当前地图和玩家的库存中移除?您的更新功能会将其从世界中删除,然后删除该对象,并且下次地图或库存尝试访问该对象时,会发生坏事。

一般来说,我会说 Update() 函数删除它正在更新的对象是非常不直观的。我还要说一个对象删除自己是不直观的。该对象应该更有可能有某种方式来触发一个表明它已经完成燃烧的事件,任何有兴趣的人现在都可以对此采取行动。例如,将其从世界上移除。要删除它,请考虑所有权。

谁拥有该对象?世界?这意味着只有世界才能决定物体何时死亡。只要世界对对象的引用比对它的其他引用更持久,那很好。你认为对象拥有自己吗?那有什么意思?当对象不再存在时应该删除对象吗?没有意义。

但是如果没有明确定义的单一所有者,实现共享所有权,例如使用实现引用计数的智能指针,如boost::shared_ptr

但是在对象本身上有一个成员函数,它被硬编码以从一个特定列表中删除该对象,无论它是否存在于那里,以及它是否也存在于任何其他列表中,并且无论哪个列表都删除对象本身对它的引用是一个坏主意。

于 2009-02-06T23:49:52.297 回答
6

在 C++ 中删除自己的对象并没有什么问题,大多数引用计数的实现都会使用类似的东西。但是,它被视为一种略微深奥的技术,您需要密切关注所有权问题。

于 2009-02-06T23:29:36.080 回答
6

如果对象没有被分配并使用 new 构造,删除将失败并可能导致程序崩溃。此构造将阻止您在堆栈上或静态实例化类。在您的情况下,您似乎正在使用某种分配方案。如果你坚持这一点,这可能是安全的。不过,我当然不会这样做。

于 2009-02-06T23:39:24.687 回答
5

我更喜欢更严格的所有权模式。世界应该拥有其中的对象并负责清理它们。要么让 world remove 这样做,要么让 update (或另一个函数)返回一个值,该值指示应该删除一个对象。

我也猜测在这种类型的模式中,您将希望引用计数对象以避免以悬空指针结束。

于 2009-02-06T23:33:42.767 回答
5

它当然适用于由调用者创建的对象new以及当调用者Update正确了解该行为时。但我会避免它。在你的情况下,所有权显然在世界,所以我会让世界删除它。该对象不会自行创建,我认为它也不应该自行删除。如果您在对象上调用函数“更新”,可能会非常令人惊讶,但是突然间,如果 World 没有自己做任何事情(除了将其从列表中删除 - 但在另一个框架中),该对象就不再存在了!代码调用对象上的更新不会注意到)。

对此的一些想法

  1. 将对象引用列表添加到 World。该列表中的每个对象都待删除。这是一种常用技术,在 wxWidgets 中用于已关闭但仍可能接收消息的顶级窗口。在空闲时间,当所有消息都处理完毕后,处理挂起列表,并删除对象。我相信 Qt 遵循类似的技术。
  2. 告诉全世界该对象要被删除。世界将得到适当的通知,并将处理任何需要完成的事情。像一个deleteObject(Object&)可能的东西。
  3. shouldBeDeleted如果对象希望被其所有者删除,则向每个对象添加一个返回 true 的函数。

我更喜欢选项 3。世界会调用更新。之后,它会查看是否应该删除该对象,并且可以这样做 - 或者如果它愿意,它会通过手动将该对象添加到待删除列表中来记住该事实。

当您无法确定何时以及何时不能访问对象的函数和数据时,这真是令人头疼。例如,在 wxWidgets 中有一个 wxThread 类,它可以在两种模式下运行。其中一种模式(称为“可分离”)是,如果它的主函数返回(并且应该释放线程资源),它会删除自己(以释放 wxThread 对象占用的内存)而不是等待线程的所有者对象来调用等待或加入函数。然而,这会导致严重的头痛。你永远不能在它上面调用任何函数,因为它可能在任何情况下都被终止,而且你不能用 new 来创建它。相当多的人告诉我,他们非常不喜欢它的这种行为。

引用计数对象的自我删除很臭,恕我直言。让我们比较一下:

// bitmap owns the data. Bitmap keeps a pointer to BitmapData, which
// is shared by multiple Bitmap instances. 
class Bitmap {
    ~Bitmap() {
        if(bmp->dec_refcount() == 0) {
            // count drops to zero => remove 
            // ref-counted object. 
            delete bmp;
        }
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount();
    int inc_refcount();

};

将其与自删除引用对象进行比较:

class Bitmap {
    ~Bitmap() {
        bmp->dec_refcount();
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount() {
        int newCount = --count;
        if(newCount == 0) {
            delete this;
        }
        return newCount;
    }
    int inc_refcount();
};

我认为第一个更好,而且我相信设计良好的引用计数对象不会“删除它”,因为它增加了耦合:使用引用计数数据的类必须知道并记住数据将自身删除为减少引用计数的副作用。请注意“bmp”如何可能成为 ~Bitmap 的析构函数中的悬空指针。可以说,不这样做“删除这个”在这里要好得多。

回答类似问题“删除这个有什么用”

于 2009-02-06T23:47:28.853 回答
2

这是一个相当常见的引用计数实现,我之前已经成功使用过。

但是,我也看到它崩溃了。我希望我能记住坠机的情况。@abelenky是我见过它崩溃的一个地方。

它可能是您进一步子类化的地方Fire,但未能创建虚拟析构函数(见下文)。当您没有虚拟析构函数时,该Update()函数将调用~Fire()而不是适当的析构函数~Flame()

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this; //Make sure this is a virtual destructor.
        }
    }
};

class Flame: public Fire
{
private: 
    int somevariable;
};
于 2009-02-06T23:36:23.290 回答
2

其他人提到了“删除这个”的问题。简而言之,既然“世界”管理着火的创建,它也应该管理着它的删除。

另一个问题是,如果世界以仍在燃烧的大火而告终。如果这是可以摧毁火灾的唯一机制,那么您最终可能会出现孤儿或垃圾火灾。

对于您想要的语义,我将在您的“Fire”类(或 Object,如果适用)中有一个“active”或“alive”标志。然后这个世界有时会检查它的物品清单,并摆脱那些不再活跃的物品。

--

还有一点需要注意的是,您编写的代码具有从 Object 私有继承的 Fire,因为这是默认设置,尽管它比公共继承少得多。您可能应该明确表示这种继承,我怀疑您真的想要公共继承。

于 2009-02-07T03:23:12.333 回答
1

除非必要或以非常直接的方式使用,否则执行“删除”通常不是一个好主意。在您的情况下,看起来 World 可以在删除对象时删除它,除非我们没有看到其他依赖项(在这种情况下,您的 delete 调用将导致错误,因为它们没有得到通知)。将所有权保留在对象之外可以让您的对象更好地封装和更稳定:对象不会因为选择删除自身而在某个时候变得无效。

于 2009-02-06T23:32:33.657 回答
0

我不认为对象删除本身存在任何内在错误,但另一种可能的方法是让世界负责删除对象作为 ::Remove 的一部分(假设所有已删除的对象也被删除。)

于 2009-02-06T23:25:20.677 回答
0

删除这个是非常危险的。它没有任何“错误”。这是合法的。但是当你使用它时有很多事情可能会出错,所以应该谨慎使用它。

考虑这样一种情况,有人打开了对象的指针或引用,然后它去删除自己。

在大多数情况下,我认为对象生命周期应该由创建它的同一个对象管理。它只是更容易知道破坏发生在哪里。

于 2009-02-07T00:23:33.990 回答