(C++, and most likely only C++:)
In C++, a virtual destructor is a destructor that
happens to be a virtual function (which see, if you'd
like a recap of why member functions sometimes like to be
virtual) . So, it gets called with (run time)
polymorphism.
Why bother?
To apply run-time polymorphism in C++, you need to hold the object
itself, via pointer or reference. If ever you copy an object
to a variable of a base class, object slicing takes place and
your new copy has that class.
Now suppose you want to write code like this:
class X { /* ... */ };
class Y : public X { /* ... */ };
X* make_a_new_X(int);
void f(int t) {
X* px = make_a_new_x(t);
// do stuff with px
delete px;
}
This code works perfectly -- as long as you
know make_a_new_x always returns a pointer to an actual
X, not a Y. As soon as it can return a Y* under some circumstances,
you may have a problem: delete px calls px->~X(), so ~Y() is not
called -- even if you expected it to.
For automatically-allocated objects ("on the stack"),
the case never arises: you never explicitly delete them. So the
rule is clear, if not simple:
-
If you plan to manage dynamically allocated objects via
polymorphic pointers, and if these require non-trivial
destructors, then the destructor of the base class needs to be
declared virtual.
As soon as we declare virtual ~X(); in the definition of
class X above, the code will work in all circumstances.
Since code correctness is pretty important, the rule above has
been relaxed to something easier to understand and apply:
-
If you plan to inherit from a class, declare its destructor
virtual.
This rule is certainly easy to understand and apply. And which accounts for its popularity: "I declare my destructors virtual, so I
are an Object Oriented C/C++ Programmer!" (see the
ten commandments for C++ programmers for yet another example). Unfortunately
it is misguided and dangerous. Blindly declaring methods "virtual" is never a good idea. Blindly declaring your destructors "virtual" may prevent some bugs -- but if it does, you must have some other methods that
also must be declared "virtual" to prevent bugs.
Why not always?
The second version of the rule is curious: if it's so important, why
does the ISO C++ standard even give the option not to
declare destructors virtual?
A destructor is just another function (even if one that gets called
automatically at various points). The above arguments would apply
to any member function of the class:
- If you intend to call the member function via a polymorphic
pointer or a polymorphic reference, you should declare it
virtual.
-
If you plan to inherit from a class, declare all its methods
virtual.
And C++ does not make methods virtual by default. Doing
so would break some promises of C++:
-
C++ allows you to program is a certain paradigm, but
never forces you to do so.
-
C++ tries to give a consistent programming
environment (more
details of this).
- You only pay for C++ feature that you actually use.
For (I), the case is clear: making all methods virtual would force
you to program in an
OO manner.
C++ is not an object-oriented
language: it just acts like one, and lets you write OO code
if
you so desire. Obviously, making
all methods virtual
would hurt (performance-wise, at least), but why not just
destructors? (II) takes care of that point: the inconsistency would
be jarring. And (III) is the finishing touch: virtual method calls,
which often have to be (in the C++ and in the general modern
linkage model) implemented as some wrappers for
function
pointers, must be less efficient than direct calls.
Making all method calls virtual would force C++ into the (pure)
object-oriented mold of programming language, at some execution
cost. Sure, some applications can take the hit, but not
all. Think, e.g., of the cost of 108 virtual
method calls in a loop.
Making just destructors virtual would be a curious mixup. Exactly
the same programming errors as occur with destructors
would still occur. The only difference would be that
programs might give incorrect results, rather than crash. And the
inconsistency would break point (II) above, when there is really
nothing special about dispatching destructor calls.
The STL in the standard C++ library gives examples of all
these points. It is not object oriented. Accordingly, all
STL destructors and almost all standard library methods
are not virtual. This supports (I): you can program in C++
as "a better C", just using e.g. STL data structures
as highly optimized data structures with precisely guaranteed
semantics. It also supports (III): it is perfectly safe to use
e.g. std::pair in performance critical code, knowing that no
virtual method calls are introduced.
It is not even safe to inherit from an STL container. Containers
have a huge interface. An inheriting class would need to
implement them all. For instance, consider methods at() and
operator of std::vector. While the first
might be implemented in terms of the second, it need not be --
so even if they were both virtual, you'd need to reimplement each
one when inheriting.
Templates
ISO C++ does not allow virtual function templates. It is simply too
far outside the current (probably broken) linkage model employed on
modern platforms. If we allowed
class X {
public:
template<typename T> virtual f(T t) { /* ... */ }
};
class Y : public X { /* ... */ }
then
every instantiation of X::f<T>
for
any T would immediately require instantiation
of Y::f<T>. Since objects in different
translation units may inherit from one another, the complexity
would be unmanageable.
The mind boggles at the thought of
dynamically loaded objects; while not part of any C++ standard, all
modern platforms support them -- and allowing template virtual
functions would require compilation as part of dynamic
loading!
In the current state of affairs, making all methods virtual would
make templated methods impossible -- again going against the status
of C++ as a multi-paradigm language.
Obviously, using a class in a generally object-oriented manner
requires making its destructor virtual. Accordingly, the
traditional way ("the C++ way") of marking a
class "inheritable" is making its destructor
virtual.
Some people (myself included) like to mark this explicitly:
class X /* ... */ {
public:
// ...
virtual ~X(); // Inherit from this class!
};
Another example: As mentioned above, it is not safe to
inherit from STL containers, and this is well known. Since STL
destructors are not virtual, even this upholds the
tradition.
What should I do?
As always: Think.
If you have inheritance, think whether or not you need virtual
functions. This certainly includes the destructor -- but
if any function needs to be declared virtual
then most likely all functions that can be overridden will
also need to be declared virtual.
If inheritance is supported by your class, good for you. If dynamic
typing is required (i.e. you're using a particular form of
inheritance), you'll probably want to have some virtual functions in
it.
And if the lifetimes of instances of your class can be managed
through dynamic typing, you'll probably want a virtual destructor.
A note on PsyMar's writeup above. While it is probably true that base::~base should be virtual, there is no memory leak in the code he gives. This is important enough to repeat:
There is no memory leak in the example code!
True, ~base has no code freeing those extra 1024 bytes of derived::aLongCstring. BUT -- neither does ~derived! How on earth could it? The memory does not come from new[], so it cannot be delete[]d. If derived is on automatic storage ("the stack"), it still needs to be reclaimed.
The compiler has an excellent idea of how to free a derived struct. As part of its excellent idea, it takes care to know how much memory it takes. derived::~derived needs to do nothing about it, and neither does base::~base. Claiming that this is a memory leak is plain wrong.
Entirely equivalently, a class
class derived2 : public derived {
X x;
};
will correctly destroy x, calling X::~X, as part of its destruction.
No code needs to be written to make this happen. No code CAN be written in C++ that will make this happen. The compiler has to do it. The compiler does it.