Background

Scope Guard is a concept reminiscent of the RAII (Resource Acquisition Is Initialization) principle in C++. The idea is to manage resources (like memory, files, network sockets, etc.) using object lifetime. When the object goes out of scope, its destructor ensures that the resource is cleaned up properly. The scope guard is intended to run a given callable (like a function or lambda) when it is destroyed.

RAII (Resource Acquisition Is Initialization) is a programming idiom used in C++ where the lifetime of an object is bound to the lifetime of its scope (typically represented by a block of code wrapped in curly braces {}).

Here’s a breakdown of RAII:

  • Resource Acquisition: When an object is created, it acquires a specific resource.
  • Initialization: The resource acquisition is done during the object’s construction (i.e., when it’s initialized).

RAII ensures the following:

  1. Resources are acquired in a deterministic manner (during object creation).
  2. Resources are released in a deterministic manner (during object destruction).
  3. Exception safety, as resources are automatically cleaned up even if an exception is thrown.

Example

A simple example of RAII is the use of std::unique_ptr to manage dynamically allocated memory:

void exampleFunction() {
    std::unique_ptr<int> p(new int(5));  // Resource (memory) is acquired and "owned" by p.

    // Do some operations with p...

}  // p goes out of scope and its destructor is called, which deletes the memory. No memory leak!

This RAII behavior is contrasted with manual memory management where you’d have to remember to call delete:

void nonRAIIExample() {
    int* p = new int(5);  // Memory is acquired.

    // Do some operations...

    delete p;  // You have to manually release the memory. Risky!
}

Implementation of a Scope Guard

Requirements

Three requirements are listed in the following code block for implementing the scope guard.

#include <cstdio>
#include <cassert>

#include <stdexcept>
#include <iostream>
#include <functional>

int main() {
    {
        // Requirement 0: Support lambda
        FILE * fp = nullptr;
        try{
            fp = fopen("test.txt","a");
            auto guard = scope_guard([&] {
                fclose(fp);
                fp = nullptr;
            });

            throw std::runtime_error{"Test"};
        } catch(std::exception & e){
            puts(e.what());
        }
        assert(fp == nullptr);
    }
    puts("----------");
    {
        // Requirement 1: Support function object invocation
        // & binding arguments to the callable.
        struct Test {
            void operator()(X* x) {
                delete x;
            }
        } t;
        auto x = new X{};
        auto guard = scope_guard(t, x);

    }
    puts("----------");
    {
        // Requirement 2: Support member functions and std::ref.
        auto x = new X{};
        {
            struct Test {
                void f(X*& px) {
                    delete px;
                    px = nullptr;
                }
            } t;
            auto guard = scope_guard{&Test::f, &t, std::ref(x)};
        }
        assert(x == nullptr);
    }

Solutions

Naive

To meet the basic requirement, all you need to do is keep the lambda stored within a std::function:

// naive solution
class scope_guard {
public:
    explicit scope_guard(std::function<void()> onExitScope) : onExitScope_(onExitScope) {}
    
    ~scope_guard() {
        on_exit_scope();
    }

private:
    std::function<void()> on_exit_scope;
};

Conventional

However, for requirements 2 and 3, things get trickier. We need to deal with more complex situations like binding arguments and passing by reference. As a result, we’re stepping up our game with an upgraded solution:

// conventional solution
class scope_guard {
public:
    template <typename Callable, typename... Args>
    scope_guard(Callable&& func, Args&&... args) {
        on_exit_scope = std::bind(std::forward<Callable>(func), std::forward<Args>(args)...);
    }
    
    ~scope_guard() {
        on_exit_scope();
    }

private:
    std::function<void()> on_exit_scope;
};

Fancy

However, this simple solution isn’t cool anymore. The use of std::bind dates back to the old c++11 days, but we’re now in the modern world of c++23. Let’s modernize (and over-complicate) the code:

// fancy solution
class scope_guard {
public:
    template<typename Callable, typename... Args>
    requires std::invocable<Callable, std::unwrap_reference_t<Args>...>
    scope_guard(Func&& func, Args&&...args) :f{ [func = std::forward<Func>(func), ...args = std::forward<Args>(args)]() mutable {
            std::invoke(std::forward<std::decay_t<Func>>(func), std::unwrap_reference_t<Args>(std::forward<Args>(args))...);
        } }{}

    ~scope_guard() {
            on_exit_scope();
    }
    // Prevent copying, but allow moves.
    scope_guard(const scope_guard&) = delete;
    scope_guard& operator=(const scope_guard&) = delete;
    scope_guard(scope_guard&&) = default;
    scope_guard& operator=(scope_guard&&) = default;

private:
    std::function<void()> on_exit_scope;

};