5.2.7 · D4C++ Programming

Exercises — Destructor — RAII principle

3,400 words15 min readBack to topic

Quick vocabulary refresher, so no symbol appears unearned:


Level 1 — Recognition

Can you spot the rule?

Recall Solution 1.1

Valid: (a) and (d).

  • (a) correct declaration: name ~Box, no parameters, no return type. ✅
  • (b) ✗ — a destructor takes no parameters.
  • (c) ✗ — a destructor has no return type, not even void.
  • (d) ✅ — this is the out-of-class definition of the same destructor. Why: the standard fixes the destructor's signature exactly; you only get to write its body.
Recall Solution 1.2

False. There is exactly one destructor per class. Since it takes no parameters, there is nothing to overload on. (Contrast with constructors, which can be overloaded because they take arguments.)

Recall Solution 1.3
Event Destructor runs?
Local object reaches } Yes
delete p; Yes (destroys the pointee — the object at the address, defined above)
Raw pointer goes out of scope No — the pointer (the address slip) dies, but the pointee it names is untouched → leak
Temporary at the ; Yes — a temporary is destroyed at the end of the full expression, i.e. at the ;, not at the block's }
Global object at program end Yes
Why the third is No: a raw pointer is just a number holding an address. Destroying the number does nothing to the memory it pointed at. That is exactly the hole RAII plugs.
Why the temporary is Yes and early: the compiler builds it, uses it within the expression, then destroys it immediately at the ;. This is why std::lock_guard<std::mutex>(m); (an unnamed temporary!) is a bug — it locks and instantly unlocks at the ;, protecting nothing. You must give it a name.

Level 2 — Application

Predict the output.

Recall Solution 2.1

+a +b +c -c -b -a Construction is top-to-bottom: a, then b, then c. Destruction is LIFO — reverse order — so c dies first, then b, then a. Read the figure below: it draws the three objects as a stack of chalk boxes. The white arrow on the left shows construction pushing a, b, c upward (bottom to top); the pink arrow on the right shows destruction popping from the top down — c first (top, dies first), a last (bottom, dies last). That top-off-first motion is LIFO — a stack of plates: the last plate on is the first plate off.

Figure — Destructor — RAII principle

Recall Solution 2.2

+a +b -b +c -c -a b lives only inside the inner { }, so it is destroyed before c is even built. Then at the outer }, the survivors c and a die LIFO: c first, a last. Lesson: destruction is driven by scope exit, not by declaration order alone.

Recall Solution 2.3

Prints +h +s -s — and there is a memory leak. s is destroyed at } (prints -s). But p is a raw pointer; leaving scope destroys the pointer, not the Loud pointee on the heap. No delete p;~Loud for 'h' never runs ⇒ -h never prints ⇒ leak. Fix: delete p; before }, or use std::unique_ptr<Loud>.


Level 3 — Analysis

Find the bug and explain the mechanism.

Recall Solution 3.1

Double delete[] → undefined behaviour (likely crash). Buf b = a; uses the compiler-generated copy constructor, which does a shallow copy: it copies the pointer value d, so a.d and b.d point to the same buffer. Read the figure below: the blue box a.d and the pink box b.d are two separate pointer variables, but both chalk arrows land on the same yellow "heap buffer int[10]" — they alias it. At }, both destructors run: ~a does delete[] and frees the buffer; ~b then does delete[] on that same, already-freed buffer — the double free the caption warns about.

Figure — Destructor — RAII principle
Fix — the Rule of Three: because you wrote a destructor that owns a raw resource, you must also write a copy constructor and copy assignment (doing a deep copy), or delete them. Best of all: follow Rule of Zero — store a std::vector<int> and write no destructor at all.

Recall Solution 3.2

Prints ~Base only — buf leaks. delete p; looks at p's static type Base*. Because ~Base is not virtual, the call is resolved at compile time to ~Base alone; ~Derived never runs, so delete[] buf never happens. Fix: make the base destructor virtual: virtual ~Base(). Then delete p; dispatches at runtime to ~Derived first (frees buf), then ~Base. With the fix the output is ~Derived ~Base . See Virtual Functions & Polymorphism.

Recall Solution 3.3

~Dbody ~M2 ~M1 ~Base Destruction of one object goes: (1) the derived destructor body runs first (~Dbody), then (2) members in reverse declaration orderm2 before m1 — then (3) the base class destructor (~Base). It is LIFO applied inside a single object: the base was built first, so it dies last.


Level 4 — Synthesis

Design correct RAII code.

Recall Solution 4.1
class FileGuard {
    FILE* f;
public:
    explicit FileGuard(const char* path)          // ACQUIRE
        : f(std::fopen(path, "r")) {
        if (!f) throw std::runtime_error("open failed");
    }
    ~FileGuard() noexcept {                        // RELEASE — always, never throws
        if (f) std::fclose(f);
    }
    FileGuard(const FileGuard&)            = delete;  // forbid copy (no two owners of one handle)
    FileGuard& operator=(const FileGuard&) = delete;
    FileGuard(FileGuard&& o) noexcept : f(o.f) { o.f = nullptr; } // MOVE: steal + null out source
    FileGuard& operator=(FileGuard&& o) noexcept {                // move-assign: close mine, steal
        if (this != &o) { if (f) std::fclose(f); f = o.f; o.f = nullptr; }
        return *this;
    }
    FILE* get() const { return f; }
};

Why each piece:

  • Acquire in the constructor; throw if it fails, so a FileGuard never holds a broken handle.
  • Release in the destructor, guarded by if (f) (the handle may have been moved-from → null). Marked noexcept — see the next box.
  • Copy deleted: copying would make two guards own one handle → double fclose.
  • Move defined: moving transfers ownership — the source's f is set to nullptr so its destructor closes nothing. This is the move completion of the Rule of Five: once you delete copy but own a resource, you must supply move (or the type becomes immovable, which is often too restrictive). Now FileGuard g = openLog(); can return a guard by value cheaply. Usage void read(){ FileGuard g("data.txt"); parse(g.get()); } closes the file even if parse throws.
Recall Solution 4.2
struct Tracker {
    int& c;                                                  // reference to the shared counter
    explicit Tracker(int& counter): c(counter) { ++c; }      // grab
    ~Tracker() noexcept { --c; }                             // drop — cannot throw
};

Each loop iteration: ++active on construction (→ 1), then --active at the closing } (→ 0). Net change per iteration is 0. After the loop, active == 0. Because the decrement lives in the destructor, it fires even if the work throws — the counter can never get stuck too high. This is the same idea as lock_guard: acquire in the constructor, release in the destructor, and scope exit does the release on every path.


Level 5 — Mastery

Trace a full program with exceptions, order, and polymorphism together.

Recall Solution 5.1

+a +x -x [caught] +b -b -a Step by step:

  1. +aa built in outer.
  2. inner() called → +x.
  3. throw fires. y was never constructed, so it is not destroyed. Stack unwinding destroys fully-built locals of inner in LIFO order → only x-x.
  4. Control lands in catch[caught].
  5. Execution continues past the try+b.
  6. outer ends: destroy b then a (LIFO) → -b -a. Key point: an object that was never fully constructed is never destroyed; only completed locals participate in unwinding. This is the guarantee that makes RAII exception-safe — provided none of those destructors throws while unwinding is already underway.
Recall Solution 5.2

Output: ~Circle ~Log ~Shape ~Shape is virtual, so delete s (a Shape*) dispatches to the most-derived destructor. Order within one object: derived body ~Circle, then members in reverse declaration order (~Log), then base ~Shape. If ~Shape were non-virtual: delete s would call only ~Shape, printing just ~Shape ~Circle and ~Log would be skipped. Any resource owned by Circle or its members would leak, and the behaviour of deleting a derived object through a non-virtual base pointer is actually undefined. Rule: a base used polymorphically needs a virtual destructor.


Recall Self-check score card

All L1–L2 correct? You recognise and apply the rules. ::: Solid foundation — you can read RAII code. L3 bugs spotted? ::: You understand why the Rule of Three and virtual destructors exist. L4–L5 designed/traced correctly? ::: You can write exception-safe RAII, handle move semantics, keep destructors noexcept, and reason about unwinding — mastery.


Connections