【问题标题】:default override of virtual destructor虚拟析构函数的默认覆盖
【发布时间】:2017-04-21 08:29:02
【问题描述】:

每个人都知道基类的析构函数通常必须是虚拟的。但是派生类的析构函数是什么呢?在 C++11 中,我们有关键字“override”和显式使用默认析构函数的能力。

struct Parent
{
  std::string a;
  virtual ~Parent()
  {
  }

};

struct Child: public Parent
{
  std::string b;
  ~Child() override = default;
};

在 Child 类的析构函数中同时使用关键字“override”和“=default”是否正确?在这种情况下编译器会生成正确的虚拟析构函数吗?

如果是,那我们是否认为这是一种很好的编码风格,我们应该总是这样声明派生类的析构函数以确保基类析构函数是虚拟的?

【问题讨论】:

  • 也可以static_assert(std::has_virtual_destructor<Parent>::value, "contract violated");
  • 请注意,基类析构函数不一定是虚拟的。所以这只是(可能)一个好主意,如果这是一个要求。
  • 如果它有效,我喜欢它,但milleniumbug 更好(更清晰的意图)。另一方面,Stroustrup 讨厌防止常见错误的“编码标准”结构,并坚持编译器应该生成适当的警告。
  • 我认为@milleniumbug 的方法清楚地表达了意图。如果我在代码库中遇到~Child() override = default;,我可能会删除该行。
  • “花点时间学习一些 C++ 可能是值得的”——请参阅this post 末尾的“责备程序员”。另外,请注意,我实际上并没有说我不理解static_assert,只是它比override 版本令人困惑。这是真的,因为它更长、更冗长,并且使用了标准库的一个相对晦涩的特性。

标签: c++ c++11 c++14


【解决方案1】:

在 Child 类的析构函数中同时使用关键字“override”和“=default”是否正确?在这种情况下编译器会生成正确的虚拟析构函数吗?

是的,没错。在任何理智的编译器上,如果代码编译没有错误,这个析构函数定义将是一个空操作:它的缺失一定不会改变代码的行为。

我们能不能认为这是很好的编码风格

这是一个偏好问题。对我来说,只有基类类型被模板化才有意义:它将强制要求基类具有虚拟析构函数。否则,当基本类型固定时,我会认为这样的代码是噪音。基类不会神奇地改变。 但是如果你有愚蠢的队友,他们喜欢改变事情而不检查取决于他们可能破坏的代码,最好保留析构函数定义 - 作为额外的保护层。 p>

【讨论】:

  • 请注意,有时,您是自己的死脑筋队友:(
【解决方案2】:

override 只不过是一张安全网。如果基类析构函数是虚拟的,则子类的析构函数将始终是虚拟的,无论它是如何声明的 - 或根本没有声明(即使用隐式声明的)。

【讨论】:

  • 问题是基类必须有一个虚拟析构函数,但错误地没有。
  • @SergeyA 这不是问题的答案。
【解决方案3】:

根据CppCoreGuidelines C.128,派生类的析构函数不应声明为virtualoverride

如果将基类析构函数声明为虚拟,则应避免声明派生类析构函数virtualoverride。一些代码库和工具可能会坚持对析构函数进行覆盖,但这不是这些指南的建议。

更新:回答为什么我们有析构函数的特殊情况。

Method overriding 是一种语言特性,它允许子类或子类提供特定的 已由其超类或父类之一提供的方法的实现。子类中的实现通过提供与父类中的方法具有相同名称、相同参数或签名以及相同返回类型的方法来覆盖(替换)超类中的实现。

换句话说,当您调用一个被覆盖的方法时,只有该方法的最后一个实现(在类层次结构中)实际执行,而 all 析构函数(从最后一个子级到根父级)必须调用才能正确释放对象拥有的所有资源。

因此,我们并没有真正替换(覆盖)析构函数,而是在对象析构函数链中添加了一个。

更新:此CppCoreGuidelines C.128 规则已更改(由14481446 问题)以简化已经详尽的例外列表。所以一般规律可以概括为:

对于类用户,包括析构函数在内的所有虚函数都是同样多态的。

在国有子类上标记析构函数 override 是教科书式的卫生习惯,你们都应该按照惯例 (ref.)。

【讨论】:

  • 非常有趣!但他们没有解释为什么我们有析构函数的特殊情况,所有其他函数都应该被覆盖或最终......
  • C.128 没有这么说,它根本没有提到析构函数
  • @reddy 好吧,看起来他们改变了主意。您可以在此处找到更多信息:github.com/isocpp/CppCoreGuidelines/issues/1446。稍后我可能会更新我的答案。
【解决方案4】:

在此处使用override 有(至少)一个原因——确保基类的析构函数始终是虚拟的。如果派生类的析构函数认为它正在覆盖某些东西,但没有任何东西可以覆盖,这将是一个编译错误。如果您正在这样做,它还为您提供了一个方便的地方来留下生成的文档。

另一方面,我可以想到两个不这样做的理由:

  • 派生类从基类强制执行行为有点奇怪和倒退。
  • 如果您在标头中定义析构函数(或者将其设为内联),则确实会引入奇怪的编译错误的可能性。假设您的课程如下所示:

    struct derived {
        struct impl;
        std::unique_ptr<derived::impl> m_impl;
        ~derived() override = default;
    };
    

    您可能会收到编译器错误,因为析构函数(与此处的类内联)将寻找不完整类的析构函数 derived::impl

    这是我的迂回说法,每一行代码都可能成为一种责任,如果它在功能上什么都不做,也许最好跳过一些东西。如果你真的需要在父类的基类中强制执行虚拟析构函数,有人建议使用static_assertstd::has_virtual_destructor,这样会产生更一致的结果,恕我直言。

【讨论】:

  • 解决这个问题(前向声明的类)的方法是在你的编译单元中实现析构函数。例如,在您的 .cpp 文件中,derived::~derived() = default; 您可以在编译单元中使用 = default
【解决方案5】:

我认为“覆盖”对析构函数有点误导。 当您覆盖虚拟功能时,您将替换它。 析构函数是链式的,所以你不能从字面上覆盖析构函数

【讨论】:

  • 我不会这么说的。被覆盖的函数应该执行相同的语义任务(尽管派生版本应该更专业)。替换也不是全部。它只是意味着调用默认解析为对象的类型。您仍然可以使用范围运算符显式调用基类函数。见这里stackoverflow.com/questions/38010286/…
  • Override 指定基类实现的必需属性:该函数必须是虚拟的。说这句话实际上是完全合理的:~Derived() override = default; 使用覆盖是保证基类被正确定义的唯一方法。这在声明从模板参数派生的模板时尤其重要,并且需要保证基础正确声明其析构函数。
  • @Speed8ump 这与功能无关。重写构造函数没有多大意义。这里有一个有趣的讨论github.com/isocpp/CppCoreGuidelines/issues/721
  • 在英语语义上,你可能是正确的,重写('中断操作,以进行手动控制')析构函数是没有意义的。在 C++ 语义意义上(“要求基类方法是虚拟的”),将析构函数标记为覆盖并具有特定实用性是有意义的,正如我所提到的
【解决方案6】:

CPP Reference 表示 override 确保函数是 virtual 并且它确实覆盖了虚函数。所以override 关键字会确保析构函数是虚拟的。

如果您指定override 而不是= default,那么您将收到链接器错误。

你不需要做任何事情。留下Child dtor undefined 就可以了:

#include <iostream>

struct Notify {
    ~Notify() { std::cout << "dtor" << std::endl; }
};

struct Parent {
    std::string a;
    virtual ~Parent() {}
};

struct Child : public Parent {
    std::string b;
    Notify n;
};

int main(int argc, char **argv) {
    Parent *p = new Child();
    delete p;
}

这将输出dtor。但是,如果您删除 Parent::~Parent 处的 virtual,它不会输出任何内容,因为这是未定义的行为,正如 cmets 中所指出的那样。

好的风格是根本不提Child::~Child。如果您不能相信基类将其声明为虚拟,那么您对override= default 的建议将起作用;我希望有更好的方法来确保而不是在你的代码中乱扔那些析构函数声明。

【讨论】:

  • 如果删除Parent::~Parent 处的virtual,您将有未定义的行为。它可能什么也不输出。它可能会显示一个致命错误对话框。或者覆盖您正在使用的数据文件。
  • 有趣!我假设它只会调用子类上的基本 dtor,这将导致对基本成员的定义明确但可能是不希望的清理,但不会对任何派生成员进行清理。我已更新我的答案以纳入您的评论。
【解决方案7】:

虽然析构函数不是继承的,但标准中明确规定派生类的虚拟析构函数会覆盖基类的析构函数。

来自 C++ 标准(10.3 虚函数)

6 尽管析构函数不是继承的,但在派生的析构函数 类覆盖声明为虚拟的基类析构函数;见 12.4 和 12.5.

另一方面也有写(9.2类成员)

8 virt-specifier-seq 最多应包含每个 virt-speciifier 中的一个。 virt-specifier-seq 只能出现在 a 的声明中 虚成员函数 (10.3)。

虽然析构函数被称为特殊的成员函数,但它们也是成员函数。

我确信应该对 C++ 标准进行编辑,以明确析构函数是否具有 virt-specifier override。目前还不清楚。

【讨论】:

  • “我测试的编译器发出错误...”这正是override 的要点。您已经使用非virtual 析构函数编写了基类;编译器的行为正确。
  • @KyleStrand 我删除了关于编译器的注释,因为我可以使用旧的编译器。
  • 我怀疑你没有理解我的意思。您能否将您的代码复制并粘贴到在线编译器中(例如Coliru)?
猜你喜欢
  • 2010-10-24
  • 2014-01-30
  • 1970-01-01
  • 2016-03-26
  • 2014-01-09
  • 2021-03-27
  • 2012-07-14
  • 2018-06-29
  • 2010-11-10
相关资源
最近更新 更多