5.2.13 · D5C++ Programming

Question bank — RAII — resource acquisition is initialization — why it's the key idiom

1,816 words8 min readBack to topic

Two pictures to hold in your head

Before the traps, burn these two images in — almost every question below is really about one of them.

Picture 1 — the stack destroys in reverse (LIFO). Objects are built bottom-up as a scope runs; when the scope ends for any reason, they are torn down top-down. RAII rides on this guaranteed teardown.

Figure — RAII — resource acquisition is initialization — why it's the key idiom

Picture 2 — one owner vs. shared ownership. A unique_ptr is the sole owner: exactly one destructor frees the resource. A shared_ptr keeps a reference count in a side "control block": only the last owner to die runs delete. This is why one is copyable-safe and the other forbids copying.

Figure — RAII — resource acquisition is initialization — why it's the key idiom

True or false — justify

Every item is "True or False, and say why". A bare yes/no scores zero — the justification is the whole point.

RAII's name accurately describes what makes it powerful.
False. The power is in the destructor (guaranteed release), not in initialization — a better name would be "Scope-Bound Resource Management". The destructor is the special member that runs at end of an object's life; see Constructors and Destructors.
If a function has no throw statements, RAII gives you no benefit.
False. Any function it calls might throw, and even without exceptions, multiple return/break paths each skip manual cleanup — RAII covers all exits, not just exceptions.
A destructor is guaranteed to run when a local object's scope ends by normal return, break, or a thrown exception.
True. All three leave the scope via stack unwinding (the reverse-order teardown in Picture 1), which runs every local's destructor. The one exception is a process-terminating path like std::exit(), std::abort(), or std::terminate() — those skip local destructors (see Edge cases). See Stack Unwinding and Exceptions.
Wrapping a raw new in a std::unique_ptr can cause a double-free if you copy the pointer.
False. A unique_ptr (the single-owner smart pointer in Picture 2) is non-copyable by design; the compiler rejects a copy, so the double-free is structurally impossible. See Smart Pointers - unique_ptr shared_ptr.
Two shared_ptrs to the same object will double-free when both go out of scope.
False. A shared_ptr keeps a reference count; each copy bumps it up, each destruction bumps it down, and only the last owner (count → 0) runs delete — exactly the mechanism in Picture 2. See Smart Pointers - unique_ptr shared_ptr.
std::vector<int> v(100) and int* p = new int[100] are equally leak-safe.
False. vector's destructor frees the buffer automatically on any scope exit; the raw new[] needs a matching delete[] that an exception or early return can skip.
Local objects are destroyed in the same order they were constructed.
False. They're destroyed in reverse (LIFO) order — the up-pointing arrow in Picture 1 — so a later object can safely depend on resources an earlier one owns until the dependent is gone.
Moving from an RAII object and then letting the source be destroyed frees the resource twice.
False. A correct move steals the pointer and sets the source's to nullptr; the source's destructor then does nothing, keeping "exactly one owner frees" true. A move transfers ownership rather than copying it; see Move Semantics.
Making a destructor noexcept is just a style preference.
False. If a destructor throws during stack unwinding (while another exception is active), the program calls std::terminate — so noexcept (a promise not to throw) on destructors is a correctness requirement.

Spot the error

Each line describes flawed code or reasoning; the reveal names the bug and the fix.

Widget* w = new Widget(); use(w); delete w; — what breaks it?
If use(w) throws, delete w is skipped → leak. Fix: auto w = std::make_unique<Widget>(); so the destructor frees it on any exit.
A class holds a FILE* and relies on the compiler-generated copy constructor — what goes wrong?
The default copy duplicates the pointer value, giving two objects the same handle → double fclose. Fix via the Rule of Five (control every copy/move/destroy moment); see Rule of Three Five Zero.
m.lock(); risky(); m.unlock(); inside a function — the trap?
If risky() throws, unlock() never runs → the mutex stays locked forever → deadlock. Fix: std::lock_guard<std::mutex> g(m);, a guard whose destructor unlocks automatically. See std::lock_guard and Mutexes.
An RAII owner's destructor does throw std::runtime_error(...) on a failed cleanup — why forbidden?
If it fires during unwinding of another exception, two exceptions are in flight → std::terminate. Fix: swallow or log the error, mark the destructor noexcept.
A function returns const char* pointing into a local std::string, then the string goes out of scope. What's the fault?
The string's buffer is freed at scope end, so the returned pointer dangles → use-after-free. Fix: return by value or transfer ownership.
std::unique_ptr<int> a = b; where b is another unique_ptr — will it compile?
No — copy is deleted. You must move: std::unique_ptr<int> a = std::move(b);, which transfers ownership and empties b. See Move Semantics.
Someone writes a Rule-of-Five class but forgets to release the old resource in copy-assignment.
The old owned resource leaks when the object is overwritten. Copy assignment must release-then-acquire (or copy-and-swap). See Rule of Three Five Zero.

Why questions

Why is putting a resource inside a stack object the whole trick, rather than being careful with delete?
Because the language guarantees stack objects' destructors run on every exit path (Picture 1); "being careful" relies on a human never missing a return/throw, which fails at scale.
Why does move semantics need to null the source pointer, not just copy it?
To preserve the single-owner invariant — if both pointers stayed valid, both destructors would free the same resource → double-free. See Move Semantics.
Why does shared_ptr need a separate control block instead of just a raw count in the object?
The count must survive independently and be shared across all copies (the side box in Picture 2); embedding it would tie its lifetime to the object it's trying to manage. See Smart Pointers - unique_ptr shared_ptr.
Why is the Rule of Five about controlling "every moment the resource could be created, copied, or destroyed"?
If you leave any of those moments to the compiler default, it does a shallow pointer copy — silently breaking ownership; you must own all five to keep the invariant. See Rule of Three Five Zero.
Why does RAII give you the "basic" and even "strong" exception-safety guarantee almost for free?
Because cleanup is automatic on unwinding, no resource leaks when an exception propagates — the guarantee is a consequence of scope-bound release. See Exception Safety Guarantees.
Why prefer std::lock_guard over manually pairing lock()/unlock()?
The guard's destructor unlocks on any exit including exceptions, so you can never leave a critical section locked. See std::lock_guard and Mutexes.
Why is "Rule of Zero" often the best RAII advice?
If your class only holds already-RAII members (smart pointers, vectors, strings), you write no special members — the members clean themselves up, and there's no hand-written destructor to get wrong. See Rule of Three Five Zero.

Edge cases

If a constructor throws after acquiring one resource but before finishing, does the destructor run to clean it up?
No — the destructor only runs for fully constructed objects. But already-constructed members are destroyed, which is exactly why sub-resources should themselves be RAII members. See Constructors and Destructors.
What happens to RAII cleanup if you call std::abort(), std::exit(), or the process crashes?
Destructors of local objects are not run — these terminate the process without normal stack unwinding, so RAII's guarantee doesn't apply and OS-level cleanup (closing handles) is your only backstop.
Does a normal thrown exception (that gets caught) skip destructors like std::exit() does?
No — a thrown exception is not a crash. As it propagates it unwinds the stack normally, running every local destructor along the way; only an unhandled exception reaching std::terminate, or an explicit std::abort()/std::exit(), skips them.
A unique_ptr is default-constructed and never assigned a resource. Does its destructor cause trouble?
No — it holds nullptr, and delete nullptr is a well-defined no-op, so the empty state is perfectly safe.
An exception is thrown while the stack is already unwinding from a prior exception — what does C++ do?
It calls std::terminate; you can't have two exceptions propagating at once, which is precisely why destructors must be noexcept. See Stack Unwinding and Exceptions.
A shared_ptr cycle (two objects pointing at each other) goes out of scope. Are they freed?
No — each keeps the other's count above zero, so neither reaches count 0 → a leak. Break cycles with weak_ptr. See Smart Pointers - unique_ptr shared_ptr.
You move-construct from an RAII object and then use the moved-from source. Is that undefined behaviour?
Not UB, but the source is in a valid-but-unspecified (usually empty/null) state; reading it is legal, relying on old contents is a bug. See Move Semantics.
A global/static RAII object owns a resource — when does its destructor run?
At program exit, in reverse order of construction — but ordering across translation units is unspecified, so cross-dependencies between statics are fragile.
Recall One-line summary to carry away

RAII works because the destructor of a stack object is guaranteed to run whenever the scope unwinds normally; every trap above is either "someone bypassed that unwinding (e.g. std::exit/abort)" or "someone broke the single-owner invariant."