Debug Foundry

The early return keeps the lock. The throw keeps it too. One small object doesn't.

Drag to turn · pinch or ctrl-scroll to zoom · pinch to zoom

Finished state of the RAII simulation: a function scope containing a small guard object, tethered to a shared mutex block beside it, the mutex shown unlocked

The mutex
is locked by hand

Replay the ending, leaving by
void update(Account& a, Amount amt) {
  std::lock_guard<std::mutex> g(a.m);  // acquires the lock
  if (!a.open) return;               // early exit — still unlocks
  a.post(amt);                       // may throw — still unlocks
}                                     // the guard's scope ends here
Read the chapter

What it's for

  • Every resource with an acquire and a release: locks, files, sockets, transactions, handles of every kind. Smart pointers settled memory; RAII is the same move for everything else.
  • The bug it kills is the one you can't see in review: the unlock that runs on the happy path but not on the early return, the close that the exception jumps over. Real code has more exits than readers.

Good to know

RAII binds a resource's release to an object's lifetime, and an automatic object's lifetime is its scope. The constructor acquires, the destructor releases — so the language, not your memory, pairs them up. The guarantee is precise: destructors run for fully constructed automatic objects on normal scope exit and during exception stack unwinding, generally in reverse construction order. That is also the whole guarantee — abort(), std::_Exit, a killed process or a power cut run no destructors at all. Three honest edges. First, construction can fail halfway: a throwing constructor destroys the members and bases that finished building, but the half-built object's own destructor never runs — which is why members should each own their own resource. Second, ownership policy is yours to choose: copying a raw handle makes two owners and two releases, so RAII types are either non-copyable (like std::lock_guard, which isn't movable either) or move-only (like std::unique_lock), and a move transfers the one ownership, leaving the source safely empty. Third, a destructor must never let an exception escape during unwinding — and a destructor quietly closing a file is not a durable commit: flush and close can fail, and that failure needs an explicit call in your code, on the paths where you're allowed to see it. And for mutexes specifically: the lock's lifetime must cover every access it protects, and RAII does nothing about deadlock, data races, or lock ordering — it pairs the acquire with the release, no more.

Why the steps go in this order

  • The leak comes first, twice: the early return and the throw are the two exits manual cleanup forgets, and they have to hurt before the fix means anything.
  • The guard is introduced by its contract — constructor acquires, destructor releases — before any exit is replayed, so each replay tests the contract, not the syntax.
  • Normal exit, then early return, then throw: the same object, the same identity, three ways out, one release each time. The throw comes last because unwinding is the subtle one.
  • The payoff names the mechanism: release is bound to a lifetime, not to remembering.

Before you rely on it

This is a toy: one mutex, one guard, three paths, and a padlock that changes color. The code card is real, compilable C++, and the behavior it claims is the language's stated rule — destructors for fully constructed automatic objects run on scope exit and during unwinding, in reverse construction order — not something the simulation measured. The omitted edges are the ones the article names: no destructors after abort, kill or power loss; no destructor for a half-built object; no durability from a closing destructor; no help with deadlock or races.

References