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.

The [[nodiscard]] Factory Pattern

A scope guard constructed as a temporary is destroyed in the same expression that creates it — the cleanup fires immediately rather than at scope exit, making the guard completely useless:

cpp
scope_exit([&] { rollback(); });  // BAD: temporary, destructs at semicolon
auto guard = scope_exit([&] { rollback(); });  // GOOD: named, lives until scope ends

The compiler cannot always diagnose this silently failing pattern on its own. Marking the factory [[nodiscard]] turns the mistake into a warning (GCC, Clang, MSVC all honour it):

cpp
template<typename F>
[[nodiscard]] auto make_scope_exit(F&& f) {
    return ScopeExit<std::decay_t<F>>{std::forward<F>(f)};
}

With [[nodiscard]] in place, make_scope_exit([&]{ rollback(); }); produces a warning like "ignoring return value of function declared with [[nodiscard]] attribute". The attribute is cheap — it produces no runtime cost and no code generation change — so there is no reason to omit it from any factory that returns an owning object.

The standard types in <scope> apply [[nodiscard]] to the entire class via the class-level attribute, so even std::scope_exit{fn}; as a discarded statement triggers the diagnostic in C++26.

Move Semantics and Ownership Transfer

A scope guard must be move-constructible so it can be returned from a factory function (NRVO may or may not apply), but it must never be copyable. Copying would duplicate the cleanup obligation, causing the callable to fire twice — or at the wrong time if one copy is destroyed early.

The subtle bug with a defaulted move constructor is worth repeating in isolation:

cpp
// WRONG — `= default` move copies `bool`, so both objects have active_ = true
ScopeExit(ScopeExit&&) = default;

// CORRECT — explicitly disarm the moved-from object
ScopeExit(ScopeExit&& other)
    noexcept(std::is_nothrow_move_constructible<F>::value)
    : fn_{std::move(other.fn_)}, active_{other.active_}
{
    other.active_ = false;  // source must not fire
}

After the move, the source guard is in a "disarmed" state: its destructor still runs, but the if (active_) guard ensures it does nothing. This is the same design used by std::unique_ptr: the moved-from pointer is null and its destructor is a no-op.

Move assignment is intentionally deleted. Allowing it would require deciding what to do with the existing cleanup in the destination object — run it immediately? swap? disarm it? None of these choices is obviously correct in all situations, and the added complexity is rarely justified. If you need to reassign a guard, use release() on the old one and construct a new one.

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

std::experimental::scope_exit — The Standard Vocabulary

P0052 (Generic Scope Guard and RAII Wrapper for the Standard Library) has been part of the Library Fundamentals Technical Specification since TS v3 and is scheduled for C++26 via the <scope> header. GCC libstdc++ ships a conforming preview under <experimental/scope>, which means real code can use the standard types today on most Linux distributions:

cpp
#include <experimental/scope>   // libstdc++ preview (GCC 9+)
// #include <scope>             // C++26 final

namespace se = std::experimental;

void process_file(const char* path) {
    FILE* fp = fopen(path, "r");
    if (!fp) throw std::runtime_error{"cannot open file"};

    // Always runs — whether we return, throw, or fall off the end
    se::scope_exit  close_fp  {[fp] { fclose(fp); }};

    // Runs only if an exception propagates out of this scope
    se::scope_fail  log_error {[path] {
        fprintf(stderr, "failed while processing %s\n", path);
    }};

    // Runs only on clean (non-exception) exit
    se::scope_success audit   {[] { log_audit("file processed OK"); }};

    parse(fp);   // may throw
}

The three types share a common interface:

TypeFires when
scope_exitAlways, on any scope exit
scope_failOnly when std::uncaught_exceptions() increased since construction
scope_successOnly when std::uncaught_exceptions() did not increase

All three types have a release() member that disarms the guard without running the callable. All three are move-constructible (to allow factory-function returns) and non-copyable. The standard explicitly forbids move assignment for the same reasons described above.

std::make_scope_exit, std::make_scope_fail, and std::make_scope_success are the corresponding [[nodiscard]] factory functions in C++26, though CTAD makes them unnecessary in most cases.

Constructor and release() Semantics in the Standard Types

The standard types wrap the callable in an optional-like engaged state. The constructor is explicit and accepts any EF where the callable can be stored; it is also noexcept conditional on the callable's move/copy constructors. If the callable's constructor throws, no destructor call is ever scheduled — the object is never "active". This ensures exception safety during guard construction itself.

release() is noexcept and idempotent in the standard specification: calling it twice is harmless. It sets an internal execute_on_destruction flag to false (the exact field name is implementation-defined). The standard also mandates that the destructor of a moved-from scope guard is a no-op, which the standard achieves by setting the same flag in the move constructor.

cpp
// Idiomatic: construct, work, release on success — standard types honour this contract
std::scope_exit cleanup{[&] { temporary_file.remove(); }};
produce_output(temporary_file);
cleanup.release();   // file is now a permanent output — suppress removal

std::unique_resource — Generalized unique_ptr

P0052 also standardizes std::unique_resource, which handles the common case of a non-pointer resource handle where std::unique_ptr cannot be used:

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

// Integer file descriptor — unique_ptr doesn't work for int handles
auto fd = std::make_unique_resource_checked(
    ::open(path, O_RDONLY),
    -1,                            // sentinel "invalid" value
    [](int d) { ::close(d); }
);

if (!fd) throw std::system_error{errno, std::system_category()};
// fd closes automatically whether we throw or return

make_unique_resource_checked skips invoking the deleter if the handle equals the sentinel value, preventing a double-close when the acquisition itself fails. This is the same semantic as std::unique_ptr's null check before deletion.

The defer Analogy and LIFO Ordering

Go's defer statement is the closest mainstream-language analogue to a C++ scope guard. Both bind a callable to scope exit; both accumulate in LIFO (last in, first out) order within a scope. The ordering matters when cleanup operations have dependencies:

cpp
// Correct ordering — guard_b fires FIRST (declared last), guard_a fires SECOND
auto conn   = db.open();
auto guard_a = scope_exit([&] { db.close(conn); });      // fires 2nd

auto tx     = conn.begin_transaction();
auto guard_b = scope_exit([&] { tx.rollback_if_open(); }); // fires 1st

// tx is rolled back before conn is closed — correct dependency order

Declare guards from "outermost" resource to "innermost" resource: the last-declared guard fires first at scope exit, which mirrors the acquisition order in reverse. This is identical to how ordinary destructors run in reverse construction order for class members, and it mirrors the LIFO discipline of a stack, making dependency management predictable.

Contrast with a naive approach that registers all guards upfront in a vector of std::function:

cpp
// Anti-pattern: deferred registration in a container loses LIFO per-variable ordering
// and requires manual management. Use named guards instead.
std::vector<std::function<void()>> deferred;
deferred.push_back([&] { db.close(conn); });      // pushed first = runs last if we reverse
deferred.push_back([&] { tx.rollback(); });
// Must remember to reverse-iterate on cleanup...
for (auto it = deferred.rbegin(); it != deferred.rend(); ++it) (*it)();

Named scope guards eliminate the book-keeping: the language guarantees the order.

Key differences from Go's defer:

  • Go evaluates deferred function arguments eagerly at the defer statement; C++ lambdas capture by value or reference, giving you explicit control over when state is snapshotted.
  • Go's defer runs after return sets the named return value, allowing modification of the return value. C++ destructors cannot modify the return value.
  • C++ scope guards can be disarmed via release(); Go has no built-in cancellation mechanism for defer.
  • Go's defer has a measurable runtime cost per call in older toolchains; C++ scope guards with inlined lambdas typically compile to zero overhead versus equivalent hand-written cleanup code.

Two-Phase Commit Pattern

The most idiomatic use of scope guards in systems code is the two-phase commit: unconditionally register a rollback, then call release() at the exact point where success is confirmed. This eliminates all control-flow analysis about whether cleanup is needed:

cpp
class CatalogUpdater {
    Database& db_;
    std::vector<ItemId> staged_;

public:
    void commit() {
        db_.begin();

        // Phase 1: register rollback unconditionally
        auto rollback = scope_exit([this] {
            db_.rollback();
            staged_.clear();
        });

        // Phase 2: do the work — any exception triggers rollback
        for (const auto& id : staged_)
            db_.upsert(id, catalog_[id]);

        db_.prepare_commit();   // two-phase: may throw
        // Still inside the guard's scope — rollback still armed

        rollback.release();     // all checks passed, disarm rollback
        db_.commit();           // final phase — should be near-infallible
    }
};

The pattern scales to any number of intermediate steps. Each step that can fail leaves the rollback armed; release() is called only at the single point where all fallible operations are complete. Compare this to nested try/catch blocks that must each manually undo prior steps — a classic maintenance hazard as the number of steps grows.

Restore-Previous-Value Pattern

A recurring variant saves a value, modifies it, then restores it on exit regardless of how the function leaves:

cpp
template<typename T>
[[nodiscard]] auto save_and_restore(T& target) {
    T saved = target;
    return scope_exit([&target, saved = std::move(saved)]() mutable {
        target = std::move(saved);
    });
}

void with_locale(const std::locale& loc, auto fn) {
    auto restore = save_and_restore(std::locale::global(loc));
    // The existing global locale is saved; fn() runs under loc
    fn();
    // Original locale is restored — even if fn() throws
}

This generalizes to any global or thread-local state: signal masks, errno, floating-point rounding modes, numeric locale facets, OpenGL state machine flags. Named RAII types are better when the resource appears frequently, but the save_and_restore factory covers one-off uses without writing a dedicated class.

The mutable on the lambda is required because std::move(saved) modifies the captured saved variable; lambda captures are const by default and mutable lifts that restriction for the lambda body.

When combined with C++26's std::scope_exit, this pattern requires no custom implementation at all: the standard guard handles all edge cases including exception-during-construction and double-release.

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