Virtual destructors are like a virus

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 A

B* = 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:

  1. We tell the machine to delete the b0
  2. We look at b0 and sees that we are dealing with a class
  3. As this is a class which we call through pointer/reference maybe we should use a virtual table (VT)?
  4. There is no virtual members in A thus no VT is used
  5. We call the standard destructor of A
  6. Bang! We are trying to delete class A but it happens that the pointer has lead us to object of B which contains VT which A didn’t know of. sizeof(A) is 1 (as AFAIK it’s not legal to have size equal 0) and sizeof(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: , ,

Leave a comment