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

31

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

然而,他的第二个例子是“具有虚成员的虚析构函数”(只是这样)。 这是什么意思? 我知道必须声明基类的析构函数为虚函数,以便在出现像下面这样的情况时调用正确的析构函数:

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

这里有一个很好的解释:何时使用虚析构函数? 但是如果您有虚成员,现在在C++11中声明虚析构函数是否已经无用了?

8
这只是一个猜测,但使用智能指针时,你可以安排好使正确的析构函数被调用,即使基类的析构函数不是虚拟的。这在使用shared_ptr时很容易实现。 - juanchopanza
2
有关三大法则或需要虚析构函数已经不再必要的说法纯属谬论。新功能都没有改变这方面的任何情况。 - James Kanze
关于三法则,作者可能也意味着它已经过时了,因为现在是四/五法则。对于零法则,我真的认为它很有用,但前提是你的类对其拥有的所有资源都使用RAII原则。 - Florian Richoux
@FlorianRichoux 并不是所有的类都需要支持移动操作,这会增加额外的复杂性。(而且使使用必须被释放的资源的类正常工作通常需要比通常的RAII类更多的工作。) - James Kanze
1
@JamesKanze,你没有理解重点。如果你的所有非平凡成员都有正确的析构函数或是一个带有适当删除器的RAII类型(无论是仅删除内存还是执行更复杂的清理),那么_支持移动就没有额外的复杂性_。你只需要写Foo(Foo&&) = default;,它就可以正常工作了。并且因为你的所有成员都会自我清理,所以你也可以默认析构函数。这需要一种不同的类设计方法,但这正是Sommerlad教授在这些幻灯片中提倡的方法。(虚拟位我不确定,我会问他。) - Jonathan Wakely
显示剩余2条评论
4个回答

37

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

如果您编写代码明确分配使用基类指针销毁Derived实例的newdelete,那么您需要定义一个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 多态性在许多系统中被过度使用。我们不应该像 Java 一样编程,除非我们有一个问题,需要使用堆分配的对象进行动态多态性解决方案。


我经常重构我的继承结构,有时最终的基类会是其他类,如果使用shared_ptr<Base> pb=make_shared<Derived>(),那么这种情况该如何处理? - Jojje
我不确定我是否正确理解了您的问题。如果BaseDerived的基类,那么我的论点仍然有效。但是,如果BaseDerived完全无关,则这段代码不应该编译。 - PeterSom
2
我认为,如果一个类被设计成具有多态性,但没有定义虚析构函数,会给类的使用者带来很大的负担——他们必须严格使用 shared_ptr 来持有该类。但是,shared_ptr 被强烈反对并认为过度使用,应尽可能用 unique_ptr 替换。因此,我认为不定义虚析构函数会导致比接受必须将复制和移动构造函数以及赋值运算符标记为 =default 更严重的问题。我认为 C++11 对于何时以及如何使用虚析构函数没有改变任何东西。 - HiFile.app - best file manager
2
这似乎不是很好的建议 - 在类声明时节省了微不足道的(精神)开销,而以一种相当意外的方式限制了客户端使用,从而施加了非微不足道的(精神)开销。您还在交换一个虚拟查找的小开销,一旦对象被销毁就会发生,与...一次小的虚拟查找。对我来说,这似乎并不是很有帮助。 - Cubic
该建议仅适用于作者所描述的特定条件。然而,幻灯片中的陈述给人的印象是C++11在虚析构函数方面改变了行为,这并不是事实。这种“概括”的说法非常误导。 - ap-osd

2
我认为这与演示中其他地方提到的“零规则”有关。
如果您只有自动成员变量(即对于原始指针,使用shared_ptrunique_ptr),那么您不需要编写自己的复制或移动构造函数或赋值运算符 - 编译器提供的默认值将是最佳的。通过类内初始化,您也不需要默认构造函数。最后,您根本不需要编写析构函数,虚拟的或非虚拟的。

是的,但根据Scott Meyers的说法,明确声明复制/移动构造函数、复制/移动赋值运算符和析构函数为“默认”仍然是首选(http://scottmeyers.blogspot.fr/2014/03/a-concern-about-rule-of-zero.html)。因此,遵循这个修改后的零规则,我猜仍然需要声明基类析构函数为虚拟的。 - Florian Richoux
有点傻,如果某处有虚成员,则没有虚析构函数是未定义行为;但如果没有虚成员,则拥有虚析构函数是浪费的。这很脆弱;是否有任何理由不应该在已经有虚表的类中“自动”将析构函数设置为虚函数,在其他类中则设置为非虚函数? - M.M
1
我认为Scott Meyers在讨论“零规则”时过于固执于他自己的过去。我试图尽可能地保持简单。定义通常由编译器提供的特殊成员函数(正确地!)应该是留给库专家完成的功能,而不是发生在大多数C ++程序员创建的常规代码中的事情。 - PeterSom
1
@Matt McNabb:如果你遵循我的规则,那么你就不会在没有虚析构函数的情况下得到UB,并且你永远不会陷入编写虚析构函数以产生不必要开销的境地。 - PeterSom
2
你的规则是只使用shared_ptr指向多态对象?好的,尽管我更喜欢类定义本身就是正确的,而不依赖于用户使用特定的习惯用语。有时候那些用户会做一些奇怪的事情... - M.M
+1 Mcnabb提到了定义类和使用类之间的区别,非常好。如果定义一个必须使用特定ptr类型的类,那将是很糟糕的形式。 - Mike

0

回答具体问题...

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

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

幻灯片中的陈述给人的印象是,C++11在虚拟析构函数方面的行为发生了变化 - 这并不是事实。正如作者所澄清的那样,它仅适用于使用shared_ptr时。但是除了使用shared_ptr之外,仍然需要虚拟析构函数的事实在长时间的解释中被淡化了。


0

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

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

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

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


1
我明白了,确实很有道理。感谢你和juanchopanza! - Florian Richoux
1
我仍然会将析构函数声明为虚函数。这样做不会有任何影响,而且如果有人以旧的方式使用你的类,它仍然可以正常工作。 - Danvil
4
这样做是不行的,只会调用基础析构函数: 在这里展示。移动操作并不改变接收器的类型,删除器是其组成部分。这需要像shared_ptr一样进行类型抹除。 - galop1n
1
@galop1n:说得好,我试图反向工程化论文的论点,但它似乎过于脆弱。我认为在简单的面向对象情况下,您不需要完全擦除shared_ptr的类型,但unique_ptr提供的确实是不足的。 - MSalters
1
@Danvil 我也会使用虚析构函数,但是这可能会有风险。如果一个类型还没有多态性,使用虚析构函数可能会引入开销并且潜在地改变运行时语义(typeiddynamic_cast)。 - sehe
显示剩余4条评论

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