使用非虚拟析构函数有啥具体原因吗?

Posted

技术标签:

【中文标题】使用非虚拟析构函数有啥具体原因吗?【英文标题】:Are there any specific reasons to use non-virtual destructors?使用非虚拟析构函数有什么具体原因吗? 【发布时间】:2012-01-31 15:25:24 【问题描述】:

据我所知,任何被指定为具有子类的类都应该用虚拟析构函数声明,这样在通过指针访问它们时可以正确地销毁类实例。

但是为什么甚至可以用非虚拟析构函数声明这样的类呢?我相信编译器可以决定何时使用虚拟析构函数。那么,这是 C++ 设计疏忽,还是我遗漏了什么?

【问题讨论】:

+1。我想问一个类似的问题:如果一个基类有一个virtual 函数,那么为什么我们仍然需要将析构函数设为虚拟?为什么不编译器自己把它变成/认为它是虚拟的? 派生(子类)析构函数时,什么都不做。 见Raymond Chen's blog。 另见***.com/questions/7403883/… 【参考方案1】:

是否有任何使用非虚拟析构函数的具体原因?

是的,有。

主要归结为性能。虚函数不能被内联,相反,您必须首先确定要调用的正确函数(这需要运行时信息),然后再调用该函数。

在性能敏感的代码中,无代码和“简单”函数调用之间的区别可能会有所不同。与许多语言不同,C++ 并不认为这种差异是微不足道的。

但是为什么甚至可以用非虚拟析构函数声明这样的类呢?

因为(对于编译器)很难知道类是否需要虚拟析构函数。

在以下情况下需要虚拟析构函数:

您在指针上调用 delete 通过基类到派生对象

当编译器看到类定义时:

它无法知道您打算从此类派生 - 毕竟您可以从没有虚拟方法的类中派生 但更令人生畏:它无法知道您打算在此类上调用delete

许多人认为多态性需要更新实例,这只是缺乏想象力:

class Base  public: virtual void foo() const = 0; protected: ~Base()  ;

class Derived: public Base 
  public: virtual void foo() const  std::cout << "Hello, World!\n"; 
;

void print(Base const& b)  b.foo(); 

int main() 
  Derived d;
  print(d);

在这种情况下,不需要为虚拟析构函数付费,因为在析构时不涉及多态性。

归根结底,这是一个哲学问题。在可行的情况下,C++ 默认选择性能和最少的服务(主要的例外是 RTTI)。


关于警告。有两个警告可以用来发现问题:

-Wnon-virtual-dtor (gcc, Clang):每当具有虚函数的类未声明虚析构函数时都会发出警告,除非基类中的析构函数为protected。这是一个悲观的警告,但至少你不会错过任何东西。

-Wdelete-non-virtual-dtor(Clang,也移植到 gcc):每当在指向具有虚函数但没有虚析构函数的类的指针上调用 delete 时发出警告,除非该类标记为final。它的误报率为 0%,但会发出“迟到”的警告(可能会多次)。

【讨论】:

@Nawaz:感谢您的通知,它允许我编辑并注意 gcc 现在已收到我的小警告 :)【参考方案2】:

为什么析构函数默认不是虚拟的? http://www2.research.att.com/~bs/bs_faq2.html#virtual-dtor

准则 #4:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的。 http://www.gotw.ca/publications/mill18.htm

另见:http://www.erata.net/programming/virtual-destructors/

编辑:可能重复? When should you not use virtual destructors?

【讨论】:

【参考方案3】:

你的问题基本上是这样的,“如果类有任何虚拟成员,为什么 C++ 编译器不强制你的析构函数是虚拟的?”这个问题背后的逻辑是,应该将虚拟析构函数与它们打算派生的类一起使用。

C++ 编译器试图超越程序员的原因有很多。

    C++ 的设计原则是物有所值。如果你想要一些虚拟的东西,你必须要求它。明确的。虚拟类中的每个函数都必须显式声明(除非它覆盖基类版本)。

    如果具有虚拟成员的类的析构函数自动设为虚拟,那么如果这是您想要的,您会如何选择使其非虚拟? C++ 没有能力显式声明非虚拟方法。那么您将如何覆盖这种编译器驱动的行为。

    对于具有非虚拟析构函数的虚拟类是否有特定的有效用例?我不知道。也许某处有一个退化的案例。但如果你出于某种原因需要它,你将无法根据你的建议说出来。

您真正应该问自己的问题是,当具有虚拟成员的类没有虚拟析构函数时,为什么更多的编译器不会发出警告。毕竟,这就是警告的用途。

【讨论】:

我有点同意警告可能是一个好主意——再说一次,在实践中你也会让人们感到困惑和/或抱怨他们,就像 GCC 的“类有虚拟函数和可访问的非虚拟析构函数”:***.com/questions/5827719/…;不确定解决方案是什么——编译器的建议和理由? “请编写安全代码”或者引用我之前发布的 GotW 中的指南 #4 :-)【参考方案4】:

当一个类毕竟是非虚拟的(注 1)时,非虚拟析构函数似乎是有意义的。

但是,我没有看到非虚拟析构函数有任何其他好的用途。

我很欣赏这个问题。非常有趣的问题!

编辑:

注 1: 在性能关键的情况下,最好使用没有任何虚函数表的类,因此根本没有任何虚析构函数。

例如:考虑一个只包含三个浮点值的class Vector3。如果应用程序存储了它们的数组,那么该数组可以以紧凑的方式存储。

如果我们需要一个虚函数表,并且如果我们甚至需要在堆上存储(如在 Java & co. 中),那么该数组将只包含指向内存中“SOMEWHERE”实际元素的指针。

编辑 2:

我们甚至可能有一个完全没有任何虚拟方法的类的继承树。

为什么?

因为,即使拥有“虚拟”方法似乎是常见且可取的情况,这也不是我们人类可以想象的唯一情况。

与该语言的许多细节一样,C++ 为您提供了选择。您可以选择提供的选项之一,通常您会选择其他人选择的选项。但有时你不想要那个选项!

在我们的示例中,类 Vector3 可以从类 Vector2 继承,并且仍然没有虚函数调用的开销。想想,那个例子不是很好;)

【讨论】:

【参考方案5】:

我在这里没有提到的另一个原因是 DLL 边界:您想使用相同的分配器来释放您用来分配它的对象。

如果方法存在于 DLL 中,但客户端代码使用直接 new 实例化对象,则客户端的分配器用于获取对象的内存,但对象使用来自DLL,它指向一个析构函数,该析构函数使用 DLL 链接的分配器来释放对象。

当在客户端从 DLL 子类化类时,问题就消失了,因为 DLL 中的虚拟析构函数没有被使用。

【讨论】:

析构函数不会释放内存。它由释放内存的函数调用。如果类重载 new() 和 delete() 运算符,您的答案可能是正确的,否则,我认为不是。 如果派生类覆盖operator delete,那么通过基指针销毁对象的代码不知道,所以要么你发明一种机制让析构函数返回内存是否已经被释放,或者让析构函数直接调用释放函数。 G++ 和 MSVC 都是后者。

以上是关于使用非虚拟析构函数有啥具体原因吗?的主要内容,如果未能解决你的问题,请参考以下文章

cpp中的析构函数会自动调用吗?即使析构函数没有提及非动态变量,它们是不是也会被删除?

具有虚拟析构函数的基类子类中的默认析构函数

模板化的指针类可以有虚拟析构函数吗?

每个类都应该有一个虚拟析构函数吗?

c++中,析构函数和delete各有啥作用啊

虚拟析构函数是继承的吗?