When to provide an empty destructor

If 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:

  1. Do you need to anchor the vtbl (doesn’t apply to class templates)?
  2. 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)?
  3. 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.

9 Responses to “When to provide an empty destructor”

  1. Gregory Says:

    I faced situations where MSVC++ would refuse to force inline a function returning an object with an empty destructor by value because it considered the object to be unwindable hence EH generated code went in the way of the optimizer

    see http://stackoverflow.com/a/2322314/216063

  2. Boris Kolpackov Says:

    Gregory, yes, that makes sense if a compiler-provided destructor is trivial (which means there is nothing to call).

  3. Alex Turner Says:

    I your pimpl example is a bit short on explanation. Why cannot the destructor see the shared_ptr declaration as that declaration is in the class declaration and so the template should be instantiated there?

  4. Boris Kolpackov Says:

    Alex, it is not the shared_ptr declaration (actually, unique_ptr in my example) that the compiler cannot see. It is the object::impl declaration. C++ requires that the class should be complete when an instance of this class is freed using delete. In our case, if the object’s destructor is inline, then when an instance of object is destroyed somewhere in user code, the inline destructor calls impl_’s destructor which in turn calls delete on a pointer to call object::impl, which is incomplete.

  5. Andrea Says:

    Nice and interesting article. I got a bit confused on the “Class with non-virtual destructor case”.
    I’m perfectly on track when you say “…However, chances are good a C++ compiler will not actually inline calls to such a destructor due to its complexity.”, but then the following: “In this case, object files corresponding to translation units that call such a destructor may end up containing multiple instances of the destructor” got me lost.
    Isn’t the case that if the compiler didn’t inline calls to such destructors, the TU that call those destructors will only have a “call” (to be resolved by the linker) and not the whole copy of the destructor code in the generated object file?
    Am I mis-reading what you said?
    Thanks,
    Andrea.

  6. Boris Kolpackov Says:

    Andrea, when a compiler decides not to inline a function, then it must “instantiate” this function as if it was non-inline. Or, in other words, as you said, the compiler will generate a call instruction to the function but this function that is being called must be created somehow (i.e., it must end up in the program text).

    The way this is done depends on the implementation but generally there are two methods. If the compiler uses the first approach, then it instantiates the function in every translation unit that calls it and then weeds out duplicates at the link stage. With the second approach the compiler maintains some sort of a code database. When it compiles the first translation unit that calls our function, it instantiates this function and then adds it to this database. When another translation unit calls the same function, the compiler checks the database and sees that it has already been instantiated. Most popular C++ compilers (e.g., GCC, VC++) use the first method, which can lead to the situation I described.

  7. Andrea Says:

    Boris, thanks for your explanation, but I think I’m still missing the point. As you say, for the first approach (which I agree: is what most of the mainstream compiler follow) “… then it instantiates the function in every translation unit that calls it …”. So why is this worse than the case where destructor calls were actually inlined? If destructor calls are really inlined, their code is expanded at _every_ call site, even in the same TU. In the first approach that you describe, they’re expanded _once_ in every TU that calls it. Sorry to insist, but I think I’m missing what you’re trying to say and since I’m really interested in this subject I’d reallly like to fully understand…
    Thanks,
    Andrea.

  8. Boris Kolpackov Says:

    Andrea, I am not comparing this to the case where the constructor calls are actually inlined. Instead, I am comparing it to the case were we provide our own, non-inline empty definition. In this case we will only have one instance of the function in one object file.

  9. Andrea Says:

    Ok, that makes sense now. Thanks for your clarification