【问题标题】:Leaving "zombie" flag upon object destruction在物体破坏时留下“僵尸”标志
【发布时间】:2012-11-10 12:11:08
【问题描述】:

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

我想到了以下方法:

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 会正确释放前一个(正常)对象消耗的整个内存吗?


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

【问题讨论】:

  • 您似乎在寻找std::shared_ptr
  • 这是个坏主意。使用智能指针。
  • new(this) BaseClass(); - 如果this 是“最初属于派生类型”,我敢肯定,在此之后所有的赌注都没有了。
  • @CygnusX1 - 这是我很久以来见过的最愚蠢和最愚蠢的代码。你到底想达到什么目的?!
  • @CygnusX1,惊喜 - 这正是 weak_ptr 的工作原理。

标签: c++


【解决方案1】:

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

关于你关于新位置的问题:

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

总结一下:

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

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

编辑

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

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

我重复我的建议 - 重载 delete 和 new 来满足你的需要

【讨论】:

  • “实际上,调用析构函数并没有任何好处,因为调用析构函数与实际删除或重置任何内容无关,除非您在析构函数中实际编写代码来执行此操作。” -- 注意析构函数会自动调用所有类成员的析构函数。使用析构函数而不是自己实现 reset() 可以省去遍历所有成员并自己调用 reset() 的麻烦(前提是成员具有这样的功能)。
  • @CygnusX1 你是对的,但自动销毁仅适用于非动态分配的类变量。在我看来,您的方法非常冒险-您依赖仅对部分类数据正确的自动行为。此外,由于对于这些变量没有发布任何内容,因此几乎没有收益。但是,我知道这对您有什么帮助 - 请记住要防止对析构函数的双重调用或试图释放已释放的内存。
  • 好点。但是,在使用上述构造时(多线程危险除外),我(还)没有看到任何额外的双重破坏或双重释放风险。即使destroy() 被调用两次,我认为它应该仍然可以正常工作?这是因为当我手动销毁一个对象时,我会立即在其位置创建一个新对象。只在同一个指针上调用delete 两次可能会导致错误,但即使没有上述构造也是错误的。
  • @CygnusX1 - 我试图总结我和其他人看到的所有风险。编辑了答案
  • 感谢您抽出宝贵时间总结一切,尽管我不认为其中一些问题是真正的问题。
【解决方案2】:

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

然后您可以显式地使循环智能指针之一为空(reset,如果您使用的是shared_ptr)并打破循环。

或者,如果您提前知道周期的位置,您还应该能够提前避免使用weak_ptr 代替一些shared_ptrs。 p>

--- 编辑---

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

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

【讨论】:

  • 显式地使智能指针无效-在我的例子中,这意味着遍历我的所有类,为每个类实现reset 方法,这将使每个类的所有智能指针无效。连同标记该对象不再可用。是的,我可以做到这一点,但这是最初发明析构函数的工作。
  • @CygnusX1 所以你使用的是你自己的智能指针?不,您不需要“使每个类的所有智能指针无效”。您只需要取消使循环保持活力的那个。不,这不是“最初发明析构函数的工作” - 你不是在破坏智能指针,你只是在改变它的状态。
  • 我想我措辞不正确。我的意思是 - 我必须实现 reset() 成员函数,该函数将重置该对象的所有成员指针(而不是所有指针)。我想通过使节点“弱”而不是通过使边缘“弱”来打破循环。
  • @CygnusX1 那么,有什么问题呢?您需要以某种方式 使循环内指针之一无效。无论您是在成员函数中还是在其他地方使其无效,您都必须这样做。无论您是通过使指针为空还是通过对它指向的对象进行僵尸化来做到这一点,在逻辑上都是相同的。我认为你从错误的角度处理这整个事情 - 如果你知道循环在哪里,那么你就知道哪个指针指向 null-ify 来打破它,那么你为什么不能这样做呢?
  • @CygnusX1 好吧,你说“我知道自己在哪里(以及何时!)可以打破圆链”,这暗示了相反的意思。好的,因为你实际上并不知道,所以将一个单个“知名”shared_ptr 指向对象,所有其他指针将是weak_ptr。当需要释放对象时,只需重置这个shared_ptr,所有weak_ptrs 将自动过期(即它们的weak_ptr::expired 将返回true)。
【解决方案3】:

与其他人一样,我建议您只使用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_ptrshared_ptr 之上构建来获得它。这些类是通用的,所以如果你想禁止shared_ptr 访问BaseClass,那么你可以为shared_ptr<BaseClass> 编写一个行为不同的特化。隐藏shared_ptr<BaseClass> 的一个实例以防止删除,并通过您控制的工厂方法提供此类指针。

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

【讨论】:

  • 好点!我没有考虑线程安全。但是,通过使用智能指针,我也不会获得线程安全,不是吗?任何成员函数(无论是上面的destroy 还是建议的reset)都必须以某种方式加以保护。如果我改变shared_ptr 的行为,这部分代码也必须受到保护。
  • 在智能指针上放置一个单独的保护系统,而不是在每个成员函数上放置一个保护系统确实是使用智能指针的原因之一。从设计模式的角度来看,这是最有力的理由,因为它很好地分离了关注点。 shared_ptrweak_ptr 的线程安全已经完全解决。除此之外,唯一需要关注的是destroy() 的语义;我在回答中添加了关于该主题的段落。
【解决方案4】:

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

恕我直言,安置新运营商

new(this) BaseClass();

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

更新:

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

【讨论】:

  • 我正在销毁对象,我没有删除底层内存。相反,我自己重用内存以在那里创建一个新对象。
  • 是的,我看到写完答案后,反正是可疑的。
  • 这很可疑 - 我同意。这就是为什么我想在投入之前了解自己所承担的所有风险。
  • 我不太明白您对“恕我直言”部分的回应。我从不删除“destroy()”方法中的任何内存,因此不会释放任何内存。 weak_ptr 是...弱...如果我通过它访问一个对象,该对象可能不再存在。
  • @CygnusX1 你有没有考虑过... 添加一个标志?你可以reset() 设置该标志时它持有的任何指针。
猜你喜欢
  • 2023-03-29
  • 1970-01-01
  • 2014-10-10
  • 1970-01-01
  • 2011-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-17
相关资源
最近更新 更多