When you call delete
on a pointer, the destructor of the most-derived object (type D
) need be invoked; however the compiler only knows how to invoke the destructor of the static type (B
) it sees at the moment, so if those types do not match (B != D), then B
need have a virtual destructor.
This has several implications:
void stack_allocated() {
MostDerived md; // no need for a virtual destructor,
... // the static type (MostDerived) is the actual type.
}
void dynamically_allocated() {
Derived* d = new MostDerived;
delete d; // Derived needs a virtual destructor,
// because Derived != MostDerived
// Bases of Derived need no virtual destructor
}
A simplification of the rule is to say:
- only inherit from class which have
virtual
methods
- as soon as a class has a
virtual
method, its destructor should be private
, protected
or virtual
which is generally what is taught (much easier to remember) even though it is sometimes too simplistic.
-Wnon-virtual-dtor
matches this simplification.
Note: there is usually no penalty to making the destructor virtual
if the class already has virtual
methods, because current implementations use a virtual-pointer to a single static table which regroups all the methods of the class. Furthermore, the compiler may be able to devirtualize the call if it is able to predict the dynamic type in which case it does not even have a runtime cost.
There is an alternative though: I developed -Wdelete-non-virtual-dtor
to warn not at the declaration site, but at the call site (delete
here). As a result, it has no false positives (providing you use final
to mark leaf classes), but warns later. YMMV.
Note: for example, std::shared_ptr<Derived> p = std::make_shared<MostDerived>();
is fine even without a virtual destructor because std::make_shared
creates a Deleter based on the actual type created; this is also why you can do std::shared_ptr<void> p = ...;
without issues.
Regarding the IBM link you provide:
- the discussion is focused on abstract classes thus they should the minimum amount of code that creates an abstract class. Note, for example, how the type names are meaningless, which is arguably very bad in production code!
- The class is perfectly usable as-is, only if someones calls
delete
on a A*
will you run into trouble.