默认覆盖虚析构函数

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

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

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

};

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

在子类的析构函数中同时使用关键字"override"和"=default",这样做是否正确?编译器会生成正确的虚析构函数吗?

如果是,那么我们可以认为这是良好的编码风格,我们应该总是以这种方式声明派生类的析构函数,以确保基类析构函数是虚拟的吗?


8
最好执行 static_assert(std::has_virtual_destructor<Parent>::value, "contract violated"); - milleniumbug
4
我认为@milleniumbug的方法清晰地表达了意图。如果我在代码库中遇到~Child() override = default;,我可能会直接删除这行。 - juanchopanza
5
@milleniumbug 我有点困惑。override意义——唯一的意义——就是在父类方法不是virtual时强制编译器报错。那么,static_assert 怎么算是一种改进呢? - Kyle Strand
7
可能值得花点时间来学习一些C++。请参见此帖子末尾的“责怪程序员”部分。另外,请注意,我并没有说我不理解static_assert,只是它比override版本更加令人困惑。这是真的,因为它更长、更啰嗦,并且使用了标准库中相对较为晦涩的特性。 - Kyle Strand
5
“大家都知道基类的析构函数通常需要是虚函数。” 嗯,并不完全是这样。 - Lightness Races in Orbit
显示剩余18条评论
7个回答

35
在Child类的析构函数中同时使用"override"和"=default"关键字是正确的吗?这样做编译器会生成正确的虚拟析构函数吗?
是的,这是正确的。在任何正常的编译器上,如果代码编译没有错误,这个析构函数定义将是一个空操作:它的缺席不应该改变代码行为。
我们可以认为这是良好的编码风格吗?
这是一个偏好问题。对我来说,只有当基类类型是模板时才有意义:它将强制要求基类具有虚拟析构函数。否则,当基本类型已固定时,我会认为这样的代码是噪音。就像基类会神奇地改变一样。但是,如果你有懒惰的队友喜欢在没有检查可能破坏依赖其代码的情况下更改事物,最好把析构函数定义留在其中--作为额外的保护层。

43
有时候,你自己就是无用的队友。注意:此处的“deadheaded”指的是无用或没用的意思。 - Kyle Strand

26
根据CppCoreGuidelines C.128,派生类的析构函数不应声明为virtualoverride
如果基类析构函数声明为虚函数,则应避免将派生类析构函数声明为virtualoverride。一些代码库和工具可能会坚持对析构函数使用override,但这并不是这些准则的推荐。 更新:回答为什么我们有析构函数的特殊情况。 方法重写是一种语言功能,它允许子类或子类提供一个特定的实现,该实现已由其超类或父类之一提供。子类中的实现通过提供与父类中的方法相同的名称、参数或签名以及返回类型的方法来覆盖(替换)超类中的实现。
换句话说,当调用被覆盖的方法时,只有该方法的最后一个实现(在类层次结构中)实际上被执行,而必须调用所有析构函数(从最后一个子对象到根父对象)才能正确释放对象拥有的所有资源。
因此,我们实际上并没有替换(覆盖)析构函数,而是将其添加到对象析构器链中。

更新: 为了简化已经详尽的异常列表,CppCoreGuidelines C.128规则已被更改(通过14481446问题)。因此,一般规则可以概括为:

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

对于状态拥有子类,将析构函数标记为override是你应该常规执行的基本卫生措施(ref.)。


4
非常有趣!但他们没有解释为什么我们需要一个析构函数的特殊情况,所有其他函数应该是override或final... - Sandro
2
C.128没有这样说,它根本没有提到析构函数。 - reddy
4
@reddy 好的,看起来他们改变了想法。你可以在这里找到更多信息:https://github.com/isocpp/CppCoreGuidelines/issues/1446。我可能稍后会更新我的回答。 - Alexandre A.
到目前为止,这是最有价值的答案。唯一的问题是必须阅读所有的UPDATE部分才能理解这个答案。如果你有时间,我很高兴看到在这方面的重新制定。可能只需在开头给出最终版本的提示就足够了。 - Wolf

25

override 并不过多地提供了一个保险措施。只要基类析构函数是虚拟的,无论如何声明(或者根本不声明,即隐式声明),子类的析构函数始终会是虚拟的。


5
针对基类必须有虚析构函数但实际上没有的情况,这个问题是针对这种情况的。 - Kenny Ostrom
5
@SergeyA 这并不是对问题的回答。 - Vlad from Moscow

9

在这里使用override至少有一个原因——你确保基类的析构函数始终是虚拟的。如果派生类的析构函数认为它正在覆盖某些内容,但实际上没有什么需要覆盖,那么编译将出现错误。此外,如果您正在生成文档,则可以将其放在方便的位置。

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

  • It's a little weird and backwards for the derived class to enforce behavior from the base class.
  • If you define a destuctor in the header (or if you make it inline), you do introduce the possibility for odd compilation errors. Let's say your class looks like this:

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

    You will likely get a compiler error because the destructor (which is inline with the class here) will be looking for the destructor for the incomplete class, derived::impl.

    This is my round-about way of saying that every line of code can become a liability, and perhaps it's best to just skip something if it functionally does nothing. If you really really need to enforce a virtual destructor in the base class from the parent class, someone suggested using static_assert in concert with std::has_virtual_destructor, which will produce more consistent results, IMHO.


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

8

我认为“override”在析构函数上有点误导性。当你覆盖虚函数时,你会替换它。析构函数是链接在一起的,所以你不能直接覆盖析构函数。


3
我不会这么说。覆盖函数应该执行相同的语义任务(尽管派生版本应该更加专业化)。此外,替换并不完全。它只意味着默认情况下调用会解析为对象的类型。您仍然可以使用作用域运算符显式调用基类函数。请参见此处 https://stackoverflow.com/questions/38010286/how-can-i-call-virtual-function-definition-of-base-class-that-have-definitions-i - Paul Floyd
3
Override指定了基类实现的必需属性:该函数必须是虚函数。实际上,这样说是完全合理的:~Derived() override = default;。使用override是确保基类已经正确定义的唯一方法。当声明从模板参数派生的模板时,并且需要保证基类正确声明其析构函数时,这一点尤为重要。 - Speed8ump
@Speed8ump 这不是关于函数的问题。重载构造函数并没有太多意义。这里有一个有趣的讨论 https://github.com/isocpp/CppCoreGuidelines/issues/721 - Alexandre A.
3
从英语语义上来看,你可能是正确的,覆盖('中断操作,以便手动控制')一个析构函数是没有意义的。但从C++语义上('要求基类方法为虚拟方法')来看,将析构函数标记为override是有意义的,并且具有特定的实用性,正如我所提到的。 - Speed8ump

4
CPP Reference中提到,override确保函数是virtual的,并确实重写了一个虚函数。所以override关键字会确保析构函数是虚拟的。
如果你指定了override但没有指定= default,那么你将得到链接错误。
你不需要做任何事情。 留下未定义的Child析构函数就可以正常工作:
#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,它将不会输出任何内容,因为正如评论中指出的那样,这是未定义的行为。
好的编程风格应该根本不提及Child ::〜Child。如果您不能信任基类声明它为虚拟,则您使用override和= default的建议将起作用;我希望有更好的方法来保证其正确性,而不是在代码中添加这些析构函数声明。

5
如果您在Parent::~Parent中删除virtual,那么会导致未定义行为。它可能没有任何输出,也可能显示一个致命错误对话框,甚至可能覆盖您正在处理的数据文件。 - Ben Voigt
1
有趣!我一直以为它只会调用子类的基本析构函数,这将导致基本成员的清理得到明确定义但可能不受欢迎,而派生成员则不会被清理。我已更新我的答案以纳入您的评论。 - Martin Ueding

0

尽管析构函数不会被继承,但标准中明确写道,派生类的虚析构函数会覆盖基类的析构函数。

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

6 尽管析构函数不会被继承,但在派生类中声明为虚函数的析构函数会覆盖基类的析构函数;参见12.4和12.5。

另一方面,也有这样的说明(9.2类成员)

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

尽管析构函数像特殊成员函数一样被调用,但它们也是成员函数。

我相信C++标准应该被编辑成明确析构函数是否可以具有 virt-specifier override 的方式。目前还不清楚。


我测试的编译器报错......这正是“override”的关键所在。你用一个非虚析构函数编写了基类;编译器的行为是正确的。 - Kyle Strand
@KyleStrand 我删掉了关于编译器的注释,因为我可以使用旧版本的编译器。 - Vlad from Moscow
我怀疑你没有理解我的意思。你能否将你的代码复制粘贴到一个在线编译器(例如Coliru)中? - Kyle Strand

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接