When to provide an empty destructor
Wednesday, April 4th, 2012If you search around on the Internet, you will find various opinions about whether it is a good idea to provide an explicit empty definition of a destructor or if it is best to let the compiler synthesize an implementation for you. The other day I also caught myself thinking about this choice for a class I’ve been working on. This made me realize that I don’t have a complete and clear picture of the tradeoffs involved. Ideally, I would like a hard and fast rule so that I don’t have to waste a few minutes thinking about this every time I create a new class. So today I decided to lay this matter to rest by analyzing all the common and special cases that I am aware of while taking into account not only performance, but also footprint and even the compilation time.
There are three distinct use-cases that I would like to analyze: a class or a class template with a non-virtual destructor, a class with a virtual destructor, and a class template with a virtual destructor. But before we jump to the analysis, let’s first review some terms used by the standard when talking about synthesized destructors. At the end of the analysis I would also like to mention some special but fairly common cases as well as how C++11 helps with the situation.
If we declare our own destructor, the standard calls it a user-declared destructor. If we declared a destructor, we also have to define it at some point. If a class has no user-declared destructor, one is declared implicitly by the compiler and is called an implicitly-declared destructor. An implicitly-declared destructor is inline. An implicitly-declared destructor is called trivial, if (a) it is not virtual, (b) all its base classes have trivial destructors, and (c) all its non-static data members have trivial destructors. In other words, a trivial destructor doesn’t need to execute any instructions and, as a result, doesn’t need to be called, or even exist in the program text. Note that the first condition (that a destructor shall not be virtual) was only added in C++11, but, practically, I believe all the implementations assumed this even for C++98 (virtual function table contains a pointer to the virtual destructor and one can’t point to something that doesn’t exist).
Another aspect about destructors that is important to understand is that even if the body of a destructor is empty, it doesn’t mean that this destructor won’t execute any code. The C++ compiler augments the destructor with calls to destructors for bases and non-static data members. For more information on destructor augmentation and other low-level C++ details I recommend the “Inside the C++ Object Model” book by Stanley L. Lippman.
Note also that an explicit empty inline definition of a destructor should be essentially equivalent to an implicitly-defined one. This is true from the language point of view with a few reservations (e.g., such a class can no longer be a POD type). In practice, however, some implementations in some circumstances may choose not to inline an explicitly-defined destructor or expression involving such a destructor because an empty inline destructor is still “more” than the trivial destructor. And this makes an implicitly-declared trivial destructor a much better option from the performance and footprint point of view. As a result, if we are providing an empty destructor, it only makes sense to define it as non-inline. And the only reason for doing this is to make the destructor non-inline. Now, the question is, are there any good reasons for making an empty destructor non-inline?
Class with non-virtual destructor
Let’s start by considering a class with a non-virtual destructor. While there are a few special cases which are discussed below, generally, there are no good reasons to prefer a non-inline empty destructor to the synthesized one. If a class has a large number of data members (or bases) that all have non-trivial destructors, then, as mentioned above, the augmented destructor may contain quite a few calls. However, chances are good a C++ compiler will not actually inline calls to such a destructor due to its complexity. In this case, object files corresponding to translation units that call such a destructor may end up containing multiple instances of the destructor. While they will be weeded out at the link stage, the need to instantiate the same destructor multiple times adds to the compilation time. However, in most cases, I believe this will be negligible.
The same reasoning applies to class templates with non-virtual destructors.
Class with virtual destructor
If a destructor is made virtual, then we also get an entry for it in the virtual function table (vtbl from now on for short). And this entry needs to be populated with a pointer to the destructor. As a result, even if the destructor is inline, there will be a non-inline instantiation of this destructor.
At first this may sound like a good reason to provide our own non-inline empty implementation. But, on closer inspection, there doesn’t seem to be any benefit in doing this. In either case there will be a non-inline version of the destructor for the vtbl. And when the compiler is able to call the destructor without involving the vtbl (i.e., when it knows that the object’s static and dynamic types are the same), then we can apply exactly the same reasoning as above.
Another thing that we may want to consider here is the instantiation of the vtbl itself. Normally, the vtbl for a class is generated when compiling a translation unit containing the first non-inline member function definition of this class. In this case we end up with a single vtbl instantiation and no resources are wasted. However, if a class only has inline functions (including our compiler-synthesized destructor), then the compiler has to fall to a less optimal method by instantiating the vtbl in every translation unit that creates an instance of an object and then weeding our duplicates at the link stage. If this proves to be expensive (e.g., you have hundreds of translation units using this class), then you may want to define an empty non-inline destructor just to anchor the vtbl.
Note also that in C++98 it is not possible to declare a destructor virtual but let the compiler synthesize the implementation (this is possible in C++11 as we will see shortly). So here we have to define an empty destructor and the question is whether to make it inline or not. Based on the above analysis I would say make it inline for consistency with the derived classes which will have inline, compiler-synthesized destructors. That is:
class base { public: virtual ~base () {} ... };
Class template with virtual destructor
The same analysis applies here except now we always have potentially multiple vtbl instantiations, regardless of whether our destructor is inline or not. And this gives us one less reason to provide one ourselves.
To summarize, in all three cases my recommendation is to let the compiler define an inline destructor for you. Let’s now consider a few special cases where we have to make the destructor non-inline.
Special cases
There are two such special but fairly common cases that I am aware of. If you know of others, I would appreciate it if you mentioned them in the comments.
The first case can be generally described as needing extra information to be able to correctly destroy data members of a class. The most prominent example of this case is the pimpl idiom. When implemented using a smart pointer and a hidden “impl” class, the inline destructor won’t work because it needs to “see” the “impl” class declaration. Here is an example:
// object.hxx // class object { public: object (); // ~object () {} // error: impl is incomplete ~object (); ... private: class impl; std::unique_ptr<impl> impl_; }; // object.cxx // class object::impl { ... }; object:: object () : impl_ (new impl) { } object:: ~object () { // ok: impl is complete }
Another example of this case is Windows-specific. Here, if your object is part of a DLL interface and the DLL and executable use different runtime libraries, then you will run into trouble if your object allocates dynamic memory using the DLL runtime (e.g., in a non-inline constructor) but frees it using the executable runtime (e.g., in an inline destructor). By defining the destructor non-inline, we can make sure that the memory is allocated and freed using the same runtime.
The second case has to do with interface stability. Switching from a compiler-provided inline definition to a user-provided non-inline one changes the binary interface of a class. So if you need a binary-compatible interface, then it may make sense to define a non-inline empty destructor if there is a possibility that some functionality may have to be added to it later.
C++11 improvements
C++11 provides us with the ability to control inline-ness and virtual-ness of the compiler-defined destructor using the defaulted functions mechanism. Here is how we can declare a virtual destructor with the default implementation:
class base { public: virtual ~base () = default; // inline ... };
To make the default implementation non-inline we have to move the definition of the destructor out of the class, for example:
// derived.hxx // class derived: public base { public: virtual ~derived (); ... }; // derived.cxx // derived::~derived () = default;
Note that making a default implementation virtual or non-inline also makes it non-trivial.
Checklist
To be able to quickly decide whether a class needs an empty non-inline destructor definition I condensed the above analysis into a short checklist. When designing a class interface, ask yourself the following three questions:
- Do you need to anchor the vtbl (doesn’t apply to class templates)?
- Does proper destruction of data members require additional declarations or functionality that is not available in the class interface? Does the destruction need to be done consistently with construction (e.g., using the same runtime)?
- Do you need to define a stable interface and chances are that later you may have to add some functionality to the destructor?
If the answers to all these questions are “No”, then let the compiler provide the default implementation of the destructor.