How to Use absl::Cleanup for RAII-Style Scope-Exit Callbacks in Abseil C++
absl::Cleanup stores a callable that executes automatically when the object goes out of scope, providing deterministic resource cleanup without heap allocation.
The absl::Cleanup class in the Abseil C++ library implements the classic scope guard (RAII) idiom for managing resource lifetimes. Defined in absl/cleanup/cleanup.h, this utility captures lambdas or functors that run during destruction, ensuring cleanup code executes exactly once—whether during normal scope exit, early returns, or exception unwinding. Unlike manual cleanup patterns, absl::Cleanup eliminates boilerplate while guaranteeing zero dynamic memory allocation through placement-new storage.
Basic Usage: C++17 Deduction vs. C++11 Factory
You can create a cleanup object using class template argument deduction (CTAD) in C++17 or the absl::MakeCleanup factory function for C++11 compatibility.
#include "absl/cleanup/cleanup.h"
absl::Status ProcessFile(const char* path) {
FILE* file = fopen(path, "r");
if (!file) return absl::NotFoundError("File not found");
// C++17: Automatic deduction of callback type
absl::Cleanup file_closer = [file] { fclose(file); };
// C++11: Explicit factory function
auto buffer_guard = absl::MakeCleanup([&] { ClearBuffer(); });
// Process data...
if (error_condition) {
return absl::InternalError("Processing failed"); // file_closer runs automatically
}
return absl::OkStatus(); // Both cleanups execute here
}
The Cleanup constructor in absl/cleanup/cleanup.h uses a cleanup_internal::Tag template parameter to forbid explicit template arguments, enforcing usage solely through deduction or MakeCleanup.
Internal Architecture and Zero-Allocation Storage
The implementation relies on cleanup_internal::Storage<Callback> defined in absl/cleanup/internal/cleanup.h. This storage class performs placement-new construction of the callable inside a raw byte buffer (callback_buffer_), avoiding heap allocation while enabling eager destruction.
Key implementation details include:
- Engagement tracking: A boolean flag
is_callback_engaged_tracks whether the callback is still active and should be invoked during destruction. - Manual lifecycle management: The destructor checks
IsCallbackEngaged(); if true, it callsInvokeCallback()followed byDestroyCallback()to ensure the cleanup runs exactly once. - Signal safety: The class contains no locks and only performs placement-new and destructor calls, making it safe for use in signal handlers (provided the user’s callback is also signal-safe).
Explicit Control: Early Invocation and Cancellation
absl::Cleanup provides move-only semantics with methods to control execution timing. Both Invoke() and Cancel() require std::move because they consume the cleanup object's state.
Early Invocation with Invoke()
Call std::move(cleanup).Invoke() to execute the callback immediately and disarm the destructor:
absl::Cleanup transaction_guard = [] { RollbackTransaction(); };
// Commit successful—rollback not needed
CommitTransaction();
std::move(transaction_guard).Invoke(); // Calls Rollback now (or use Cancel for no-op)
Cancellation with Cancel()
Use std::move(cleanup).Cancel() to suppress callback execution entirely:
absl::Cleanup guard = [] { ReleaseResource(); };
if (resource_already_released) {
std::move(guard).Cancel(); // Prevent ReleaseResource from running
}
// Guard destroyed safely with no side effects
Working with Move-Only and Non-Copyable Callables
absl::Cleanup is move-constructible (Cleanup(Cleanup&&) = default) but not copyable, ensuring unique ownership of the cleanup action. This design supports non-copyable callable objects:
struct NonCopyableDeleter {
NonCopyableDeleter() = default;
NonCopyableDeleter(const NonCopyableDeleter&) = delete;
NonCopyableDeleter(NonCopyableDeleter&&) = default;
void operator()() const { /* cleanup code */ }
};
absl::Cleanup cleanup = NonCopyableDeleter{}; // Compiles: move-only semantics preserved
Summary
absl::Cleanupprovides deterministic scope-exit callbacks through RAII, implemented inabsl/cleanup/cleanup.h.- Zero allocation: Callbacks stored via placement-new in
callback_buffer_withinabsl/cleanup/internal/cleanup.h. - Explicit control:
std::move(c).Invoke()runs callbacks early;std::move(c).Cancel()prevents execution. - Move-only: Cannot be copied, ensuring unique ownership and supporting non-copyable callables.
- Signal-safe: Lock-free implementation suitable for signal handlers when callbacks are signal-safe.
Frequently Asked Questions
What is the difference between absl::Cleanup and std::unique_ptr with a custom deleter?
absl::Cleanup is designed for arbitrary scope-exit actions, not just resource destruction. While std::unique_ptr manages object ownership through a deleter, absl::Cleanup accepts any callable (including captures) and provides explicit Cancel() and Invoke() methods. Additionally, absl::Cleanup guarantees stack-only storage through placement-new, whereas std::unique_ptr typically manages heap-allocated objects.
Can I use absl::Cleanup in signal handlers?
Yes, provided your callback is signal-safe. The absl::Cleanup implementation contains no locks, atomic operations, or dynamic allocation—only placement-new and destructor calls. According to the Abseil source code, this makes the class itself safe for signal handlers, though you must ensure the callable you store (e.g., fclose or custom logging) is also async-signal-safe.
Why do Invoke() and Cancel() require std::move?
These methods consume the object's engagement state. absl::Cleanup implements move-only semantics to ensure unique ownership of the cleanup action. Calling std::move(cleanup).Invoke() transfers ownership to the method, explicitly disarming the destructor to prevent double-execution. This design enforces at compile-time that the cleanup object cannot be used after manual invocation or cancellation.
How do I store absl::Cleanup in a container or class member?
You cannot copy absl::Cleanup, so you must use move semantics. Store it in a std::unique_ptr<absl::Cleanup<T>> if you need optional cleanup, or declare it as a direct member with std::optional<absl::Cleanup<T>> for deferred initialization. For containers, use std::vector<std::unique_ptr<absl::Cleanup<T>>> or wrap the cleanup in a move-only custom type.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →