2012-01-08

When to make a Destructor Virtual

When to make a Destructor Virtual

When do I make my destructors virtual?

I've interviewed several people for C++ positions and one question we always ask is what a virtual destructor is and when to use one. For some reason, a suprising number of people say that you should make your destructor virtual whenever you have anything else in the class that is virtual. That is not the correct answer. (Although, it is suggested by Bjarne Stroustrup in the ARM (page 278), and I suppose that is a pretty good basis for the claim...) There may be a correlation between the two, but there is absolutely no causative relationship.

There are three reasons that must all be fulfilled(*) before making a destructor virtual.

  1. The class might be derived from.
  2. A pointer to this type might be used to destroy instances of derived class.
  3. Either of (*):
    • Someone might define custom delete functions (that might be different for the base and derived classes, or might be sensitive to the object's real size).
    • Some derived classes might need their destructors called.


(*) Unless you want to follow The Standard... (which, you know, maybe you should...)

Technically, you should ignore that last one. The standard says:

5.3.5 Delete
[...]
3 In the first alternative (delete object), if the static type of the operand is different from its dynamic type, the static type shall be a base class of the operand's dynamic type and the static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.

(Emphasis mine.)

Which means: if you're going to delete an object of class B through a pointer to class A (where B is derived from A), then A had better have a destructor that is virtual. That doesn't mean that it needs to declare it virtual itself but if one of A's superclasses doesn't declare the destructor virtual then A's destructor does need to be declared as virtual. And if you intend to rely on a higher base class declaring it virtual, you should probably just declare it so as well... in case someone does something utterly foolish like making it no longer virtual.

If that doesn't convince, see what some learned elders have to say on it:

  • Scott Meyers, Effective C++ Second Edition (Addison Wesley, 1997) 60-62. Item 14 discusses the problem of virtual destructors in detail.
  • Bjarne Stroustrup, The C++ Programming Language Third Edition (Addison Wesley, 1997) 319. Section 12.4.2 discusses abstract classes and mentions rather offhandedly: "Many classes require some form of cleanup for an object before it goes away. Since the abstract class [the base class] cannot know if a derived class requires such cleanup, it must assume that it does require some. We ensure proper cleanup by defining a virtual destructor [base class]::~[base class]() in the base and overriding it suitably in the derived classes."
  • Margaret A. Ellis and Bjarne Stroustrup, The Annotated C++ Reference Manual, (Addison Wesley, 1990) 277-278. (Also known as "The ARM".) The commentary at the end of the page rather tepidly states "It is often a good idea to declare a destructor virtual".
  • Marshall Cline, C++ FAQ (http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.7), A good description of the problem and the rule.

However, if you feel like living dangerously, or even if you don't but you really need that extra performance, then you might be able to get away with it.

Now, normally I'm really a fairly honest person. Really! And I'm a big advocate of not lying to the compiler. (Generally, it takes its revenge.) But sometimes lying is just communicating something in a different way. In this case, I'm saying to the compiler: "I know you really want to dot your I's and cross your T's, but believe me, in this case you really don't need to. Just call the destructor of the type of the pointer."

That said, you do still have to be really careful. There are a few reason why you really would need to have the derived class's destructor called:

  • The derived class has data members (such as raw pointers), or other logical ownerships such as open log files, that must be cleaned up.
  • You have data member items in the derived class and they need their destructors to be called. (Such as an auto_ptr.)
  • You have explicitly defined the derived class's destructor code and need it to be called.
  • A derived class, or one of its derived classes, might provide a custom delete operator. If so, the size passed to the delete function is determined by the dynamic type of the class if the destructor is virtual, but by the static type of the pointer if the destructor is not virtual.
  • A derived class might be a different size, and you may be deleting an array of them.

If all of those reasons are absent, then you might be able to get away with not having your class use virtual destructors.

When can I not delete through a pointer to the base class?

Related to the discussion about when a function needs to be virtual, is when can an array of a derived class be deleted through a pointer to a base class? If the sizes are the same (and assuming you don't have to worry about the destructor not being virtual), then the answer is whenever you like. If they aren't the same, then never.

To see why, imagine that you have class A and B, where B derives from A, and both hold an integer. A always sets it to 1, and B sets it to 2. Something like (source here):


 class A {
  int nDataA;
  A() : nDataA(1) {}
 };

 class B : public A {
  int nDataB;
  B() : nDataB(2) {}
 };
If I have it create an array of three Bs and delete them as Bs, then (after instrumenting the classes) I see:

 Creating B[3] dynamically, deleting through B*
 Constructing at 0xd915c
 Constructing at 0xd9168
 Constructing at 0xd9174
 Dest of 0xd9174. I am now B :(size is 12 bytes)  B83F0400 01000000 02000000
 Dest of 0xd9174. I am now A :(size is 8 bytes)  08400400 01000000
 Dest of 0xd9168. I am now B :(size is 12 bytes)  B83F0400 01000000 02000000
 Dest of 0xd9168. I am now A :(size is 8 bytes)  08400400 01000000
 Dest of 0xd915c. I am now B :(size is 12 bytes)  B83F0400 01000000 02000000
 Dest of 0xd915c. I am now A :(size is 8 bytes)  08400400 01000000
If I do the same, but delete it through an A pointer, then I see:

 Creating B[3] dynamically, deleting through A*
 Constructing at 0xd915c
 Constructing at 0xd9168
 Constructing at 0xd9174
 Dest of 0xd916c. I am now A :(size is 8 bytes)  08400400 02000000
 Dest of 0xd9164. I am now A :(size is 8 bytes)  08400400 B83F0400
 Dest of 0xd915c. I am now A :(size is 8 bytes)  08400400 01000000

(These results were obtained with the DJGPP compiler, which uses gcc.)

Notice how it deletes backwards; it starts at 0xd916c and goes down to 0xd915c. Also notice that those aren't the addresses that were constructed, unlike the first case where we destroyed through a B pointer. Notice what happens when you call it through Bs destructor: it starts with the vptr at 00043FB8 and calls ~B, then sets it to 00044008 for when it calls ~A. This is as expected, of course, since when you call through the constructors and destructors, virtual functions resolve to the current class being constructed/destructed, not the class that the object will eventually be.

Now look at the sequence of destructor when it is done through the A* pointer. The first one that is destroyed is destroyed through a pointer that is actually pointing to the "1" of the second item. Before the ~B code is called the vptr for A's vtable is written on top of the "1". The same goes for the second item, which starts out pointing to the "2" of the first item.

Clearly this is bad. Unfortunately, even making the destructor of A virtual will not solve this problem. Theoretically, if the class being pointed to has a vtable it would be possible to encode the sizeof in the table and use that when iterating through the array. Or, the compiler could store the sizeof in the table, as it stores the number of entries in the array. Gcc doesn't do this, for better or worse. The bottom line is that if you want to destroy an array of Bs with a pointer to As, then sizeof(B) must equal sizeof(A), in addition to the other rules stated above, regardless of whether or not the destructor is virtual.

Hacky "Fixes"

You could ensure that class A is not ever deleted in array form by overloading that operator and throwing an exception, but then how do you know that you aren't just deleting a bunch of "A"s, which is fine? (If you overload the delete array operator for "B" (in order to throw), then it will only get called in the instance where it is being deleted properly, which is actually worse than doing nothing.)

You could implement a custom destructor for arrays of A, and test in the destructor if it is seeing an object of class B in order to handle it specially. But you would still have the wrong pointer to the class... you'd have to fix that up with pointer math after you figure out what your base is. This is so terrible that I refuse to think about it anymore.

Why not always make it virtual

There is a performance penalty to making and calling a virtual destructor. Making a destructor virtual will force the class to store a pointer (called a vptr) to the class's virtual table. If there are no other virtual functions in the class, then this is a little bit of extra overhead (whatever the pointer size is: usually 32 bits, but sometimes 64 bits or even something else). This is probably where the typical erroneous answer comes from. Presumably people think that if you are already paying the space cost, then you might as well make the destructor virtual.

There is also a performance penalty to calling a virtual function, including virtual destructors. The vptr must be dereferenced and the resulting function called. This can cause memory cache misses, et c.

Use at Your Own Risk

There you go. Be careful. Or just play it safe and follow the standard. Or did I miss something?


Creative Commons License
When to make a Destructor Virtual by Mark Santesson is licensed under a Creative Commons Attribution 3.0 Unported License.

No comments:

Post a Comment