Virtual destructor with virtual members in C++11
在有关C++ 11/14标准的幻灯片中,在幻灯片15上,作者写道"C++中的许多经典编码规则不再适用"。他提出了三个例子的清单,我同意三法则和记忆管理。
然而,他的第二个例子是"带有虚拟成员的虚拟析构函数"(仅此而已)。这是什么意思?我知道必须将基类析构函数声明为虚的,以便在有类似
1 2 3
| Base *b = new Derived;
...
delete b; |
这里很好地解释了:何时使用虚拟析构函数?
但是,如果你有虚拟成员,C++ 11中声明虚拟析构函数是没有用的吗?
- 这只是一个猜测,但是当使用智能指针时,您可以安排一些事情,以便调用右析构函数,即使基析构函数不是虚拟的。这种情况发生在使用shared_ptr的情况下。
- 事实上,三条规则或对虚拟析构函数的需求不再是必要的,这种说法只是错误的。在这方面,没有任何新功能改变过。
- @关于詹姆斯坎兹的"三法则",作者也可以说是不赞成,因为现在它是"四/五法则"。对于零规则,我真的相信它是有用的,但前提是您的类对它们拥有的所有资源都使用raii原则。
- @弗洛里安里奇不是真的。大多数类不需要支持移动的额外复杂性。(而使一个使用必须释放的资源的类正常工作通常需要比一般的raii类所能做的多得多。)
- @Jameskanze,你错过了重点。如果所有重要的成员都有一个正确的析构函数或者是一个带有合适的删除程序的raii类型(无论是只删除内存还是执行更复杂的清理),那么就没有额外的复杂度来支持move。你说的是Foo(Foo&&) = default;,它就行了?.并且因为您的所有成员都将自己清理干净,所以您也可以默认使用析构函数。这需要一种不同的课堂设计方法,但这正是Sommerlad教授在这些幻灯片中提倡的方法。(不过,我不确定虚拟位,我会问他。)
- @弗洛里安里奇:彼得森的回答对我来说似乎是完美的…
- @梅萨,真的!我在询问有关这些幻灯片的详细信息,并从作者本人那里得到答案。:)我会在奖励结束后(明天)用奖励和最好的回答来奖励他的回答。我怀疑会有更好的答案…
作为幻灯片的作者,我将尽力澄清。
如果用new显式地分配Derived实例,并用基类指针用delete销毁该实例,则需要定义virtual析构函数,否则将导致无法完全销毁Derived实例。但是,我建议完全不使用new和delete,只使用shared_ptr来指堆分配的多态对象,例如
1
| shared_ptr<Base> pb=make_shared<Derived>(); |
这样,共享指针就可以跟踪要使用的原始析构函数,即使使用shared_ptr来表示它。一旦最后一个引用的shared_ptr超出范围或被重置,将调用~Derived()并释放内存。因此,您不需要使~Base()虚拟化。
unique_ptr和make_unique不提供此功能,因为它们不提供shared_ptr关于删除程序的机制,因为唯一指针简单得多,旨在降低开销,因此不存储删除程序所需的额外函数指针。使用unique_ptr时,deleter函数是类型的一部分,因此,带有引用~Derived的deleter的uniqe ptr将与使用默认deleter的unique_ptr不兼容,如果~Base不是虚拟的,这对于派生实例来说是错误的。
我提出的个人建议,是为了易于遵循和遵循。他们试图通过让库组件和编译器生成的代码完成所有资源管理来生成更简单的代码。
定义类中的(虚)析构函数,将禁止编译器提供的移动构造函数/赋值操作符,并且可能禁止编译器提供的复制构造函数/赋值操作符在C++的未来版本中。使用=default可以很容易地恢复它们,但看起来仍然有很多样板代码。最好的代码是您不必编写的代码,因为它不会出错(我知道这条规则还有例外)。
总结一下"不要将(虚拟)析构函数"定义为我的"零规则"的推论:
每当你在现代C++中设计一个多态(OO)类层次结构,并且想要/需要在堆上分配它的实例并通过基类指针访问它们时,使用EDCOX1(27)来实例化它们和EDCOX1,15来保持它们。这允许你保持"零规则"。
这并不意味着您必须分配堆中的所有多态对象。例如,定义一个以(Base&)为参数的函数,可以用一个局部Derived变量毫无问题地调用,并且对于Base的虚拟成员函数,它将表现出多态性。
在我看来,动态OO多态性在许多系统中被过度使用。当我们使用C++时,我们不应该像Java那样编程,除非我们遇到一个问题,其中堆分配对象的动态多态性是正确的解决方案。
- 我经常重构我的继承结构,有时会以一些其他类作为最终的基类,在使用共享的模型时,如何处理这个问题?
- 我不确定,我理解你的担心。如果Base是Derived的基类,我的论点仍然有效。但是,如果Base与Derived完全无关,则不应编译。
- 好的,谢谢。然后编译器会告诉我。
- 我认为,没有为一个类定义虚拟析构函数,而这个类将被多态地使用,这会给类的用户带来很大的负担——他们被严格要求用共享的指针来保存它们。但是,共享资源非常不受欢迎,被认为是过度使用的,应该尽可能用唯一资源替换。因此,我认为不定义虚拟析构函数会导致比接受必须将复制和移动构造函数和赋值运算符标记为默认值这一事实更糟糕的问题。我认为C++ 11改变了何时使用虚拟析构函数以及如何使用虚拟析构函数。
- 这似乎不是一个很好的建议——你在类声明中节省了少量的(精神上的)开销,而通过以一种相当意想不到的方式限制客户机的使用来施加非日常(精神上的)开销。当一个对象被破坏时,你还需要花费一点虚拟查找的开销。对象被破坏后的一种小型虚拟查找。这对我似乎没什么帮助。
我认为这与演示文稿中其他地方提到的"零规则"有关。
如果您只有自动成员变量(即对于原本是原始指针的成员使用shared_ptr或unique_ptr),那么您不需要编写自己的复制或移动构造函数或赋值运算符——编译器提供的默认值将是最佳的。对于类内初始化,也不需要默认的构造函数。最后,您根本不需要编写一个析构函数,不管是虚拟的还是非虚拟的。
- 是的,但根据Scott Meyers的说法,最好将复制/移动目录、复制/移动分配运算符和析构函数明确声明为default(scott meyers.blogspot.fr/2014/03/&hellip;)。因此,按照这个修改过的零规则,我想仍然需要声明基析构函数是虚拟的。
- 如果某个地方有一个虚拟成员,那么没有虚拟析构函数是ub,这有点愚蠢;但是如果没有虚拟成员,那么拥有虚拟析构函数是浪费的。这是很脆弱的;有什么理由说明,在已经有vtable的类中,destructors不应该"自动地"是虚拟的,而在其他类中是非虚拟的?
- 我相信斯科特·迈耶斯在讨论"零规则"时过于坚持自己的过去。我正在努力使事情尽可能简单。定义通常由编译器提供的特殊成员函数(正确!)应该是图书馆专家留下的特色,而不是大多数C++程序员创建的常规代码中发生的事情。
- @马特·麦克纳布:如果你遵循我的规则,那么没有虚拟析构函数你就不会得到UB,而且你也永远不会来这里写虚拟析构函数来产生不必要的开销。
- "你的规则"是只使用shared_ptr来指向多态对象?好吧,尽管如果类定义本身是正确的,而不依赖用户使用特定的习惯用法,我还是更高兴。那些用户有时会做一些奇怪的事情…
- +1 mcnabb,用于说明定义类和使用类之间的区别。定义一个必须与某个ptr类型一起使用的类是不好的形式。
链接的纸张显示相关代码:
1
| std::unique_ptr<Derived> { new Derived }; |
存储的删除程序是std::default_delete,它不要求Base::~Base是虚拟的。
<罢工>现在你可以把它移到一个unique_ptr,它也会移动std::default_delete,而不把它转换成std::default_delete。< /打击>
- 我明白了,这确实有道理。感谢你和Juanchopanza!
- 实际上,这只适用于std::shared_ptr,而不是std::unique_ptr。
- @Milleniumbug:根据纸张进行校正。
- 我仍然会使析构函数成为虚拟的。它不会伤害你,如果有人用老方法使用你的课程,它仍然有效。
- 这不起作用,只调用基析构函数:show here。移动不会更改接收器的类型,并且删除器是其中的一部分。它需要像其他共享资源一样的类型擦除。
- @很好的一点,我正试图对报纸所争论的内容进行逆向工程,但它似乎太脆弱了。我认为对于简单的OO案例,您不需要完全删除shared_ptr,但unique_ptr提供的内容确实不够。
- @Danvil我也会使用虚拟DTORS,但它可能会有伤害。如果还没有,它可以使类型具有多态性,从而引入开销和潜在的更改运行时语义(typeid和dynamic_cast)。
- LukasZ1985:大多数情况下,这就是为什么C++程序员通常跳过整个指针部分(智能或不完全)。但是有make_shared来降低开销。
- 这里实际上存在一个问题,因为一些编码准则会坚持将派生类的破坏者声明为私有的(这样您就不会意外地在堆栈上创建实例)。