C++ / INHERITANCE AND POLYMORPHISM
Virtual destructors and deleting through a base
Decide when a class needs a virtual destructor and predict exactly what runs when a derived object is deleted through a base pointer.
What you will learn
- Add `virtual ~Base() = default;` to any class deleted through a base pointer
- Explain why `delete` through a non-virtual base destructor is UB, not just a leak
- Trace destruction order: derived body, derived members, then the base destructor
- Use a protected non-virtual destructor to ban deletion through a mixin base
Understanding Virtual destructors and deleting through a base
`delete c` has two jobs: run the destructor of the object, then hand the storage back to `operator delete`. The destructor call is resolved like any other member call, from the static type of the expression, so with `Connection* c` the compiler emits a call to `Connection::~Connection` unless that destructor is virtual. Nothing in the derived part is then torn down: `TcpConnection`'s members are never destroyed and its destructor body never runs. The standard does not even promise that much, because the sized deallocation is handed the size of the base rather than the size of the real object, so this is undefined behaviour and not a tidy leak.
Marking the base destructor virtual adds one vtable slot, and every derived class fills it with its own destructor. Dispatch only decides where destruction starts: once `~TcpConnection` is entered, the compiler-generated tail destroys that class's members in reverse declaration order and then calls `~Connection` unconditionally. That automatic chaining is why you never call a base destructor by hand, and why destruction order is the exact mirror of construction order. The vtable pointer is also rewound to each base's table as that level begins, which is why a virtual call made from inside `~Connection` reaches `Connection`'s version, never the derived override.
The design rule follows directly: if a class will ever be deleted or owned through a pointer or reference to it, give it a public virtual destructor, usually just `virtual ~Base() = default;`. If it will not, a small mixin or a base you only hold by value, make the destructor protected and non-virtual so `delete basePtr` becomes a compile error instead of a silent bug and the class keeps its layout free of a vtable pointer. One side effect to plan for: declaring any destructor, even a defaulted virtual one, suppresses the implicit move constructor and move assignment, so a base that should stay movable has to declare those as well.
<iostream>
struct Connection {
Connection() { std::cout << "Connection()\n"; }
virtual void send(int n) { std::cout << "base send " << n << "\n"; }
virtual ~Connection() { std::cout << "~Connection()\n"; }
};
struct TcpConnection : Connection {
int* buffer;
TcpConnection() : buffer(new int[4]{}) { std::cout << "TcpConnection() got buffer\n"; }
void send(int n) override { buffer[0] = n; std::cout << "tcp send " << n << "\n"; }
~TcpConnection() override { delete[] buffer; std::cout << "~TcpConnection() freed buffer\n"; }
};
int main() {
Connection* c = new TcpConnection(); // static type Connection*, dynamic type TcpConnection
c->send(7);
delete c; // ~TcpConnection first, then ~Connection
std::cout << "done\n";
}
A destructor call is resolved from the static type at the delete site, so only `virtual` on the base destructor makes `delete basePtr` destroy the whole object.
Worked examples
The same object destroyed two ways
Shows that a non-virtual base destructor changes the result of destruction depending only on how the object is reached.
<iostream>
struct Base {
~Base() { std::cout << "~Base\n"; } // not virtual
};
struct Derived : Base {
~Derived() { std::cout << "~Derived\n"; }
};
int main() {
{
Derived d; // complete type is known here
}
std::cout << "---\n";
Base* p = new Derived;
delete p; // only ~Base is called
}
Example explained
Line 1`Derived d;` leaving the block needs no dispatch: the compiler knows the complete type and calls `~Derived`, then `~Base`.
Line 2`delete p;` names `Base::~Base`, which is not virtual, so no vtable lookup happens and `~Derived` is skipped completely.
Line 3Any member of `Derived` holding memory, a file handle or a lock would never be released on that path.
Line 4That delete is undefined behaviour; the printed result is what gcc and clang produce in practice, and both warn under `-Wall`.
unique_ptr needs it, shared_ptr does not
Demonstrates that only `shared_ptr` survives a missing virtual destructor, because it stores a deleter fixed at construction time.
<iostream>
<memory>
struct Base { ~Base() { std::cout << "~Base\n"; } }; // still not virtual
struct Derived : Base { ~Derived() { std::cout << "~Derived\n"; } };
int main() {
{
std::shared_ptr<Base> sp = std::make_shared<Derived>();
std::cout << "shared_ptr leaving scope:\n";
}
{
std::unique_ptr<Base> up = std::make_unique<Derived>();
std::cout << "unique_ptr leaving scope:\n";
}
}
Example explained
Line 1`make_shared<Derived>()` builds a control block that remembers the type it created, so it destroys a `Derived` even though the pointer is typed on `Base`.
Line 2Converting to `shared_ptr<Base>` copies that control block pointer rather than making a new deleter, which is why `~Derived` still runs.
Line 3`unique_ptr<Base>` uses `default_delete<Base>`, whose body is `delete ptr` on a `Base*`, so it reproduces the hand-written bug exactly.
Line 4Read this as a warning, not a workaround: `unique_ptr`, raw `delete` and containers of raw pointers all still require the virtual destructor.
Forbidding deletion instead of making it virtual
Uses a protected non-virtual destructor so the class can be a base but can never be deleted through a pointer to it.
<iostream>
struct Counter { // a mixin, never owned through Counter*
int hits = 0;
void bump() { ++hits; }
protected:
~Counter() { std::cout << "~Counter\n"; }
};
struct Widget : Counter {
~Widget() { std::cout << "~Widget\n"; }
};
int main() {
Widget w;
w.bump();
w.bump();
std::cout << "hits=" << w.hits << " sizeof=" << sizeof(Widget) << "\n";
// Counter* c = &w; delete c; // error: ~Counter is protected
}
Example explained
Line 1`~Counter()` under `protected:` is reachable only from `Counter` and its derived classes, so a `delete` on a `Counter*` in `main` will not compile.
Line 2`Widget`'s implicit destructor does have that access, so ordinary destruction of the stack object still chains `~Widget` then `~Counter`.
Line 3Nothing is virtual here, so `Widget` carries no vtable pointer and `sizeof=4` is just the single `int`.
Line 4Uncommenting the last line gives a diagnostic along the lines of calling a protected destructor of class `Counter`.
Important notes
The tidy "only the base destructor ran" outcome is what mainstream compilers happen to emit, not a guarantee. Under multiple inheritance the base pointer may not even hold the address the allocator handed out, so the same mistake can corrupt the heap instead of leaking quietly.
A pure virtual destructor is legal, `virtual ~Base() = 0;`, but you must still supply a definition out of line, because every derived destructor calls it as part of the chain.
Common mistakes
Marking only the derived destructor virtual. `virtual` has to be on the destructor named at the delete site, so `delete basePtr` still calls just `~Base` and every derived member leaks.
Expecting a virtual destructor to fix `delete[]` through a base pointer. Array deletion strides by the base's size, so `Base* p = new Derived[3]; delete[] p;` is undefined behaviour either way; hold a container of pointers instead.
Adding `virtual ~T() {}` to a plain value type just in case. Every object grows a vtable pointer and the implicit move operations vanish, so `std::vector<T>` starts copying where it used to move.
Try it yourself
Change, predict, then run
Write a `Shape` base and a `Polygon : Shape` that owns `new double[3]`, printing a line in each destructor, then delete a `Polygon` through a `Shape*` both with and without `virtual` on `Shape`'s destructor and compare the two outputs.
Open the C++ workspaceCheck your understanding
`Base` has a plain `~Base()`, while `Derived` declares `virtual ~Derived()`. What happens at `Base* p = new Derived; delete p;`?
- Both destructors run, because `~Derived` is declared virtual and dispatch finds it
- Only `~Derived` runs; `~Base` is skipped because the base destructor is not virtual
- Only `~Base` runs; `virtual` on `~Derived` has no effect at a delete site whose static type is `Base`
- It fails to compile: a derived destructor cannot be virtual unless the base destructor is
Show answer
Virtual dispatch requires the function named at the call site to be virtual, and the delete site names `Base::~Base`; the `virtual` on `~Derived` only matters for classes derived further from `Derived`. Option 1 is tempting because `virtual` feels like a property of the hierarchy, but it propagates from base to derived only, never upward. Option 2 also gets the chaining backwards: once `~Derived` starts, `~Base` always runs at the end of it.