When should you not use virtual destructors?
是否有充分的理由不为类声明虚析构函数?你应该在什么时候特别避免写?
当以下任何一项为真时,不需要使用虚拟析构函数:
- 不打算从中派生类
- 堆上没有实例化
- 不打算存储在超类的指针中
除非你真的需要记忆,否则没有具体的理由来避免它。
要明确回答这个问题,即何时不应该声明虚拟析构函数。
C++ 98/03
添加虚拟析构函数可能会将类从pod(普通旧数据)*或聚合更改为非pod。如果类类型在某个地方被聚合初始化,则这可以阻止项目编译。
1 2 3 4 5 6 7 8 | struct A { // virtual ~A (); int i; int j; }; void foo () { A a = { 0, 1 }; // Will fail if virtual dtor declared } |
在极端情况下,此类更改还可能导致未定义的行为,其中类的使用方式需要POD,例如通过省略号参数传递它,或将它与memcpy一起使用。
1 2 3 4 | void bar (...); void foo (A & a) { bar (a); // Undefined behavior if virtual dtor declared } |
[*pod类型是对其内存布局有特定保证的类型。标准实际上只说明,如果您要从pod类型的对象复制到一个字符数组(或无符号字符)中,然后再次复制,那么结果将与原始对象相同。]
现代C++
在C++的最新版本中,POD的概念在类的布局和它的构造、复制和破坏之间分离。
对于省略号的情况,它不再是未定义的行为,它现在用实现定义的语义(N39 32-~c++)14 -5.2.2/7有条件地支持:
...Passing a potentially-evaluated argument of class type (Clause 9) having a non-trivial copy constructor, a non-trivial move constructor, or a on-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.
声明一个除
... A destructor is trivial if it is not user-provided ...
当添加构造函数时,对现代C++的其他更改减少了聚合初始化问题的影响:
1 2 3 4 5 6 7 8 9 10 | struct A { A(int i, int j); virtual ~A (); int i; int j; }; void foo () { A a = { 0, 1 }; // OK } |
当且仅当我有虚拟方法时,我声明一个虚拟析构函数。一旦我有了虚拟方法,我就不相信自己可以避免在堆上实例化它或存储指向基类的指针。这两种操作都是非常常见的操作,如果析构函数不是声明为虚拟的,那么通常会悄悄地泄漏资源。
只要有可能在指向类类型的子类对象的指针上调用
1 2 | A *x = new B; delete x; // ~B() called, even though x has type A* |
如果您的代码不是性能关键的,那么为了安全起见,为编写的每个基类添加一个虚拟析构函数是合理的。
但是,如果您发现自己在一个紧密的循环中拥有许多对象,那么调用虚拟函数(甚至是空函数)的性能开销可能会很明显。编译器通常不能内联这些调用,处理器可能很难预测到哪里去。这不太可能对性能产生重大影响,但值得一提。
并非所有C++类都适合用作具有动态多态性的基类。
如果您希望类适合于动态多态性,那么它的析构函数必须是虚拟的。此外,子类可能希望重写的任何方法(可能意味着所有公共方法,以及可能在内部使用的某些受保护方法)都必须是虚拟的。
如果您的类不适合动态多态性,那么析构函数就不应该标记为虚拟的,因为这样做会误导您。它只是鼓励人们错误地使用你的课程。
下面是一个不适合动态多态性的类的例子,即使它的析构函数是虚拟的:
1 2 3 4 5 6 7 8 9 | class MutexLock { mutex *mtx_; public: explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); } ~MutexLock() { mtx_->unlock(); } private: MutexLock(const MutexLock &rhs); MutexLock &operator=(const MutexLock &rhs); }; |
这门课的重点是让学生坐在书堆上。如果您传递指向此类对象的指针,更不用说它的子类了,那么您就错了。
虚函数意味着每个分配的对象都会通过虚函数表指针增加内存成本。
因此,如果您的程序涉及到分配大量的某个对象,那么为了节省每个对象额外的32位,应该避免使用所有虚拟函数。
在所有其他情况下,您将为自己节省调试痛苦,使dtor成为虚拟的。
不将析构函数声明为virtual的一个很好的原因是,这样可以避免类中添加了虚函数表,并且应该尽可能避免这样做。
我知道很多人更喜欢将析构函数声明为虚拟的,只是为了安全起见。但是如果你的类没有任何其他的虚拟函数,那么拥有一个虚拟析构函数就没有意义了。即使你把你的类交给其他人,然后他们从中派生出其他类,他们也没有理由在向上投射到你的类的指针上调用delete——如果他们这样做了,我会认为这是一个bug。
好吧,有一个单一的例外,即如果你的类(mis-)被用来执行派生对象的多态删除,那么你——或者其他人——希望知道这需要一个虚拟析构函数。
换句话说,如果你的类有一个非虚拟析构函数,那么这是一个非常清楚的语句:"不要用我来删除派生对象!"
如果您有一个非常小的类和大量的实例,vtable指针的开销会对程序的内存使用产生影响。只要类没有任何其他虚拟方法,使析构函数成为非虚拟的将节省开销。
如果您绝对必须确保类没有vtable,那么您也不能有虚拟析构函数。
这是一种罕见的情况,但确实发生了。
这样做的模式最常见的例子是directx d3dvector和d3dmatrix类。这些是类方法,而不是语法上的函数,但是为了避免函数开销,类故意不具有vtable,因为这些类专门用于许多高性能应用程序的内部循环。
我通常将析构函数声明为虚拟的,但是如果您在内部循环中使用了性能关键的代码,那么您可能希望避免虚拟表查找。在某些情况下,这可能很重要,比如碰撞检查。但是,如果使用继承,那么要小心如何销毁这些对象,否则只会销毁对象的一半。
请注意,如果对象上的任何方法是虚拟的,则会对该对象进行虚拟表查找。因此,如果类中有其他虚拟方法,则没有必要删除析构函数的虚拟规范。
在将在基类上执行的操作上,并且该操作的行为应该是虚拟的。如果删除可以通过基类接口多态地执行,那么它必须表现为虚拟的并且是虚拟的。
如果不打算从类派生,则析构函数不需要是虚拟的。即使这样做了,如果不需要删除基类指针,受保护的非虚拟析构函数也是一样好的。
我所知道的唯一一个性能答案是有机会成为真实的。如果你已经测量并发现去虚拟化你的析构函数真的会加快速度,那么你可能在这个类中还有其他的东西也需要加速,但是在这一点上有更重要的考虑。有一天,有人会发现您的代码将为他们提供一个很好的基类,并为他们节省一周的工作。你最好确保他们完成那周的工作,复制和粘贴你的代码,而不是把你的代码作为基础。你最好确保你的一些重要方法是私有的,这样就不会有人从你那里继承。