5.2.7 · D3C++ Programming

Worked examples — Destructor — RAII principle

4,130 words19 min readBack to topic

The scenario matrix

Think of "cases" the way a trig topic has quadrants. For destructors, the "quadrants" are the different exit paths and ownership situations a resource can be in. If you can handle every row below, nothing can surprise you.

Cell Scenario class The tricky part Worked in
A Normal scope exit (happy path) destructor just fires at } Ex 1
B Exception thrown mid-function stack unwinding must still clean up Ex 2
C Early return before end cleanup must not be skipped Ex 3
D Multiple objects — destruction order LIFO, reverse of construction Ex 4
E Base + members inside one object derived → members → base Ex 5
F Copying a resource-owning object shallow copy → double-free (Rule of Three) Ex 6
G Polymorphic delete through base pointer needs virtual destructor Ex 7
H Degenerate/zero input (size 0, nullptr) destructor must still be safe Ex 8
I Limiting case — nested scopes, exception during unwinding how far cleanup reaches Ex 9
J Real-world word problem — file + lock together multiple RAII guards stacked Ex 10
K Exception thrown inside a constructor only built subobjects are destroyed Ex 11
L Destructor that throws during unwinding double-exception → std::terminate Ex 12

Every example below is tagged with its cell letter. Together they cover all twelve.


Setup: the toy class we trace

To make destruction visible we use a class that prints when it dies. This is the standard exam trick — a destructor with a cout so you can read the order.

struct Tracer {
    std::string name;
    Tracer(std::string n) : name(n) { std::cout << "ctor " << name << "\n"; }
    ~Tracer()                        { std::cout << "dtor " << name << "\n"; }
};

Cell A — normal scope exit


Cell B — exception thrown mid-function

This is the whole reason RAII exists. See Exception Safety & Stack Unwinding.

Figure s01 (described in words): a horizontal timeline of one function call. A blue box "ctor x (acquire)" sits at the left; a red box "throw boom!" in the middle; a dashed gray box "ctor y (never runs)" on the right; and an orange box "dtor x (release)" below. A red arrow leaves the throw box and shoots out of the function past y; an orange arrow also leaves the throw and curves back down to dtor x. The takeaway printed in green: "x built → x destroyed, even without reaching the closing brace." The picture shows that the exception skips everything after the throw except the destructors of already-built locals.

Figure — Destructor — RAII principle

Cell C — early return


Cell D — destruction order (LIFO)


Cell E — base class + members inside ONE object

When a single object is destroyed there is a strict internal order. See Virtual Functions & Polymorphism and Constructor — initialization.

Figure s02 (described in words): a vertical stack of four boxes representing one Widget object. From bottom to top: blue "Base subobject", green "member m1", green "member m2", orange "Widget body". A blue downward arrow on the left is labelled "CONSTRUCT: base → members → body". An orange upward arrow on the right is labelled "DESTROY: body → m2 → m1 → base". Caption: "Destruction is the exact mirror of construction." The image makes the mirror symmetry literal — you build from the bottom up and tear down from the top down.

Figure — Destructor — RAII principle

Cell F — copying a resource owner (Rule of Three)

Figure s03 (described in words): on the left, two object boxes — blue "a.data" and orange "b.data (copy of a)". On the right, a single green box "one int[4] buffer". A blue arrow and an orange arrow both point from the two objects to the same green buffer, labelled in red "same address". Below: "delete[] (b) then delete[] (a) → SAME buffer freed twice = double free", and a gray caption "new[] count = 1 but delete[] count = 2." The figure shows visually why two owners of one buffer is the bug.

Figure — Destructor — RAII principle

Cell G — polymorphic delete through a base pointer


Cell H — degenerate / zero inputs


Cell I — limiting case: nested scopes + how far cleanup reaches


Cell J — real-world word problem: file + lock together


Cell K — exception thrown inside a constructor

A subtle but exam-favourite case: what gets cleaned up if the object is only half-built when it throws?


Cell L — a destructor that throws during unwinding

The one place RAII can bite back: a destructor must never let an exception escape while another exception is already in flight.


Recall Self-test: name the cell

A function throws after building two locals p then q. Which die and in what order? ::: Both die; q first, then p (LIFO during unwinding — Cell B/D/I). You wrote only a destructor for a class holding a char*. What breaks on X b = a;? ::: Shallow copy aliases the pointer → double delete at end → double-free. Fix with Rule of Three or Rule of Zero (Cell F). delete p; through Base* only prints ~Base. What is missing? ::: A virtual destructor on Base; without it ~Derived is skipped and its resources leak (Cell G). Is delete[] nullptr; safe? ::: Yes — it is a defined no-op; no guard needed (Cell H). Inside one object, what order: base dtor, member dtors, derived body? ::: Derived body → members (reverse decl) → base (Cell E). A constructor throws after two members are built. Does the object's own destructor run? ::: No — the object was never fully constructed; only the already-built members are destroyed in reverse (Cell K). Why must a destructor be noexcept? ::: If it throws during stack unwinding of another exception, C++ calls std::terminate() and the program dies (Cell L).


Connections

Case Map

normal

exception

early return

many locals

base plus members

raw resource copied

base pointer delete

zero or null

throw in ctor

throw in dtor

How does the scope end

Cell A dtor at brace

Cell B unwinding cleans up

Cell C still cleaned

What is owned

Cell D LIFO order

Cell E derived members base

Cell F Rule of Three

Cell G virtual dtor

Cell H safe no-op

Edge cases

Cell K only built parts die

Cell L terminate