Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Strong Guarantee Patterns

The strong guarantee — commit-or-rollback at the function level — is not free, and most code does not need it. But for the operations that do need it (anything that mutates observable state in a way that another thread, process, or user might see partway through), there is a small, well-known set of patterns that achieve it. This chapter walks through them.

The unifying idea behind every pattern in this chapter is the same trick: separate the parts of the operation that can throw from the parts that mutate observable state, and arrange for the throwing parts to happen first, on a side copy. Once everything that can throw is past, the mutation is reduced to a sequence of no-throw operations, and the strong guarantee falls out for free.

The patterns differ in how they arrange for the side copy and the eventual swap. They are:

  1. Copy-and-swap. The classic. Mostly used for assignment operators.
  2. Pimpl with swap. Same idea, applied at the object level via opaque pointer.
  3. Two-phase commit at the function level. Generalization. Build the result, then commit.
  4. Scope guards (commit-or-rollback). When you can’t sensibly build a side copy.
  5. Persistent / functional data structures. When you can avoid mutation entirely.

Copy-and-swap

The pattern is most often shown as an assignment operator:

class Image {
    char* data_;
    size_t size_;
public:
    void swap(Image& other) noexcept {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }

    Image(const Image& other);  // copy ctor, normal
    ~Image();                   // delete[] data_

    Image& operator=(Image other) {  // by value!
        swap(other);
        return *this;
    }
};

The key moves:

  1. operator= takes its argument by value, not by reference. The caller’s other is constructed via the copy constructor (or move constructor). If that throws, the assignment never runs — *this is untouched.
  2. Inside operator=, all we do is swap. swap is noexcept. We are now guaranteed to not throw.
  3. The original *this state is now in other, which is a function parameter and goes out of scope at the function’s end. Its destructor cleans up.

This achieves the strong guarantee, plus it covers the self-assignment case naturally (assigning x = x makes a copy first), plus it unifies copy-assignment and move-assignment if swap works on both. The cost: a copy. For some types the copy is expensive, in which case you pay it and you provide the strong guarantee, or you don’t and you provide the basic guarantee. Pick.

A subtlety: swap must be noexcept. If swap throws, the whole pattern collapses, because we end up in a half-swapped state with no recovery path. This is why the C++ Standard Library specifies that std::swap is noexcept for types whose move constructors and assignment operators are noexcept, and why writing your own swap for any non-trivial type means thinking about whether your member-wise swap can throw. Memberwise swap of pointer/integer fields cannot. Memberwise swap of std::string cannot (string’s swap is noexcept). Memberwise swap involving a type with a throwing swap… can. Avoid that.

Pimpl with swap

Pimpl is “pointer to implementation.” The class declares an opaque unique_ptr<Impl>, and all the actual data and member functions live in Impl. This is most often discussed as a compile-time-firewall pattern (changes to Impl don’t recompile users), but it also has an exception-safety property: making a copy of an object means making a copy of its Impl, which is one allocation, and swapping is then a pointer swap, which is noexcept.

// header
class Widget {
    struct Impl;
    std::unique_ptr<Impl> p_;
public:
    Widget(/*...*/);
    Widget(const Widget& other);
    Widget& operator=(Widget other) noexcept {
        std::swap(p_, other.p_);
        return *this;
    }
    ~Widget();
    // public API forwards to p_->...
};

// .cpp
struct Widget::Impl { /* lots of fields */ };
Widget::Widget(const Widget& other) : p_(std::make_unique<Impl>(*other.p_)) {}
Widget::~Widget() = default;

Same shape as copy-and-swap. The win is that the swap of an arbitrarily complex Impl reduces to swapping one pointer, which is unambiguously noexcept. The cost is one heap allocation per Widget and one indirection on every member access. For most code this is invisible. For tight inner-loop types, it is unacceptable; pimpl is not free.

Two-phase commit at the function level

The earlier transfer_to rewrite was an instance of this:

void Account::transfer_to(Account& other, int amount) {
    int new_self = balance_ - amount;
    int new_other = other.balance_ + amount;
    log_transfer(amount, &other);     // throwing work
    balance_ = new_self;              // commit (no-throw)
    other.balance_ = new_other;       // commit (no-throw)
}

The structure is: compute the new state into local variables (might throw, fine), do all the throwing work (might throw, fine), then commit the new state with a sequence of no-throw operations (cannot throw).

This is a pattern, not a syntactic construction. You apply it by reading the function and asking: “where are the throwing operations? where are the mutations? can I move all the throwing operations above all the mutations?” If yes, you can provide the strong guarantee. If no, you need a different pattern.

The pattern fails when:

  • The mutation must precede the throwing operation. (Some kinds of inserts into trees: you have to allocate the node and link it in before you can validate the rebalance.)
  • The mutations are themselves throwing. (Inserting into a vector while holding the strong guarantee for the whole batch.)
  • The “side copy” is prohibitively expensive. (The object holds a million elements and you want to modify two.)

For these cases, you have to use scope guards or accept the basic guarantee.

Scope guards (commit-or-rollback)

Scope guards generalize RAII to arbitrary cleanup actions. The pattern is: register an undo operation; if you reach the end of the operation, dismiss the guards (the undos do not run); if you throw before that point, the guards run their undos in reverse order.

template<class F>
class ScopeGuard {
    F f_;
    bool dismissed_ = false;
public:
    ScopeGuard(F f) : f_(std::move(f)) {}
    ~ScopeGuard() { if (!dismissed_) try { f_(); } catch(...) {} }
    void dismiss() noexcept { dismissed_ = true; }
};

template<class F>
ScopeGuard<F> make_guard(F f) { return ScopeGuard<F>(std::move(f)); }

Used:

void Inventory::move_item(Item& it, Bin& from, Bin& to) {
    from.remove(it);
    auto undo = make_guard([&] { from.add(it); });

    to.add(it);  // throws? undo runs, item back in `from`.
    undo.dismiss();
}

This is the general-purpose pattern when you cannot avoid in-place mutation. It is also the pattern you reach for when you have to coordinate cleanup across multiple resources where each cleanup action has its own subtlety. It composes well: each guard’s lambda captures whatever it needs, and the destructor order takes care of running them in reverse.

Caveats:

  • The undo itself must not throw, or if it does, you must absorb the throw (the example does, with try/catch(...) in the destructor). A throwing undo during normal stack unwinding from another exception calls terminate. This is the same constraint as any destructor.
  • The undo must actually undo. Writing the rollback for a complex operation is itself error-prone; in many cases you discover the rollback is approximately as much work as the original action. This is a real cost, not a syntactic one.
  • The undo must be effective even given partial state. If from.remove(it) had partial side effects that from.add(it) doesn’t restore, you didn’t actually roll back. Test the rollback path; this is precisely the part of the code that production almost never exercises.

Scope guards are in C++23 (std::experimental::scope_exit, eventually std::scope_exit), Boost (BOOST_SCOPE_EXIT), and folly (folly::ScopeGuard). D has them as a language feature (scope(exit), scope(success), scope(failure)). If your language doesn’t have them, write a five-line version; you’ll use it daily.

Persistent data structures

If you can avoid mutation entirely, the strong guarantee is automatic. A persistent data structure is one where operations return a new structure rather than mutating the existing one, with the new structure sharing as much memory with the old as possible. If the operation throws halfway through, the old structure is unaffected — you never had a reference to the half-built new one.

Clojure’s persistent maps and vectors are the canonical example; Scala’s immutable collections, Haskell’s everything, and (in C++) immer provide similar primitives. The cost is constant-factor memory and time overhead from the structural sharing — usually a small multiplier, sometimes worth it.

This is not a pattern you apply to existing code; it’s a choice you make about your data structures. Where you make it, exception safety becomes a non-issue, because there is no observable mutation to worry about. Where you don’t, the other patterns in this chapter still apply.

When the strong guarantee is achievable, when it’s prohibitive

A pragmatic taxonomy of operations:

Operation shapeStrong guarantee?Cost
Pure function (no mutation)Trivially yes.Free.
Single-field assignmentYes.Free.
Multi-field update of one objectUsually yes via two-phase commit.Small; cost of computing on the side.
Update of many fields where some computations require partial-built stateUsually no. Use scope guards for partial undos, accept basic.Modest.
Cross-object update (e.g. transfer between two accounts)Yes via two-phase commit if no-throw assignments at the end.Small.
Container insertion with strong guaranteeStandard library offers this (e.g. vector::push_back if move is noexcept).A copy if move isn’t noexcept.
Container insertion that breaks invariants on exception (e.g. partial sort)No general technique. Sort, then commit, if you can.A full copy of the data being sorted.
External effect (file write, network send, syscall with side effects)Approximate strong guarantee via rollback-by-compensation.Variable. The rollback is itself fallible.
Distributed state mutationStrong guarantee impossible without consensus or sagas.Large. See chapters 8 and 9.

The hardest cases are the bottom two. There is no in-process technique that buys you the strong guarantee for an operation that has irreversibly committed state outside the process. You can compensate (saga), you can use a coordinator (two-phase commit at the distributed level), you can shrink the window (write-ahead log + idempotency), but you cannot, in general, make a network send un-happen.

How to recognize the difference

A useful exercise on a function you’ve written: read it line by line, and at each line, ask “what is the visible state of every observable object if I throw right here?” If the answer is “the same as before the call started,” you have the strong guarantee at that point. If the answer is “the same as before, plus some bookkeeping in *this,” you have the basic guarantee at that point and need to either accept it or restructure to push the throwing line earlier.

Most production C++ code, walked through this exercise, reveals that:

  • The first 60% of functions are accidentally strong-guarantee, because they happen to do all their throwing operations first.
  • The next 30% are basic-guarantee, with one or two specific lines that need either reordering (cheap) or a scope guard (medium).
  • The remaining 10% are interesting — they involve cross-cutting state that is hard to roll back, and the right answer is usually “redesign so the operation is no longer transactional,” not “make it transactional.”

That last category is where the lessons of the rest of this book apply most. The smart contract problem (chapter 8) is exactly this: cross-cutting mutable state, with throws (or reentrancy, which is the same shape) capable of interleaving the mutations. The fix is structural, not local.

A worked example: a safe_replace for a vector

Here is a small problem to make the patterns concrete: replace the ith element of a vector with a new value, providing the strong guarantee. Sounds trivial. Let’s see.

template<class T>
void safe_replace(std::vector<T>& v, size_t i, const T& new_val) {
    v[i] = new_val;  // basic guarantee
}

This is basic, not strong. If T’s assignment operator throws partway through copying, v[i] is in some unspecified valid state — possibly different from both the old and new values.

Two-phase commit version:

template<class T>
void safe_replace(std::vector<T>& v, size_t i, const T& new_val) {
    T tmp = new_val;            // copy might throw, fine
    using std::swap;
    swap(v[i], tmp);            // must be noexcept
}

If T’s swap is no-throw (true for almost all standard library types), this provides the strong guarantee. We made a copy of new_val outside the vector; if that throws, the vector is untouched. Once the copy succeeds, we swap, which can’t throw, so we either swap or we never tried.

Move-aware version:

template<class T>
void safe_replace(std::vector<T>& v, size_t i, T new_val) {  // by value
    using std::swap;
    swap(v[i], new_val);
}

Now the caller can pass an rvalue and we move-construct new_val from it. If T’s move constructor is noexcept, this is one move and one swap, both no-throw, and the only operation that could throw is the caller’s expression that produced the rvalue.

The lesson: even a one-line operation has nuances under exception safety, and the right answer is sensitive to the type being operated on. There are no universal rules, only patterns.

What to remember

  • The strong guarantee is achieved by separating throwing operations from mutating operations, and arranging for all throws to happen before any mutation.
  • Copy-and-swap, pimpl-and-swap, and two-phase commit are syntactic encodings of this idea, suited to different shapes of problem.
  • Scope guards generalize to “register an undo, dismiss on success.” They are necessary when you can’t avoid in-place mutation but still want commit-or-rollback semantics.
  • Persistent data structures sidestep the problem entirely by removing mutation.
  • The strong guarantee is not always achievable, especially for operations with external side effects. The next chapters look at the cases where it isn’t, and what to do instead.

Further reading

  • Andrei Alexandrescu, “Generic: Change the Way You Write Exception-Safe Code — Forever,” C/C++ Users Journal, December 2000. Introduces ScopeGuard.
  • Herb Sutter, Exceptional C++, Items 17–19, on the canonical assignment operator and copy-and-swap.
  • C++23 P0052, “Generic Scope Guard and RAII Wrapper for the Standard Library.”
  • Phil Bagwell, “Ideal Hash Trees,” Tech. Rep. EPFL, 2001 — the underlying paper for Clojure’s persistent maps. Worth reading for the mental model of immutable structural sharing, even if you never implement one.