Sunday, February 24, 2008

Derivation without a virtual destructor

There is a piece of advice, commonly found in C++ newsgroups and tutorials, that classes without a virtual destructor should not be derived from, on grounds that deleting an object of the derived class through a pointer to the base class is undefined behavior:

struct A
{
~A(){}
};

struct B:A{};

void f(A* a)
{
...
delete a;
}

int main()
{
B* pb=new B();
f(pb); // undefined behavior
}

This is often invoked as an argument against the suitability of deriving from fat classes (like std::string or the STL containers) to provide some extra functionality without adding state.

I do not agree with this rule forbidding derivation without a virtual destructor, and in fact I think that the advice is wrong in most situations. For one, passing an object of a derived class is not the only way of producing undefined behavior in the context described above:

int main()
{
A a;
f(&a); // undefined behavior
}

What does this teach us? Well, passing an object through a pointer does not mean that the object is deletable, and conversely it does not mean either that the function accepting the object will always try to delete it. In most (all?) situations the caller and the callee operate under a contract establishing whether a given argument passed by pointer can be deleted by the callee or not. If it is not, passing a pointer to a derived class is perfectly legal.

And adding to that, classes without virtual destructors are rarely passed by pointer, and even more rarely allocated in the heap; assuming that a class without a virtual destructor almost surely is not virtual at all, handling it polymorphically makes little sense. Consider how many times you've used std::string in your code, how frequently those std::strings were passed by pointer and, finally, in how many situations (if any) some piece of your code deleted a pointer to a std::string.

So, my opinion is that deriving from a non-virtual (and hence, without virtual destructor) class, such as std::string or STL containers, is valid in most cases, and indeed is an expedient method to add functionality to a class without resorting to tedious techniques like manual forwarding. One only has to be careful that objects of the derived class are not passed via a pointer to the base class in contexts where the pointer will be deleted; this is about as evident as avoiding passing addresses of stack objects under the same scenario.

No comments:

Post a Comment