Virtual Destructors in C++. Necessity, Good Practice, Bad Practice
One common misuse of C++ features is declaring virtual destructors for all classes, including those that do not utilize runtime polymorphism (e.g. data wrapper, container, concurrency control classes, etc.). For a C++ developer working on a common codebase, it is likely a class declaration like the following has been encountered:
Because of the frequency of this misuse, it is even possible to encounter author comments next to non-virtual destructor declarations explaining why these destructors are declared as non-virtual, warning others who may modify the code that this was intentional.
Frequently, the subject of virtual destructors has been discussed online addressing the common questions: “Should every class/Does every class have to have a virtual destructor?” and “When (not) to use virtual destructors?” Depending on whether the question seeks advice or clarification regarding the necessity of virtual destructors, many answers detail when its use is recommended, not recommended, required, or prohibited. The outcomes of declaring destructors virtual, organized into 3 categories and based on their utilization of runtime polymorphism, after closer examination, provides proof that declaring all destructors virtual is an improper practice.
Type 1: Runtime polymorphism with polymorphic destruction
These types of classes exhibit the true use case of virtual destructors. Consider the following pseudo-code:
Declaring the base class destructor as virtual ensures the call of the derived class destructor, when an object of the derived class type is destroyed via a pointer or a reference of the base class type. If Base1::~Base1
is non-virtual, executing the delete expression would invoke Base1::~Base1
, but not Derived1::~Derived1
, which would result in a leak of resources allocated by Derived1
other than those inherited from Base1
.
Well, there is not much more to say... In such a situation, using virtual destructors is a necessity for the utilization of runtime polymorphism and, in fact, this is exactly what they are used for.
Type 2: Runtime polymorphism without polymorphic destruction
What if runtime polymorphism is already in use (i.e. the base class has at least one virtual function that is not its destructor), but polymorphic destruction is not intended in the class design? In the following example, the method of destruction from the previous example has been changed to justify such a scenario:
Because runtime polymorphism is not used in the destruction of derived class instances, allowing the base class destructor to be non-virtual does not prevent the derived class destructor from being called. There is no concern for resource leaks due to missed destructor calls.
Since virtual destructors are not necessary for this case, what are the implications of its use? What is the runtime cost of such virtual function calls?
Runtime cost of a virtual function call: Although the implementation method of virtual functions is not defined by the language specification, compilers generally use a data structure called a virtual function table (vtable), an instance of which is created for each class type that declares or inherits at least one virtual function. For each virtual function of the class, there exists a row in the vtable that contains a pointer to that function. Each instance of a class with a vtable holds a pointer to this vtable as a hidden data member. It is through this pointer to the vtable, that virtual function calls through pointers and references of base class types are directed to the function of the actual object’s type. That is why a call to a virtual function effectively resembles something like the following statement:
Also worth mentioning: it is less likely for virtual functions to be inlined, than non-virtual functions.
It is obvious, that the cost of using a virtual function mechanism is not always negligible. On the other hand, for classes in a hierarchy that already utilize this mechanism (e.g. Base2
and Derived2
in the code example), the toll for a class level virtual function table and a pointer to this table in each class instance has already been paid. The extra cost of declaring a base class destructor can be expected as an additional row in the class vtables, as well as access to the appropriate vtable and row during the destructor call.
For most cases, these additional costs are considered relatively low. Therefore, when implementing a design with runtime polymorphism, but without polymorphic destruction of derived class objects, declaring destructors virtual is a good practice as a safety mechanism for unintended polymorphic destruction through client mistake. (Please note, destructors of base classes can be declared in the protected class scope in order to prevent compilation of such client code, if public scope is not required by design.)
Type 3: No runtime polymorphism
Still remaining, are all the other classes that do not use runtime polymorphism. In other words, those which do not need any virtual functions. Some classes are not designed to be base classes at all, as exhibited in SomeDataWrapper
from the first code snippet. Furthermore, a base class may not use runtime polymorphism, as shown in the following example:
Declaring virtual destructors for these classes can incur costs for the virtual function mechanism, although they will not be utilized. The runtime costs of virtual destructors have already been discussed for Type 2 classes. However, these costs are not the same for classes without any other virtual functions.
Below is a list of the potential disadvantages of declaring the destructor virtual in a class not utilizing runtime polymorphism, the importance of each depending on the particular use case:
- Assuming the virtual function mechanism is implemented by virtual function tables (vtable), separate vtables are allocated in the memory for the class and for each class that inherits it. More importantly, the size of each instance of the class are increased by the size of a hidden pointer to a vtable. This can cause significant memory overhead for small sized classes where the size of the pointer is comparable to the total size of an object, especially if many instances exist at a given moment. It would be quite reasonable to assume that other implementations for virtual function tables would also add hidden members to class objects.
- A class that has at least one virtual function is prevented from many low-level operations on a class object. (One can assume that this is due to the hidden pointer in the vtable implementation.) If the class is otherwise a plain old data (POD) structure, having a virtual function renders the class a non-POD type, violating one of the conditions of POD types. For example, this prevents the use of bytewise operating functions such as
std::memcpy
andstd::memcmp
with an object of this class type. Additionally, because memory layout of such objects is no longer simple, extra measures are needed when passing the class type to another language for developing cross-language software - During program execution, virtual function calls are slower than non-virtual function calls, because of additional instructions for accessing the target functions that must be called (typically through function pointers in vtables). The compiler/linker pairs can optimize virtual destructor calls that are invoked on the true class type and replace them with ordinary calls, only if the optimization level is high enough, they themselves are “smart” enough and aware of the true class type in the call. However, if optimization is not in place for any reason, virtual destructors that do not have to be virtual suffer from the overhead. Furthermore, destructors that would otherwise be chosen by the compiler as suitable to be inlined, may not qualify for this benefit, because compilers are less likely to inline virtual functions.
- Examining the code, a
virtual
keyword next to the name of a destructor function in a non-runtime polymorphic class declaration implies runtime polymorphism, as this is the sole purpose of virtual functions. (When encountering a virtual destructor, a reader may look for other virtual function declarations as the next thing to do, because the use of runtime polymorphism is a fundamental class trait.) This is always a valid concern, even if all other drawbacks are negligible for a given class.
Contrarily, the only advantage of declaring destructors of such classes as virtual is to prevent against possible client misuse by destructing derived class objects through pointers or references of base class types. As aforementioned, the toll can be too costly for this type of advantage, especially to make it a habit.
In fact, virtual destructors are still reasonable for non-runtime polymorphic base classes for which performance and size are not important concerns and all costs are acceptable. This is true particularly when the destructor must be public, keeping in mind that protected base class destructors prevent this misuse as well. It is also worth reminding that base classes that do not have any virtual functions are not very common in object-oriented design.
However, virtual destructors do not make much sense for classes that are not designed to be base classes (a trait that can be enforced with the final identifier since C++11). In general, blindly declaring every destructor as virtual is a bad practice and can potentially lead to a significant waste of resources.
Bad practices such as this, are typically a result of misguidance for novice C++ programmers. However, in the case of experienced programmers, this can better be explained with a tendency of overlooking the rationale and costs of using specific language features, which leads to an incorrect level of abstraction in the coding process, resulting in bad code. On the other hand, doing this deliberately may well point to a zealous belief in defensive programming to the point of significant performance loss, as well as making the code less coherent.
Upon examining the advantages and disadvantages of using virtual destructors, the best practice can be summarized by Scott Meyers, from his book Effective C++:
“Polymorphic base classes should declare virtual destructors. If a class has any virtual functions, it should have a virtual destructor. Classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors.” (p. 58)
References:
- ISO/IEC. (2017). ISO International Standard ISO/IEC 14882:2017 – Working Draft, Programming Language C++. [N4659]. Geneva, Switzerland: International Organization for Standardization (ISO). Source
- “Effective C++, Third Edition: 55 Specific Ways to Improve Your Programs and Designs” Scott Meyers