Should I default virtual destructors?

I have an abstract class that is declared as follow:

class my_type {
public:
    virtual ~my_type() = default;
    virtual void do_something() = 0;
};

Is it considered good practice to declare the destructor like this, with the default keyword? Is there a better way?

Also, is = 0 a modern (C++11) way of specifying no default implementation, or there is a better way?


Solution 1:

Yes, you can definitely use = default for such destructors. Especially if you were just going to replace it with {}. I think the = default one is nicer, because it's more explicit, so it immediately catches the eye and leaves no room for doubt.

However, here are a couple of notes to take into consideration when doing so.

When you = default a destructor in the header file (see edit) (or any other special function for that matter), it's basically defining it in the header. When designing a shared library, you might want to explicitly have the destructor provided only by the library rather than in the header, so that you could change it more easily in the future without requiring a rebuild of the dependent binary. But again, that's for when the question isn't simply whether to = default or to {}.


EDIT: As Sean keenly noted in the comments, you can also use = default outside of the class declaration, which gets the best of both worlds here.


The other crucial technical difference, is that the standard says that an explicitly defaulted function that can't be generated, will simply not be generated. Consider the following example:

struct A { ~A() = delete; };
struct B : A { ~B() {}; }

This would not compile, since you're forcing the compiler to generate the specified code (and its implicit requisites, such as calling A's destructor) for B's destructor--and it can't, because A's destructor is deleted. Consider this, however:

struct A { ~A() = delete; };
struct B : A { ~B() = default; }

This, in fact, would compile, because the compiler sees that ~B() cannot be generated, so it simply doesn't generate it--and declares it as deleted. This means that you'll only get the error when you're trying to actually use/call B::~B().

This has at least two implications which you should be aware of:

  1. If you're looking to get the error simply when compiling anything that includes the class declaration, you won't get it, since it's technically valid.
  2. If you initially always use = default for such destructors, then you won't have to worry about your super class's destructor being deleted, if it turns out it's ok and you never really use it. It's kind of an exotic use, but to that extent it's more correct and future-proof. You'll only get the error if you really use the destructor--otherwise, you'll be left alone.

So if you're going for a defensive programming approach, you might want to consider simply using {}. Otherwise, you're probably better off = defaulting, since that better adheres to taking the minimum programmatic instructions necessary to get a correct, working codebase, and avoiding unintended consequences1.


As for = 0: Yes, that's still the right way to do it. But please note that it in fact does not specify that there's "no default implementation," but rather, that (1) The class is not instantiatable; and (2) Any derived classes must override that function (though they can use an optional implementation provided by the super class). In other words, you can both define a function, and declare it as pure virtual. Here's an example:

struct A { virtual ~A() = 0; }
A::~A() = default;

This will ensure these constraints on A (and its destructor).


1) A good example of why this can be useful in unexpected ways, is how some people always used return with parentheses, and then C++14 added decltype(auto) which essentially created a technical difference between using it with and without parentheses.