【问题标题】:How to derive from a class without virtual-destructor?如何从没有虚拟析构函数的类派生?
【发布时间】:2020-10-29 01:23:10
【问题描述】:

有一个虚类作为回调接口,我不能修改,也不能要求作者修复。该类的唯一成员是很多可以覆盖的虚拟方法,以便让库回调到我的代码中。为了获得一些回调机会,我应该创建该虚拟类的派生类,并覆盖相应的虚拟方法。如果我对某些回调机会不感兴趣,我只需要避免覆盖它们。

但是,该接口类的声明存在缺陷 - 其析构函数未声明为虚拟

例如:

class callback_t {
public:
  virtual void onData( int ) {};
};

我创建了一个子类并且不重写析构函数,但是当我删除类child_t的动态对象时,我遇到了来自编译器的警告(gcc9 with C++17):

删除具有非虚拟析构函数的多态类类型“child_t”的对象可能会导致未定义的行为。

class child_t : public callback_t {
public:
  ~child_t() {
    // release things specific to the child...
  };
  void onData( int ) override {
    // do things I want when onData
  };

private:
  int m_data = 0;
};

int main() {
  child_t* pc = new child_t;

  // pass pc into the routines of the library
  // working...

  delete pc;   /*deleting object of polymorphic class type ‘child_t’ which has non-virtual destructor might cause undefined behavior */
};

问题: 如何正确优雅地消除警告(我必须提交没有警告的代码)?

注释和修改:

  1. 我不能修改类callback_t的声明,我也不能要求它的作者修复!这是一个由权威机构发布的库。 劝我改基类也没用,判断lib的代码质量也没意义;

  2. 我从来没有打算用基类类型的指针释放child_t类的对象,我清楚地知道virtual-dtor和static-dtor的区别;

  3. 我需要解决的主要问题是消除编译警告,因为我确定没有内存泄漏,并且没有遗漏恢复某些状态;

  4. 在这种情况下,基类中没有数据,没有有意义的代码,而制作它的唯一目的是派生自,因此不应将其标记为 final。但我尝试将 child_t 设为“final”,警告消失了。我不确定这种方法是否正确。如果是这样,我认为这是迄今为止最便宜的方法;

  5. 我还尝试将 child_t 的 dtor 设为virtual,警告也消失了。但我仍然不确定它是否 是正确的。

【问题讨论】:

  • 我可以想到两件事:仅使用堆栈变量和智能指针,因此不需要显式删除内存,如果在析构函数中不需要更改任何内容,那么您就可以开始了。如果不是这种情况,那么是否存在许多虚拟方法中的任何一个,您知道您不需要某些东西或可以设置一些原本会在析构函数中完成的状态?因为那时你可以在那里做,而不是析构函数。
  • @Andrew“如果在析构函数中不需要更改任何内容”,这不是决定是否存在问题的正确依据。要么你遇到未定义的行为,要么你没有。在这两种情况下,析构函数的实际作用并不重要
  • 如果您要通过该基类指针删除对象,则只需要一个虚拟析构函数。对于接口类,这并不常见(尽管有时会这样做)。礼貌起见,应该将接口类的析构函数设为 protected: 以明确这一点。
  • 你为什么要这样做:child_t* pc = new child_t;?你不能使用child_t child; 并将&child 传递给“库的例程”吗?

标签: c++ polymorphism overriding derived-class virtual-destructor


【解决方案1】:

仅当您需要通过基类指针delete 派生对象时才需要virtual 析构函数。如果您不需要这样做,那么基类析构函数不是 virtual 的事实并不重要(尽管这是一个相当大的暗示,即该类从未打算被继承from - 在现代代码中,它可能应该被标记为final)。你仍然可以很好地从类派生。你只需要注意派生类的对象是如何被销毁的。

【讨论】:

  • 您的意思是“我只需要将派生类的析构函数设为‘虚拟’即可。”?
  • @Leon “你的意思是“我只需要将派生类的析构函数设为‘虚拟’。”? - 不。那不是我要说的根本。我的意思是,如果基类析构函数不是virtual,您仍然可以从该类派生,但是您需要注意delete 派生对象的方式,因为没有virtual 基类dtor,它将通过指向基类类型的指针对delete 派生对象有效 - 但直接删除派生对象没问题。
【解决方案2】:

您收到的警告是误报。与

child_t* pc = new child_t;

// pass pc into the routines of the library
// working...

delete pc;

pc指向child_t,它的静态类型是指向child_t的指针,所以会调用正确的析构函数。如果你有

callback_t* pc = new child_t;

// pass pc into the routines of the library
// working...

delete pc;

那么警告将是正确的,因为只会调用 callback_t 析构函数。


有一个解决方法,那就是使用std::shared_ptr。指针将正确的删除器存储在其存储中,因此即使析构函数不是虚拟的,也会调用正确的派生析构函数而不是基析构函数。您可以在shared_ptr magic :) 中查看更多相关信息

【讨论】:

  • 用智能指针解决它是一个了不起的方法,我非常喜欢它!但其他答案提供了一些成本较低的方法,例如“将派生类设为‘final’”和“将 child_t 的 dtor 设为‘虚拟’”。你能谈谈我的情况下哪个是最好的吗?
  • @Leon 关于 final 的另一个答案是说编写该类的人应该将其标记为 final 以表明您不会从中派生。关于使析构函数虚拟化的另一个答案是错误的,已被删除。
  • 在我的例子中,基类中没有数据,没有有意义的代码,制作它的唯一目的是派生自,所以它不能被标记为最终的。我尝试将 child_t 设为“最终”,但警告消失了。但我不确定这个方法是否正确。
  • @Leon 您要么需要将callback_t 更改为具有虚拟析构函数,使用shared_ptr,使用std::variant 使用访问者模式,或者像您使用指向派生的指针类型。其他任何东西都会有未定义的行为。
  • 我肯定会确保始终直接调用派生类的 dtor。我只需要消除警告。
猜你喜欢
  • 2021-03-20
  • 2011-11-16
  • 2017-02-10
  • 2021-08-22
  • 1970-01-01
  • 2015-04-16
  • 2020-09-29
  • 2013-11-01
  • 2012-04-13
相关资源
最近更新 更多