The good
During your life as a C++ programmer you will hear many guidelines like: use smart pointers, RAII, don’t play with pointers, measure what you optimize, etc. One of these rules goes as follows:
Every class which will be a base for other classes, has to have its destructor defined as
virtual
.
As we all do know, the virtual destructors are inherited. In other words, they behave like regular methods. If you have a base class A
through which pointer/reference you will call a destructor defined in B
(which inherits A
), the latter will be used.
class A { public: A() { print("Constructor of A"); } virtual ~A() { print("Destructor of A"); } }; class B : public A { public: B() { print("Constructor of B"); } virtual ~B() { print("Destructor of B"); } }; int main() { print("A* = B"); A* b0 = new B(); delete b0; print("B* = B"); B* b1 = new B(); delete b1; return 0; }
A* = B
Constructor of A
Constructor of B
Destructor of B
Destructor of AB* = B
Constructor of A
Constructor of B
Destructor of B
Destructor of A
The bad
Everything works, as it should. But what if we break the virtual destructor rule?
class A { public: A() { print("Constructor of A"); } ~A() { print("Destructor of A"); } // No virtual here }; class B : public A { public: B() { print("Constructor of B"); } virtual ~B() { print("Destructor of B"); } };
A* = B
Constructor of A
Constructor of B
Destructor of A
_BLOCK_TYPE_IS_VALID assert error!B* = B
Constructor of A
Constructor of B
Destructor of B
Destructor of A
B
pointer and destruction work correctly, but for A
pointer we have an assert! What does the _BLOCK_TYPE_IS_VALID
mean? Basically we are trying to delete memory which we shouldn’t. As far as I can tell, it happens because:
- We tell the machine to
delete
theb0
- We look at
b0
and sees that we are dealing with a class - As this is a class which we call through pointer/reference maybe we should use a virtual table (VT)?
- There is no
virtual
members inA
thus no VT is used - We call the standard destructor of
A
… - Bang! We are trying to delete class
A
but it happens that the pointer has lead us to object ofB
which contains VT whichA
didn’t know of.sizeof(A)
is 1 (as AFAIK it’s not legal to have size equal 0) andsizeof(B)
is 4 (due to presence of VT). We wish to delete 1 byte, but there is a block of 4 bytes. Due to DEBUG heap monitoring, the error was caught.
To prove it, take a look at the following code:
class A { public: A() { print("Constructor of A"); } ~A() { print("Destructor of A"); } }; class B : public A { public: B() { print("Constructor of B"); } ~B() { print("Destructor of B"); } // No virtual here };
A* = B
Constructor of A
Constructor of B
Destructor of A
B* = B
Constructor of A
Constructor of B
Destructor of B
Destructor of A
As you can see above, nothing exploded. As expected, B
dtor wasn’t called, but it didn’t crash as delete
had to clean the same size of block in case of A
and B
(which is 1B).
The ugly
What about such code?
class A { public: A() { print("Constructor of A"); } virtual ~A() { print("Destructor of A"); } }; class B : public A { public: B() { print("Constructor of B"); } ~B() { print("Destructor of B"); } }; class C : public B { public: C() { print("Constructor of C"); } ~C() { print("Destructor of C"); } }; int main() { print("A* = C"); A* c0 = new C(); delete c0; print("B* = C"); B* c1 = new C(); delete c1; print("C* = C"); C* c2 = new C(); delete c2; return 0; }
A* = C // ok
Constructor of A
Constructor of B
Constructor of C
Destructor of C
Destructor of B
Destructor of A
B* = C
Constructor of A
Constructor of B
Constructor of C
Destructor of C // WTF!?
Destructor of B
Destructor of A
C* = C // ok
Constructor of A
Constructor of B
Constructor of C
Destructor of C
Destructor of B
Destructor of A
How come c1
(which is object of C
through non-virtual pointer to B
) behaves like a standard, well-behaved class? Didn’t we hear that you shouldn’t delete a class through non-virtual-destructor of its base? The idea here is simple:
Virtual destructor defined in any base class above a class through which pointer you will delete your derived object will spread like a virus to all derived classes.
If you are infected with virtual dtor/method, the VT (which will be present in every class which derive from virtual class) will make sure that the bottom-most dtor/method is called. Hope this helps some of you as I felt very ashamed not knowing this fact :).
Tags: destructor, gotcha, inheritance
Leave a comment