How to Use absl::Cleanup for RAII-Style Scope Exit Callbacks in Abseil C++

absl::Cleanup implements the scope guard idiom by storing a callable that automatically executes when the object goes out of scope, providing deterministic cleanup without heap allocation.

The absl::Cleanup class in the abseil/abseil-cpp repository provides a lightweight, zero-overhead mechanism for ensuring resources are released when execution leaves a scope. Unlike std::unique_ptr with custom deleters, absl::Cleanup accepts any callable—lambdas, functors, or function pointers—making it ideal for complex teardown logic that doesn't fit a simple pointer model. This utility is header-only and lives in the absl/cleanup/ module.

Creating a Cleanup Object

The public API resides in [absl/cleanup/cleanup.h](https://github.com/abseil/abseil-cpp/blob/master/absl/cleanup/cleanup.h). You can instantiate absl::Cleanup using either C++17 class template argument deduction (CTAD) or the absl::MakeCleanup factory function for C++11 compatibility.

C++17 Class Template Argument Deduction

With C++17, the compiler automatically deduces the callback type, enabling concise syntax:

#include "absl/cleanup/cleanup.h"

void ProcessFile(const char* path) {
  FILE* file = fopen(path, "r");
  if (!file) return;
  
  absl::Cleanup file_closer = [file] { fclose(file); };
  
  // Process data...
  // fclose called automatically when file_closer destructs
}

C++11 absl::MakeCleanup Factory

For pre-C++17 compilers, use the absl::MakeCleanup helper function to avoid explicit template arguments:

auto file_closer = absl::MakeCleanup([file] { fclose(file); });

Both approaches create a move-only object that holds your callable in internal storage.

How absl::Cleanup Works Under the Hood

The implementation leverages placement-new and manual lifecycle management to avoid heap allocation while supporting callables of arbitrary size.

Internal Storage Architecture

The internal implementation in [absl/cleanup/internal/cleanup.h](https://github.com/abseil/abseil-cpp/blob/master/absl/cleanup/internal/cleanup.h) uses cleanup_internal::Storage<Callback> to manage the callback lifetime. This storage class:

  • Allocates space using a raw buffer (callback_buffer_) sized for the callable type
  • Constructs the callback via placement-new to avoid dynamic allocation
  • Tracks engagement state via is_callback_engaged_

When you create a Cleanup object, the callable is placement-new'd into the stack-allocated buffer within the Storage object.

Engagement Tracking and Execution Guarantees

The destructor of Cleanup checks IsCallbackEngaged(). If engaged, it invokes InvokeCallback() followed by DestroyCallback(). This ensures:

  • Exactly-once execution: The callback runs either on scope exit, explicit Invoke(), or not at all if Cancel() is called
  • No heap allocation: The storage lives within the Cleanup object itself
  • Signal-safety: No locks or dynamic allocation occurs during destruction (assuming your callback is also signal-safe)

Controlling Cleanup Execution

While automatic execution on scope exit is the default behavior, absl::Cleanup provides explicit control for unusual control flows.

Early Invocation with Invoke()

Call Invoke() to execute the callback immediately and disengage the guard:

absl::Cleanup guard = [] { Log("Cleanup executed"); };

// Do work...

std::move(guard).Invoke();  // Calls Log now; destructor becomes no-op

Note that Invoke() requires moving from the cleanup object because Cleanup is move-only (non-copyable).

Canceling Execution with Cancel()

Prevent the callback from running by calling Cancel():

absl::Cleanup guard = [] { ReleaseResource(); };

if (resource_already_released) {
  std::move(guard).Cancel();  // Disengages the callback
}
// No action taken when guard destructs

Both Invoke() and Cancel() move-from the object to enforce single ownership semantics.

Practical Examples

Resource Management with Early Returns

The canonical use case handles multiple resources with complex exit paths:

absl::Status CopyGoodData(const char* source_path, const char* sink_path) {
  FILE* source_file = fopen(source_path, "r");
  if (!source_file) return absl::NotFoundError("No source file");

  absl::Cleanup source_closer = [source_file] { fclose(source_file); };

  FILE* sink_file = fopen(sink_path, "w");
  if (!sink_file) return absl::NotFoundError("No sink file");

  auto sink_closer = absl::MakeCleanup([sink_file] { fclose(sink_file); });

  while (ReadData(source_file, &data)) {
    if (!data.IsGood()) {
      return absl::FailedPreconditionError("Read bad data");  // Both cleanups run
    }
    SaveData(sink_file, &data);
  }

  return absl::OkStatus();  // Both cleanups run
}

Non-Copyable Callables

absl::Cleanup supports move-only functors, making it compatible with unique resources:

struct NonCopyable {
  NonCopyable() = default;
  NonCopyable(const NonCopyable&) = delete;
  NonCopyable(NonCopyable&&) = default;
  void operator()() const { /* cleanup code */ }
};

absl::Cleanup cleanup = NonCopyable{};  // Works fine

Summary

Frequently Asked Questions

Is absl::Cleanup thread-safe?

absl::Cleanup is not thread-safe. The class provides no internal synchronization. If you need to share cleanup logic across threads, you must provide your own locking or ensure the Cleanup object is accessed by only one thread. However, the class is signal-safe because it contains no locks and only performs placement-new and destruction operations.

What is the difference between absl::Cleanup and std::unique_ptr with a custom deleter?

std::unique_ptr manages a single pointer resource, while absl::Cleanup manages arbitrary callable logic. Use absl::Cleanup when you need to execute complex teardown code—such as logging, state restoration, or multiple resource releases—that doesn't fit the pointer ownership model. Additionally, absl::Cleanup provides explicit Cancel() and Invoke() methods for manual control.

Can I use absl::Cleanup in C++11 code?

Yes. While C++17 allows direct initialization via class template argument deduction, C++11 users can create cleanup objects using the absl::MakeCleanup factory function. This function deduces the type automatically and returns a properly instantiated Cleanup object compatible with older standards.

Does absl::Cleanup allocate memory on the heap?

No. The implementation stores the callable in a stack-allocated buffer using placement-new. The cleanup_internal::Storage class in absl/cleanup/internal/cleanup.h manages a raw byte buffer sized for the specific callable type, ensuring zero heap allocation overhead and predictable performance.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →