【问题标题】:Is there a way to get a C++ class to automatically execute a method just before it starts executing the destructor?有没有办法让 C++ 类在它开始执行析构函数之前自动执行一个方法?
【发布时间】:2012-06-06 07:58:27
【问题描述】:

这是一个问题场景:我有一个 C++ 对象,其中包含一个需要在对象被销毁之前调用的 Cleanup() 虚拟方法。为了做正确的事,这个 Cleanup 方法需要访问完整的对象(包括子类数据),所以我不能只在我自己的类的析构函数开始时调用 Cleanup(),因为当我的类的当调用析构函数时,任何子类的析构函数都已经完成,它们可能已经释放了一些 Cleanup() 需要查看的数据。

显而易见的解决方案是要求调用代码在删除对象之前手动调用我的例程:

theObject->Cleanup();
delete theObject;

但该解决方案很脆弱,因为迟早有人(可能是我)会忘记调用 Cleanup() 并且会发生坏事。

另一种解决方案是拥有一个实现 pImpl 技术的“持有人”对象来包装类,并在删除对象之前让持有人对象的析构函数调用该对象的 Cleanup();但该解决方案也不是 100% 可取的,因为它使类的工作方式与标准 C++ 类不同,并且无法在堆栈上或静态分配对象。

所以问题是,是否有一些聪明的技术(在 C++ 或 C++11 中)可以用来告诉编译器在调用第一个子类之前自动调用我的对象上的指定(可能是虚拟)方法-析构函数?

(想想看,自动调用 Init() 方法的类似机制——就在最后一个子类构造函数完成之后——也可能很方便)

【问题讨论】:

  • 太短以至于不值得回答:你想要的存在于 Common Lisp 对象系统中,它的 :before:after:around 方法。其他一些语言也有类似的概念,但 C++ 没有(至少据我所知没有)。

标签: c++ c++11 destructor


【解决方案1】:

横向思考:摆脱Cleanup 方法,这就是virtual 析构函数的用途。

virtual 析构函数的真正目标是允许在执行delete basepointer; 时调用最外层的派生类析构函数;正如 RAII 所展示的,清理析构函数的工作。

现在你可能会争辩说你想要一个早期的Cleanup 方法,以减轻析构函数的任务或者重用对象(在某种程度上有点像Reset 方法)。在实践中,它很快变成了维护的噩梦(太容易忘记清理一个字段,并且从一个使用到另一个使用状态泄漏)。

所以问自己一个问题:为什么不将析构函数用于创建它的目的?

【讨论】:

  • 通常我会这样做,但在某些情况下还不够(例如,因为清理需要调用虚拟方法,或者做其他依赖于仍然可用的子类信息的事情)
  • 评论仍然有效,但可能需要调整设计。如果您更紧密地遵循 SRP(单一职责原则)并将实际类及其数据与线程分开,那么创建线程的类可能会引用它正在活动的内容(例如通过shared_ptr)并确保它们存在足够长了。在析构函数中,加入线程,因为它持有一个shared_ptr 到数据,它们还不能开始销毁。
【解决方案2】:

您可以在 PIMPL 包装器上使用一个扭曲:

class PIMPL
{
    MyDataThatNeedsInitDestroy    object;

  public:
    PIMPL(Atgs a)
       : object(a)
    {
        object.postCreationInit();
    }

    ~PIMP()
    {
        object.preDestructionDestory();
    }

    // All other methods are available via -> operator
    MyDataThatNeedsInitDestroy* operator->() { return &object);
};

【讨论】:

  • 您可能还想将 MyDataThatNeedsInitDestroy 的 ctor/dtor 设为私有(并使用“朋友”)。另外我不确定这是否正确命名为 PIMPL,因为该对象需要在此类之上完全定义。
  • 你按值接受参数。当 op 尝试将派生类提供给这个包装器时,你认为会发生什么?
  • @SigTerm:将其模板化或使其成为指针。有一百万种解决方案。但我无法用这样一个笼统的问题来解决其中任何一个问题,我所能做的就是给出一个解决问题的陈述点。
  • 这个解决方案并不完全令人满意,因为调用代码仍然有责任知道做正确的事情——在这种情况下,调用用户需要记住将对象包装在 PIMPL 包装器中,他可能会忘记这样做。尽管如此,我认为这是迄今为止建议的最接近可行解决方案的方法。
  • @JeremyFriesner:您可以强制使用上述包装器,方法是将构造函数设为私有,然后将 PIMPL 设为类的朋友。那么创建实例的唯一方法就是包装它。我也将@Matthieu M. 视为更好的答案,但不明白为什么需要单独的虚拟清理()。可以将析构函数设为虚拟并完成这项工作。这种技术通常用于两阶段构建而不是破坏。
【解决方案3】:

是的,这可以通过虚拟继承来完成,因为虚拟基础子对象总是由最派生的类型构造(和销毁),并且基类的构造(和销毁)顺序也是明确定义的。

当然,您也可以将delete 运算符设为私有,并提供一个由Cleanup(); delete this; 组成的Release() 成员函数,但这对堆栈分配的对象没有帮助。

【讨论】:

  • 我认为他希望在子类析构函数运行之前调用 Cleanup。
  • @Andrew:你使用多重继承......一个子对象将在任何其他继承链之前被销毁。
  • 不,我的意思是我们有 B 类和 D 类:B。他有函数 B::Cleanup,他想在销毁 D 时自动调用如下 (1) B::Cleanup (2) D: :~D (3) B::~B.
  • @Andrew:我认为没有正当理由想要这样做。如果他想要,那不可能是有充分理由的。
  • @JeremyFriesner:这个用例有缺陷。线程在对象完全构造之前启动,因此您可以在构造对象时遇到竞争条件。你将不得不改变你的设计。
【解决方案4】:
struct B
{
    void cleanup()
    {
        if (!m_bCleanedUp)
        {
            m_bCleanedUp = true;
            ...
        }
    }

    virtual B::~B()
    {
        assert(m_bCleanedUp);
        ...
    }

    bool m_bCleanedUp = false;
};

struct D : B
{
    D::~D()
    {
        cleanup(); // if D author forgets, assert will fire
        ...
    }
};

【讨论】:

  • 但是添加struct E : D... 现在E::cleanup() 将不会运行。
  • 没有 E::cleanup,只有 B::cleanup。
  • 错了。从问题:“一个 Cleanup() 虚拟方法”和“这个 Cleanup 方法需要访问完整的对象”。但在你的情况下,对象的 E 部分已经被销毁了。
  • 好的,在这种情况下,E::~E 应该调用 cleanup(),但断言不会触发。
【解决方案5】:

有没有办法让 C++ 类在它开始执行析构函数之前自动执行一个方法?

您需要将智能指针(shared_ptr - 在 boost 或 c++11 中可用)与自定义删除器一起使用,而不是原始指针。

typedef shared_ptr<MyClass> MyClassPtr;
class MyClassDeleter{
public:
    void operator()(MyClass* p) const{
        if (p)
            p->cleanup();
        delete p;
    }
};

...

MyClassPtr ptr(new MyClass, MyClassDeleter());

--编辑--

这适用于动态分配的对象。堆栈分配的对象没有解决方案(我能想到),因此合理的做法是使构造函数私有化并创建将 shared_ptr 返回给构造对象的友元工厂方法 - 假设您确实需要这种机制。

【讨论】:

  • 就像我提到的Release() + private operator delete 一样,这只适用于动态分配的对象。
  • @BenVoigt:操作想要删除对象。这意味着它是动态分配的。堆栈分配的对象没有解决方案(我能想到),所以合理的做法是使构造函数私有化并创建将 shared_ptr 返回给构造对象的友元工厂方法。
  • 真的我希望解决方案适用于所有分配类型(动态、堆栈或全局)...但也许 C++ 不会给我这个:^P
猜你喜欢
  • 1970-01-01
  • 2018-04-21
  • 2019-11-29
  • 1970-01-01
  • 2016-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多