Skip to content

Instantly share code, notes, and snippets.

@keebus
Last active August 13, 2020 07:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keebus/6fbac401be99e5473e974f9d7e26817f to your computer and use it in GitHub Desktop.
Save keebus/6fbac401be99e5473e974f9d7e26817f to your computer and use it in GitHub Desktop.
Finally

Finally

One of C++'s strengths is automatic code invocation at scope-exit (aka RAII). The traditional idea behind RAII of having specialized types for every resource type with a custom destructor that frees it is good in theory but in practice comes with a lot of friction and thus boiler-plate and frustration for the developer, that now has to create a new type for every little rollback operation he wants to perform. The problem is exacerbated with C++11 move operators: making a small "smart" class that does some form of cleanup now requires even more boilerplate.

Fortunately there's a way to neatly work around this, and the idea comes from D's scope keyword or Go's defer. We want a mechanism that tells the compiler to invoke a snippet of inlined code at scope exit. Something like

// Acquire a resource
Resource* resource = AcquireResource();

// Then whatever happens, release the resource when it goes out of scope.
finally {
  ReleaseResource(resource);
};

// Safely do operations with the resource and early out if some error occurs. No need to worry about its cleanup.

More commonly, we stumble in situations where we want to write transaction like, i.e. perform a series of operations with side-effects and roll them all back in reverse order of execution if something goes wrong. Something like the following

// Returns 0 if succesful, a negative number if an error occurred.
int CalibrateUniverse()
{
    // Holds the final result of the operation. Initialize it to failure.
    int result = -1;

    // Acquire a resource
    Resource* resource = AcquireResource();

    // If result evaluates to "true" at scope exit (i.e. an error occurred), release the resource.
    rollback(result) {
      ReleaseResource(resource);
    };

    // Now run some operation that may fail.
    if (DoOperationWith(resource) < 0)
        // It failed. Just return result, no need to manually figure out what to rollback
        return result;

    // ... other operations ...

    // All went fine! Just set 0 zero to result and return it. This will disable all rollbacks in this scope.
    return (result = 0);
}

The really neat thing about rollback is that it code using it guarantees that you can return/break/goto/throw from any point in code and all the resources that need to be released, will be released appropriately. It alleviates the programmer from quite some mental burden and makes code much less error prone.

Implementation

It happens that the previous constructs finally and rollback can be easily implemented exactly as they appear, using a combination of macro, lambda and operator overload.

Implementation of finally

#define finally\
    auto __paste2(_finally_, __COUNTER__) = detail::make_finally & [&]

namespace detail
{
    enum make_finally_t { make_finally };

    template <typename Function>
    struct finally_helper
    {
        Function m_function;

        ~finally_helper()
        {
            m_function();
        }
    };

    template <typename Function>
    finally_helper<Function> operator&(make_finally_t, Function function)
    {
        return{ std::forward<T>(function) };
    }
}

Where _paste2 is somewhere defined to

#define _paste1(a, b) a##b
#define _paste2(a, b) _paste1(a, b)

And here is the rollback implementation

#define rollback(result)\
    auto _paste2(_rollback_, __COUNTER__) = detail::make_rollback<decltype(result)>{ result } & [&]

namespace detail
{
    template <typename Flag>
    struct make_rollback
    {
        Flag& flag;
    };

    template <typename Flag, typename Function>
    struct rollback_helper
    {
        Flag& flag;
        Function function;

        ~rollback_helper()
        {
            if (flag)
                function();
        }
    };

    template <typename Flag, typename Function>
    rollback_helper<Flag, Function> operator&(make_rollback<Flag> make, Function function)
    {
        return{ make.flag, std::forward<Function>(function) };
    }
}

Notes

Both GCC and Clang inline the lambda at each scope exit point with at least -O1 optimization level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment