C++11 中具有虚拟成员的虚拟析构函数

Virtual destructor with virtual members in C++11

本文关键字:虚拟 析构函数 虚拟成员 C++11      更新时间:2023-10-16

在这些关于C++11/14标准的幻灯片中,在第15张幻灯片上,作者写道,"许多经典的编码规则不再适用"C++11。他提出了三个例子的清单,我同意三法则和内存管理。

然而,他的第二个例子是"具有虚拟成员的虚拟析构函数"(仅此而已)。什么意思?我知道必须将基类析构函数声明为虚拟,以便调用正确的析构函数,如果我们有类似的东西

Base *b = new Derived;
...
delete b;

这里对此有很好的解释:何时使用虚拟析构函数?

但是,如果您有虚拟成员,现在在 C++11 中声明虚拟析构函数是没有用的吗?

作为幻灯片的作者,我将尝试澄清。

如果您编写的代码显式分配带有 newDerived 实例并使用基类指针通过 delete 销毁它,则需要定义一个virtual析构函数,否则最终会导致不完全销毁Derived实例。但是,我建议完全放弃newdelete,并专门使用shared_ptr来引用堆分配的多态对象,例如

shared_ptr<Base> pb=make_shared<Derived>();

这样,共享指针会跟踪要使用的原始析构函数,即使shared_ptr<Base>用于表示它。一旦最后一个引用shared_ptr超出范围或被重置,将调用~Derived()并释放内存。因此,您无需~Base()虚拟

unique_ptr<Base>make_unique<Derived> 不提供此功能,因为它们不提供与删除程序相关的shared_ptr机制,因为唯一指针要简单得多,并且旨在实现最低开销,因此不会存储删除程序所需的额外函数指针。对于unique_ptr,删除器函数是类型的一部分,因此具有引用~Derived的删除器的uniqe_ptr将与使用默认删除器的unique_ptr<Base>不兼容,如果~Base不是虚拟的,这对于派生实例来说无论如何都是错误的。

我提出的个人建议旨在易于遵循和一起遵循。他们尝试通过让库组件和编译器生成的代码完成所有资源管理来生成更简单的代码。

在类中定义(虚拟)析构函数将禁止编译器提供的移动构造函数/赋值运算符,并且可能还会禁止编译器提供的复制构造函数/赋值运算符在C++的未来版本中。使用=default复活它们变得很容易,但看起来仍然像很多样板代码。最好的代码是你不必编写的代码,因为它不会出错(我知道该规则仍有例外)。

总结一下"不要定义(虚拟)析构函数"作为我的"零法则"的推论:

每当在现代C++中设计多态 (OO) 类层次结构并希望/需要在堆上分配其实例并通过基类指针访问它们时,请使用 make_shared<Derived>() 来实例化它们并shared_ptr<Base>保留它们。这使您可以保留"零规则"。

这并不意味着您必须在堆上分配所有多态对象。例如,定义一个以(Base&)为参数的函数,可以使用局部Derived变量毫无问题地调用,并且对于Base的虚成员函数将表现为多态。

在我看来,动态OO多态性在许多系统中被严重过度使用。当我们使用C++时,我们不应该像Java那样编程,除非我们遇到问题,其中堆分配对象的动态多态性是正确的解决方案。

我认为

这与演示文稿中其他地方提到的"零规则"有关。

如果您只有自动成员变量(即对否则将是原始指针的成员使用 shared_ptrunique_ptr),那么您不需要编写自己的副本或移动构造函数或赋值运算符 - 编译器提供的默认值将是最佳的。使用类内初始化,也不需要默认构造函数。最后,你根本不需要编写析构函数,无论是否虚拟。

链接的论文显示了相关代码:

std::unique_ptr<Derived> { new Derived };

存储的删除器是std::default_delete<Derived>的,这不需要Base::~Base是虚拟的。

现在你可以把它移到 unique_ptr<Base> ,它还将移动std::default_delete<Derived>而不将其转换为std::default_delete<Base> .

要回答具体问题...

但是,如果您有虚拟成员,现在在 C++11 中声明虚拟析构函数是否毫无用处?

在 C++11 核心语言中,对虚拟析构函数的需求没有改变。如果要使用基指针删除派生对象,则必须将析构函数声明为 virtual。

幻灯片中的陈述给人的印象是,C++11 以某种方式改变了与虚拟析构函数相关的行为 - 事实并非如此。正如作者所澄清的那样,它仅适用于使用 shared_ptr .但是仍然需要一个虚拟析构函数的事实(除了使用shared_ptr)在冗长的解释中被稀释了。