Skip to content

← RAII: Objects With Lifetimes step 4 of 4

Medium Primitives

Close a transaction when an operation throws

A cleanup call after an operation is skipped if that operation throws:

ledger.open();
ledger.add(delta);  // may throw
ledger.close();

A local guard can pair these operations. Its constructor opens the transaction; its destructor closes it. Once construction succeeds, the guard's destructor runs at normal scope exit and when that scope is unwound toward an exception handler. Cleanup belongs to the guard instead of being repeated at each exit.

Cleanup and state are separate responsibilities

Exception guarantees describe what callers can rely on if an operation fails:

Guarantee Meaning
No-throw The operation does not let an exception escape.
Strong Failure leaves the state covered by the operation's contract unchanged.
Basic Failure preserves valid state and avoids resource leaks, but values may change.

RAII helps with resource cleanup. It does not automatically establish the basic guarantee: an operation could release every resource yet leave two related fields inconsistent. It also does not undo changes made before a failure. A strong guarantee often requires preparing changes separately and committing them with an operation that cannot fail.

Cleanup destructors should not throw. A non-throwing swap can also be useful for committing a prepared result, but not every swap is inherently non-throwing. Declaring a function noexcept is a promise about escaping exceptions; it does not make operations inside the function incapable of failing.

Your task

Implement:

int apply_all(Ledger& ledger, const std::vector<int>& deltas);

Keep the supplied Ledger unchanged. For each delta, in input order:

  1. Construct a local transaction guard that calls ledger.open().
  2. Call ledger.add(delta).
  3. Let the guard's destructor call ledger.close() exactly once.

Use one guard per iteration. Make the guard noncopyable so copying it cannot create two objects that both close the same transaction. The referenced Ledger must outlive the guard.

Ledger::add throws std::runtime_error for exactly 13, before changing the balance. Catch that exception around the loop, stop processing, and return ledger.balance(). Successful earlier updates remain applied; later deltas are skipped. Negative deltas and zero are ordinary updates.

For [10, 20, 13, 30], the first two updates produce 30. The third opens a transaction and throws; unwinding closes it before the catch runs. The final delta is skipped, and the result is 30. This is partial progress, not rollback of the whole input. Here a transaction only records a matched open and close; it is not a database transaction with automatic rollback.

Empty input leaves the ledger unchanged. If the first delta is 13, no delta is applied. The harness starts with a zero balance, but apply_all should use the ledger it receives without resetting it. Test balances, intermediate sums and transaction counts stay within int's range.

What the harness checks

It reports [returned_balance, opened_minus_closed]. For the example above, expect [30, 0]. A positive difference means unmatched opens; a negative one means too many closes. Zero is necessary, but the counter alone cannot prove that every open was paired correctly at the right time.

The starter returns the expected balance but leaves one transaction open on the throwing path. Remove its manual open/close pair from the loop and use the guard instead. Do not close again in the catch block: unwinding has already destroyed the guard before control reaches that handler.

Ledger::open and close only increment counters and do not throw for the supported inputs. A real guard must also account for failures during acquisition and ensure its cleanup can safely run during unwinding.