Have a look here,
struct Component {
int* data;
Component() { data = new int[100]; std::cout << "data allocated\n"; }
~Component() { delete[] data; std::cout << "data deleted\n"; }
};
struct Base {
virtual void f() {}
};
struct Derived : Base {
Component c;
void f() override {}
};
int main()
{
Base* b = new Derived;
delete b;
}
The output:
data allocated
but not deleted.
Conclusion
Whenever a class hierarchy has state, on the purely technical level, you want a virtual destructor all the way from the top.
It is possible that once you added that virtual destructor to your class, you triggered untested destruction logic. The sane choice here is to keep the virtual destructor you've added, and fix the logic. Otherwise, you're going to have resource and/or memory leaks in your process.
Technical Details
What's happening in the example is that, while Base
has a vtable, its destructor itself isn't virtual, and that means that whenever Base::~Base()
is called, it doesn't go through a vptr. In other words, it simply calls Base::Base()
, and that's it.
In the main()
function, a new Derived
object is allocated and assigned to a variable of type Base*
. When the next delete
statement runs, it actually first attempts to call the destructor of the directly passed type, which is simply Base*
, and then it frees the memory occupied by that object. Now, since the compiler sees that Base::~Base()
isn't virtual, it doesn't attempt to go through the vptr of the object d
. This means that Derived::~Derived()
is never called by anyone. But since Derived::~Derived()
is where the compiler generated the destruction of the Component
Derived::c
, that component is never destroyed either. Therefore, we never see data deleted printed.
If Base::~Base()
were virtual, what would happen is that the delete d
statement would go through the vptr of the object d
, calling the destructor, Derived::~Derived()
. That destructor, by definition, would first call Base::~Base()
(this is auto-generated by the compiler), and then destroy its internal state, that is, Component c
. Thus, the entire destruction process would have completed as expected.