有没有具体的理由使用非虚拟析构函数?

时间:2012-01-02 05:59:40

标签: c++ language-design virtual-destructor

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

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

5 个答案:

答案 0 :(得分:18)

  

使用非虚拟析构函数是否有任何特定原因?

是的,有。

主要是归结为性能。无法内联虚函数,而必须首先确定要调用的正确函数(这需要运行时信息),然后调用该函数。

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

  

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

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

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

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

当编译器看到类定义时:

  • 它无法知道你打算从这个类派生 - 毕竟你可以从没有虚方法的类派生出来
  • 但更令人生畏的是:它无法知道您打算在此课堂上调用delete

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

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%,但警告“迟到”(可能是几次)。

答案 1 :(得分:3)

为什么析构函数默认不是虚拟的? 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?

答案 2 :(得分:2)

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

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

  1. C ++的设计原则是获得您所支付的费用。如果你想要虚拟的东西,你必须要求它。明确。必须显式声明类中的每个虚函数(除非它覆盖基类版本)。

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

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

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

答案 3 :(得分:1)

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

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

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

修改

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

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

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

编辑2:

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

为什么?

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

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

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

答案 4 :(得分:1)

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

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

当从客户端的DLL中继承类时,问题就会消失,因为不使用DLL中的虚析构函数。