Virtual destructors in C++: why this is still relevant (and when you can forget about it)
\n\nIf you've ever written in C++ and encountered inheritance, you've probably seen this question on Stack Overflow with tens of thousands of views: «When to use virtual destructors?». In 2025, the topic hasn't lost its urgency — memory leaks due to a missing virtual continue to take their toll in production. But there is also a flip side: mindlessly adding virtual everywhere possible is also not a good practice.
Let's figure it out once and for all: when a virtual destructor is mandatory, when it's just good practice, and when its presence is a sign of poor architecture. Let's go.
\n\nShort answer (for those in a hurry)
\n\nA virtual destructor is needed if and only if you are going to delete an object of a derived class through a pointer to the base class. If there is no such scenario in the code, virtual is not required. But there are nuances that we will discuss below.
Why is this even a problem? Anatomy of a leak
\n\nLet's look at a classic antipattern that breaks the code of beginners (and not only).
\n\n#include <iostream>\n\nclass Base {\npublic:\n ~Base() { \n std::cout << "Base destructor\\n"; \n }\n};\n\nclass Derived : public Base {\nprivate:\n int* big_data = new int[1000]; // resources that need to be freed\npublic:\n Derived() { std::cout << "Derived created\\n"; }\n ~Derived() {\n delete[] big_data;\n std::cout << "Derived destructor\\n";\n }\n};\n\nint main() {\n Base* ptr = new Derived();\n delete ptr; // Oops! Only ~Base() will be called\n return 0;\n}\n\nOutput:
\nDerived created\nBase destructor\n\nThe big_data array leaked. The Derived destructor was not called because the compiler, seeing the type Base*, called the destructor of that particular class. And it is not virtual.
The solution is to add virtual before the Base destructor:
class Base {\npublic:\n virtual ~Base() = default; // or empty body\n};\n\nNow, with delete ptr, ~Derived() will be executed first, then ~Base(). Resources are freed, memory is not leaking, the static analyzer is happy.
When a virtual destructor is mandatory (ironclad rule)
\n\nThere is only one unconditional case: if the class is intended for polymorphic use through a pointer or reference to the base class, and objects of derived classes will be deleted through this pointer.
\n\nIn practice, this means:
\n- \n
- The class has at least one virtual function (pure or not) \n
- You plan to use
Base*orBase&to work withDerived\n - Objects are created on the heap (
new) and deleted viadelete\n
Example — a classic shape hierarchy:
\n\nclass Shape {\npublic:\n virtual double area() const = 0;\n virtual ~Shape() = default; // mandatory for an abstract class\n};\n\nclass Circle : public Shape {\n double radius;\npublic:\n explicit Circle(double r) : radius(r) {}\n double area() const override { return 3.14159 * radius * radius; }\n};\n\nint main() {\n std::vector<std::unique_ptr<Shape>> shapes;\n shapes.push_back(std::make_unique<Circle>(5.0));\n // unique_ptr will take care of delete itself, but the Shape destructor must be virtual\n return 0;\n}\n\nNote: even if you use std::unique_ptr (which is the recommended approach in 2025), the virtual destructor is still needed. unique_ptr calls delete through a pointer to the base class —