Skip to content
C++
Idiom
since C++11
Basic

Scope Guard

Execute cleanup code at scope exit regardless of how the scope is left — generalized RAII without a dedicated wrapper class.

Scope Guardsince C++11

A scope guard binds a callable to a stack object so it executes automatically when that object's destructor runs — whether the scope exits normally, via early return, or during stack unwinding.

Overview

RAII couples resource lifetime to object lifetime, but writing a dedicated wrapper class for every one-off cleanup operation is often excessive. A scope guard generalizes the pattern: wrap any callable in a stack object and it fires when the object is destroyed, unconditionally.

Three variants cover real-world needs:

  • scope_exit — always fires on scope exit
  • scope_fail — fires only when exiting via exception (rollback semantics)
  • scope_success — fires only on normal exit (commit, audit log)

The success/failure distinction requires std::uncaught_exceptions() (C++17, plural, returns int). The older std::uncaught_exception() (C++98, deprecated in C++17, returns bool) cannot distinguish nested exception contexts and must not be used for this purpose.

Implementation

A correct ScopeExit must be move-only and must explicitly zero out the source on move. The critical mistake in most naive implementations is using = default for the move constructor: since bool move is a copy, both the moved-from and moved-to objects retain active_ = true and both fire the cleanup.

cpp
template<typename F>
class ScopeExit {
    F fn_;
    bool active_;

public:
    explicit ScopeExit(F f)
        noexcept(std::is_nothrow_move_constructible<F>::value)  // C++11
        : fn_{std::move(f)}, active_{true} {}

    // Must disable source — `= default` leaves other.active_ = true
    ScopeExit(ScopeExit&& other)
        noexcept(std::is_nothrow_move_constructible<F>::value)
        : fn_{std::move(other.fn_)}, active_{other.active_} {
        other.active_ = false;
    }

    ScopeExit(const ScopeExit&)            = delete;
    ScopeExit& operator=(const ScopeExit&) = delete;
    ScopeExit& operator=(ScopeExit&&)      = delete;

    ~ScopeExit() { if (active_) fn_(); }

    void release() noexcept { active_ = false; }
};

template<typename F>
auto scope_exit(F&& f) {
    return ScopeExit<std::decay_t<F>>{std::forward<F>(f)};  // std::decay_t: C++14
}

The destructor does not suppress exceptions. If fn_() throws while the guard fires during stack unwinding, the runtime calls std::terminate. Design cleanup callables to be noexcept.

Examples

C API handles — always close, regardless of exit path:

cpp
void transfer_file(const char* src, const char* dst) {
    FILE* in = fopen(src, "rb");
    if (!in) throw std::runtime_error{std::string{"cannot open "} + src};
    auto close_in = scope_exit([in] { fclose(in); });   // captures by value

    FILE* out = fopen(dst, "wb");
    if (!out) throw std::runtime_error{std::string{"cannot create "} + dst};
    auto close_out = scope_exit([out] { fclose(out); });

    // copy data — any throw closes both handles
}

Transactional rollback with release():

cpp
void move_funds(Ledger& ledger, AccountId from, AccountId to, Cents amount) {
    ledger.debit(from, amount);
    auto undo = scope_exit([&] { ledger.credit(from, amount); });

    ledger.credit(to, amount);  // may throw
    undo.release();             // success path — cancel the rollback
}

The guard is constructed unconditionally. The rollback fires on any exception; release() suppresses it on success. This is cleaner than branching to decide whether to invoke cleanup — branching misses exceptions entirely.

Global state restoration:

cpp
void render_wireframe(Renderer& r, auto draw_fn) {  // abbreviated template: C++20
    auto prev = r.fill_mode();
    r.set_fill_mode(FillMode::Wireframe);
    auto restore = scope_exit([&] { r.set_fill_mode(prev); });
    draw_fn();
    // fill mode restored even if draw_fn throws
}

Success and Failure Guards

Both variants record the exception count at construction and compare at destruction (C++17 required):

cpp
template<typename F>
class ScopeFailure {
    F fn_;
    int exceptions_{std::uncaught_exceptions()};  // C++17

public:
    explicit ScopeFailure(F f) : fn_{std::move(f)} {}
    ScopeFailure(const ScopeFailure&) = delete;
    ScopeFailure& operator=(const ScopeFailure&) = delete;

    ~ScopeFailure() {
        if (std::uncaught_exceptions() > exceptions_) fn_();
    }
};

template<typename F>
class ScopeSuccess {
    F fn_;
    int exceptions_{std::uncaught_exceptions()};  // C++17

public:
    explicit ScopeSuccess(F f) : fn_{std::move(f)} {}
    ScopeSuccess(const ScopeSuccess&) = delete;
    ScopeSuccess& operator=(const ScopeSuccess&) = delete;

    ~ScopeSuccess() {
        if (std::uncaught_exceptions() == exceptions_) fn_();
    }
};

Using both together for explicit commit/rollback:

cpp
void execute_batch(Database& db, const std::vector<Query>& queries) {
    db.begin();
    auto on_fail    = ScopeFailure{[&] { db.rollback(); }};  // CTAD: C++17
    auto on_success = ScopeSuccess{[&] { db.commit();   }};

    for (const auto& q : queries)
        db.execute(q);
    // exception → rollback(); normal exit → commit()
}

Class template argument deduction (ScopeFailure{...}) requires C++17. In C++11/14, use factory functions instead.

C++26 Standard Library

The scope guard idiom is standardized in C++26 in <scope> (P0052). The standard provides std::scope_exit, std::scope_fail, std::scope_success, and std::unique_resource:

cpp
#include <scope>  // C++26

void with_transaction(Database& db, auto work) {
    db.begin();
    std::scope_fail   on_fail  {[&] { db.rollback(); }};
    std::scope_success on_commit{[&] { db.commit();   }};
    work();
}

// unique_resource: unique_ptr generalized to non-pointer handles
auto sock = std::make_unique_resource(
    open_socket(host, port),
    [](SocketHandle h) { close_socket(h); }
);
// C++26

std::unique_resource fills the gap between std::unique_ptr (pointer-only) and a full RAII class. It handles integer file descriptors, opaque handles, and any type where the "null" value is not zero.

GCC's libstdc++ ships a preview under <experimental/scope> (std::experimental::scope_exit etc.) for pre-C++26 environments. The interface mirrors the final standard.

Best Practices

Construct the guard immediately after acquiring the resource. Any code between acquisition and guard construction is a leak window if an exception is thrown.

Use release() to express "cleanup is no longer needed," not to conditionally skip construction. The unconditional guard + conditional release pattern covers both the exception and the early-return case:

cpp
auto cleanup = scope_exit([&] { rollback(); });
perform_work();       // may throw or return early
cleanup.release();    // reached only on success

Prefer named RAII types when they exist. std::unique_ptr for heap objects, std::unique_lock for mutexes, std::fstream for files — these carry semantic meaning and have richer interfaces. Scope guards are for handles that lack standard RAII wrappers and for one-off operations that don't justify a class.

Capture handles by value, state by reference. A guard capturing a raw pointer or integer handle by value is always correct; one capturing by reference and accessing freed memory during cleanup is not.

Common Pitfalls

= default move constructor. Because bool move is a copy, the moved-from object retains active_ = true. Both objects run the cleanup. Always write the move constructor explicitly and set other.active_ = false.

Throwing cleanup callables. If fn_() throws during destructor execution triggered by stack unwinding, std::terminate is called immediately. Wrap risky cleanup in try/catch inside the callable, or mark the callable noexcept.

Destructor ordering is LIFO. Guards in the same scope fire in reverse declaration order. If guard_b depends on state that guard_a releases, guard_a must be declared after guard_b:

cpp
auto guard_a = scope_exit([&] { free_a(); });  // fires second
auto guard_b = scope_exit([&] { free_b(a); }); // fires first — b uses a

Using std::uncaught_exception() (singular, C++98). It returns bool and is meaningless when an exception is already in flight from a different scope. Always use std::uncaught_exceptions() (plural, C++17) for success/failure guards.

Macro-based defer. A #define DEFER(code) macro using __LINE__ for uniqueness is a common pattern but creates hidden captures via [&], fires at block scope exit (not necessarily where the macro appears semantically), and cannot express release(). Prefer named guards.

See Also

  • RAII — the foundational pattern scope guards generalize
  • std::unique_ptr — preferred for single-ownership heap resources (C++11)
  • std::unique_lock — mutex ownership with deferred locking and early release (C++11)
  • Exceptions — how destructors interact with stack unwinding