Deleting derived objects through base pointers turns the destructor into the function that decides whether the program leaks resources. C++ does not automatically run the right destructor when the static type of a pointer is different from the dynamic type of the object it points to. The fix is one keyword in the right place. This lesson covers what goes wrong without it, why the rule exists, and how to apply it correctly.
A polymorphic hierarchy almost always involves a base pointer or reference, because that's the entire point of polymorphism. Code that handles Product* works for DigitalProduct, PhysicalProduct, and anything else that inherits from Product. The Polymorphism chapters in this section use that pattern repeatedly.
The trouble starts when ownership also flows through the base pointer. Sooner or later, delete basePtr; appears. If the base class's destructor is not virtual, that single line misbehaves without warning.
Look at what's missing. DigitalProduct destroyed never prints. The derived destructor did not run. The downloadUrl_ string lives inside the derived part of the object, and that part of the object never got its cleanup. In this toy example, std::string is a member of a small object and its memory is reclaimed when the allocator releases the block, so nothing visible leaks. In real code the leak is real, and a few examples down shows one.
Two important details about what's happening here. First, the call to delete knows only what the pointer says it knows. The pointer type is Product*, so the compiler looks up the destructor on Product, finds a regular non-virtual function, and emits a direct call to ~Product. The actual object's type doesn't enter the picture. Second, the C++ standard does not call this "wrong output". It calls it undefined behavior: if the static type and the dynamic type differ and the destructor is not virtual, the result is whatever the implementation produces. Most compilers do exactly what was just shown: skip the derived destructor and free the base-sized block. Other compilers, other targets, other optimization settings can do other things.
"Undefined behavior" is not "behaves differently in some cases". It means the standard makes no promises at all. The program could leak, crash, or appear correct, and any of those can change after a recompile.
The diagram is the entire lesson in one picture. Everything else is the consequences of the right side of that diagram, and how to make sure your code ends up there.
The first example didn't actually leak anything visible because std::string cleans itself up only when its destructor runs, and that destructor lives inside ~DigitalProduct. With this version, the leak is impossible to ignore: the derived class owns a std::unique_ptr<DownloadHandle> to a heap-allocated handle, and skipping the derived destructor strands that allocation.
That's the leak. The handle opened. The handle was never closed. ~DigitalProduct did not run, so the std::unique_ptr did not run its destructor, so the DownloadHandle it owned was not released. Consider that this could be a network socket, an open file, a lock on a database row. Each delete leaks one. In an e-commerce system that ships thousands of digital downloads an hour, the leak is no longer theoretical.
Now change one line. Add virtual to ~Product. Same delete p;, same everything else.
Now the destruction sequence runs end to end. ~DigitalProduct fires first, which destroys the std::unique_ptr, which closes the DownloadHandle. Then ~Product runs and the base part is cleaned up. The order matches what you'd see for any normal destruction, because that's exactly what virtual dispatch gives you here.
The whole reason virtual works on the destructor is the same reason it works on any other member function. The vtable mechanism from the earlier chapter on vtables looks up the right function based on the actual object's type. The destructor is just another entry in that table.
The rule of thumb that catches every case worth catching is short.
If a class is intended to be used polymorphically through a base pointer or reference, its destructor must be virtual.
In practice that boils down to one even simpler heuristic, the one most teams put in their style guides:
If a class has any virtual function, give it a virtual destructor.
The first statement is the rigorous version, the second is the version applied when writing code. A class that has virtual functions is almost always one intended for polymorphic use (otherwise why bother with the virtual?), and it's almost always one that someone, somewhere, will eventually want to delete through a base pointer. Adding the virtual destructor up front costs nothing extra (the vtable already exists) and removes an entire category of future bugs.
The C++ Core Guidelines state this as rule C.35: "A base class destructor should be either public and virtual, or protected and non-virtual". The second half of that rule is for cases that forbid polymorphic deletion entirely, covered below.
virtual ~Product() = default;Most base class destructors do nothing custom. They exist purely to be virtual so the vtable picks up the entry. The modern idiom is to default the body:
= default tells the compiler to generate the body of the destructor automatically, which means "destroy each member in reverse declaration order and stop". That's all most base destructors need to do. The virtual keyword in front is what routes the function through the vtable, which is the part that matters.
Once ~Product is virtual, the derived destructor is implicitly virtual too. There's no need to write virtual on ~DigitalProduct. The C++11 override keyword is allowed on destructors and is a good habit; it doesn't change behavior, but it tells the compiler to verify that something with the same name in a base class is virtual. If the virtual is removed from the base by accident, the override on the derived destructor turns the mistake into a compile error.
A common shape for a base class meant for polymorphism: a virtual destructor defaulted, plus the virtual functions you want derived classes to override. A common shape for a derived class: data members that hold the actual resources, plus a defaulted destructor (the compiler-generated body is almost always right when members manage their own cleanup).
The destruction order with a virtual destructor is the same as the normal C++ destruction order, just kicked off correctly. The Polymorphism magic happens only at the entry point: virtual dispatch picks the most-derived destructor. After that, the chain runs the way the language always says it does.
For delete p; where p is a Product* pointing to a DigitalProduct:
~DigitalProduct body runs.DigitalProduct's data members are destroyed in reverse declaration order.~Product body runs.Product's data members are destroyed in reverse declaration order.Without virtual, step 1 is skipped and the destructor chain starts at step 3, which is the whole problem.
A small program that prints every step:
Read the bottom four lines top to bottom. ~DigitalProduct body runs. Then derivedPart_ (the only Logger declared inside DigitalProduct) gets destroyed. Then ~Product body runs. Then basePart_ (the Logger declared inside Product) gets destroyed. The name_ string in Product is destroyed after that, before the memory is released; it doesn't show up in the output because std::string doesn't log.
What not to do: try to delete derived members from the base destructor. The base destructor doesn't know about derived state, and by the time it runs in this sequence, the derived part is already gone. Keep ownership at the level where the member is declared, and let each class's destructor handle its own members.
Making a destructor virtual has the same cost as making any other member function virtual: the class needs a vtable, every instance needs a vptr, and every call to that function goes through one extra indirection. The earlier chapter on vtables covers the mechanics. The summary is short:
virtual to the destructor costs one extra slot in the vtable. That slot exists once per class, not once per object. The per-instance cost is zero extra bytes beyond what the class already pays.That second case is the only one worth thinking about. If the class isn't using polymorphism at all (no virtual functions, never used as a base for polymorphic deletion), adding a virtual destructor pays for something that won't be used.
Adding virtual to a destructor in a class with no other virtual functions adds 8 bytes per instance (the vptr) on most 64-bit platforms. Not free, but also not the first thing to optimize.
The right way to think about the trade-off: if the class is polymorphic, the vtable cost is already paid, and the virtual destructor is a marginal cost on top of an existing cost. The benefit is that delete basePtr; is safe instead of undefined behavior. That's a great trade. If the class is not polymorphic, a virtual destructor isn't needed at all, so the trade doesn't come up.
Switching from new/delete to smart pointers doesn't make the virtual destructor problem disappear, at least not for std::unique_ptr.
Same bug, same undefined behavior. The std::unique_ptr deletes the held pointer using the static type it was templated on, which is Product here. Internally, ~unique_ptr calls something equivalent to delete static_cast<Product*>(rawPtr), which is the line that's broken when ~Product isn't virtual. Smart pointers are a thin wrapper around the same delete, so the same rule applies: for a unique_ptr<Base> whose contained object might be a derived type, ~Base must be virtual.
std::shared_ptr is the one exception, and it isn't a safe pattern to rely on. A shared_ptr records its deleter in its internal control block at construction time, using the type the shared_ptr was constructed from, not the type it was stored as.
This works, and it works even when ~Product is not virtual, because the control block remembers it needs to call ~DigitalProduct. It's a real escape hatch, occasionally useful in templated code where the base class isn't local.
Don't rely on it as a substitute for a virtual destructor. Two reasons. First, the protection vanishes the moment the shared_ptr is constructed from a Product* instead of a DigitalProduct*, which is easy to do by accident:
Second, the rest of the code, every unique_ptr<Base>, every raw delete, every base pointer in a container, still has the original bug. Making the destructor virtual fixes all of them at once. Relying on shared_ptr's behavior fixes only the cases that happen to be make_shared'd.
The general guideline is the simple one: if a class is intended for polymorphic use, make its destructor virtual, and don't depend on the smart-pointer implementation to compensate.
Not every class wants to pay the vtable tax, and not every class is a polymorphic base. A Coupon value type that holds a code and a discount percent has no inheritance, no virtual methods, no need for runtime dispatch. Giving it a virtual destructor adds 8 bytes per object for no benefit.
The rule cuts both ways. The same logic that says "if it's polymorphic, make the destructor virtual" also says "if it's not polymorphic, don't". A value-type class that's never inherited from, never deleted through a base pointer, has no use for a virtual destructor.
A nuance: even a class that does have a base class doesn't necessarily need a virtual destructor, if the inheritance is being used for code reuse rather than runtime dispatch. The strict version of the rule is about polymorphic deletion specifically. The reason most style guides round it up to "any class with a virtual function" is that those classes are almost certainly used polymorphically in practice. A class with virtual functions but no polymorphic deletion is a theoretical case for skipping the virtual destructor, but the argument rarely survives contact with a code review.
The C++ Core Guideline C.35 gives the precise escape: to forbid polymorphic deletion, declare the destructor protected and non-virtual:
That makes delete basePtr a compile error when basePtr is a Mixin*. The class can still have virtual methods, can still be inherited from, just not deleted through the base. This is unusual and mostly appears in libraries that need to be very explicit about ownership. For everyday code, the rule remains the simple one.
One quick reminder. A destructor can be declared pure virtual, which makes the class abstract.
The pure virtual declaration prevents anyone from creating a Product directly, the same way any pure virtual function does. The body is still required because every derived destructor chains into the base destructor, so the base destructor's body has to exist somewhere. This pattern makes a class abstract when no other pure virtual method fits naturally. Pure virtual destructors exist and are useful in exactly this niche.
The pieces in one place. A Product base with a virtual destructor and a virtual describe method, three derived classes with their own state, a std::vector<std::unique_ptr<Product>> that owns them. Each derived destructor cleans up its own resources, the base destructor cleans up the base. Run with -fsanitize=address and there are no leaks.
The vector destroys its elements in order (front to back) when it goes out of scope. Each unique_ptr deletes its held pointer, virtual dispatch picks the right destructor, and the derived cleanup runs before the base cleanup chains in. Remove the virtual on ~Product, and close download and release packaging both disappear from the output, replaced by undefined behavior. The single keyword is the only thing standing between this working version and a leaking one.
10 quizzes