3

我想显式地销毁一个对象(调用它及其所有字段的析构函数),但可能会发生我仍然持有一些指向相关对象的指针。因此,我还不想释放内存;相反,我想留下一种标志“我是一个被破坏的物体”。

我想到了以下方法:

class BaseClass { //all objects in question derive from this class
public:
    BaseClass() : destroyed(false) {}
    virtual ~BaseClass() {}
private:
    bool destroyed;
public:
    bool isDestroyed() { return destroyed; }
    void destroy() {
        this->~BaseClass(); //this will call the virtual destructor of a derivative class
        new(this) BaseClass();
        destroyed=true;
    }
};

destroy被调用时,我基本上会破坏我拥有的任何对象(可能是派生对象)并在同一个地方创建一个新的“僵尸”对象。因此,我希望实现:

  • ptr以前指向该对象的任何其他指针仍然可以调用ptr->isDestroyed()以验证其存在。
  • 我知道,如果我不检查僵尸标志并尝试访问属于任何派生对象的字段,则可能会发生不好的事情
  • 我知道僵尸对象仍然消耗与被破坏对象一样多的内存(因为它可能是 的派生对象BaseClass
  • 我仍然必须释放被破坏对象的内存。但是,我希望那个调用delete仍然正确吗?

问题:

使用上述模式时还有其他问题需要考虑吗?

调用delete僵尸对象会正确释放前一个(正常)对象消耗的整个内存吗?


虽然我很欣赏您对如何以不同方式进行操作的意见,并且我可能倾向于按照您的方式进行操作 - 我仍然想了解上述代码带来的所有风险。

4

4 回答 4

5

有一个使用智能指针的建议。事实上 - 我正在这样做,但我的参考是循环的。我可以使用一些成熟的垃圾收集器,但是因为我知道自己在哪里(以及何时!)循环链可以被破坏,我想自己利用它。

然后您可以显式地使循环智能指针之一无效(如果您正在使用则重置shared_ptr)并打破循环。

或者,如果您提前知道循环在哪里,您也应该能够提前避免使用它们来weak_ptr代替一些shared_ptrs.

- - 编辑 - -

如果仅由弱指针引用的对象只会被标记为“无效”并释放对其包含的所有指针的控制(这句话清楚吗?),我会很高兴。

那么weak_ptr::expired应该会让你开心:)

于 2012-11-10T12:49:50.463 回答
5

你对你的问题有一些讨厌的评论。现在我认为他们不值得,尽管可能有更好的方法来做你想做的事。我知道您来自哪里,但实际上您使用析构函数的方式与使用您拒绝编写的重置函数的方式相同。实际上,调用析构函数并没有任何好处,因为调用析构函数与实际删除或重置任何内容无关,除非您实际编写代码在析构函数中执行此操作。

至于你关于安置新的问题:

您可能已经知道放置 new 不会分配任何内存,因此调用它只会在同一个位置创建对象。我知道这正是您想要的,但这不是必需的。由于您不会在对象上调用 delete 只是破坏,因此您可以在不初始化类的情况下将 destroy 设置为 true。

把它们加起来:

  1. 如果您将析构函数用作常规虚函数,您将一无所获。不要这样做,因为如果析构函数被调用两次,你可能会遇到麻烦
  2. 对放置 new 的调用不会分配内存,只会执行不必​​要的初始化。您可以将destroyed设置为true。

为了正确地做你想做的事情并获得析构函数的好处,你应该重载类的 new 和 delete 运算符并使用正常的销毁机制。然后,您可以选择不释放内存但将其标记为无效,或者可能释放大部分内存但让指针指向某些标志。

编辑

根据评论,我决定总结一下我看到的所有风险以及其他人指出的风险:

  1. 在多线程环境中访问无效指针:使用您的方法可以在析构函数运行之后但在设置破坏标志之前访问一个类(至于您在其中一个评论中的问题 - shared_ptr 在大多数情况下是线程安全的)
  2. 中继您无法完全控制的行为:您的方法依赖于析构函数自动调用未动态分配的其他成员的析构函数的方式:这意味着您仍然必须专门释放动态分配的内存,您无法控制如何正是这实现了,您无法控制调用其他析构函数的顺序。
  3. 中继您无法完全控制的行为(第 2 点):您正在中继编译器实现调用其他析构函数的析构函数部分的方式,您无法判断您的代码是否可移植,甚至如何移植处理两次调用它。
  4. 析构函数可能会被调用两次:根据您的实现,这可能会导致内存泄漏或堆损坏,除非您防止两次释放相同的内存。您声称您通过调用放置 new 来防止这种情况 - 但是在多线程环境中,这不能进一步保证您假设所有内存分配都由默认构造函数完成 - 取决于您的具体实现,这可能是也可能不是真的。
  5. 你违背了每个回答你的问题或评论它的人的更好判断 - 你可能是天才,但很可能你只是通过将你的实施限制在它可以正常工作的一小部分情况下,给自己开枪. 这就像当你使用错误的螺丝刀时,你最终会损坏螺丝。同样,以不打算使用的方式使用语言构造可能最终会导致程序有问题 - 析构函数旨在从删除和编译器生成的代码中调用以清除堆栈。直接使用它是不明智的。

我重复我的建议 - 重载删除和新的你想要的

于 2012-11-16T22:28:21.173 回答
2

与其他人一样,我建议您只使用weak_ptr. 但是你问为什么你的方法不起作用。有一些优雅的实现和您的代码遍历的关注点分离的问题,但我不会争论这些。相反,我只想指出您的代码非常不安全。

考虑以下两个控制线程的执行顺序:

// Thread One
if ( ! ptr -> isDestroyed() ) {     // Step One
    // ... interruption ...
    ptr -> something();             // Step Three

和另外一个:

// Thread Two
ptr -> destroy();                   // Step Two

到第 3 步到来时,指针不再有效。现在可以通过实现lock()或类似的方法来解决这个问题,但是现在您可能会出现不释放锁的缺陷。大家推荐的原因weak_ptr是,这整个类的问题,无论是在类的接口还是在它的实现上,都已经解决了。

一个问题仍然存在。您似乎想要一个可以随意杀死物体的设施。这相当于要求指向对象的唯一指针是弱指针,不存在在手动删除对象时会中断的强指针。(我会规定这不是一个坏主意,尽管我必须说我不知道​​为什么它不是你的情况。)你可以通过在weak_ptrand之上构建来获得这个shared_ptr。这些类是通用的,所以如果你想禁止shared_ptr访问BaseClass,那么你可以编写一个特殊化的shared_ptr<BaseClass>行为不同的。隐藏一个实例shared_ptr<BaseClass>以防止删除,并通过您控制的工厂方法提供此类指针。

在这个模型中destroy()需要注意语义。第一个选择是您想要同步还是异步操作。同步destroy()将阻塞,直到所有外部指针都被释放并且不允许发布新指针。(我假设指针上的复制构造函数已经被禁用。)有两种异步destroy()。如果仍然存在外部引用,则两者中较简单的一个会失败。调用unique()hiddenshared_ptr()使其成为一个简单的实现。更复杂的行为类似于异步 I/O 调用,将销毁安排在未来的某个时间点发生,大概是在所有外部引用消失后。这个函数可能被调用mark_for_death()反映语义,因为对象可能会或可能不会在返回时被销毁。

于 2012-11-17T00:12:48.390 回答
1

我会考虑使用一种适当的智能指针模式。访问已删除对象的行为仍未定义,“僵尸”标志并没有真正的帮助。与被删除的对象实例关联的内存可能会立即被创建的任何其他对象占用,因此访问僵尸标志不是您可以信任的信息。

恕我直言,安置新运营商

new(this) BaseClass();

在您的destroy()方法中使用不会真正有帮助。取决于如何使用此方法。而不是删除派生对象或已删除对象的内部析构函数。在后一种情况下,无论如何都会释放内存。

更新:

根据您的编辑,使用共享指针/弱指针习语来解决循环引用的出现不是更好吗?否则,我会将这些视为设计缺陷。

于 2012-11-10T12:20:40.193 回答