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++11A 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.
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:
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():
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:
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):
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:
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:
#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++26std::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:
auto cleanup = scope_exit([&] { rollback(); });
perform_work(); // may throw or return early
cleanup.release(); // reached only on successPrefer 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:
auto guard_a = scope_exit([&] { free_a(); }); // fires second
auto guard_b = scope_exit([&] { free_b(a); }); // fires first — b uses aUsing 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