C++ List of ScopeGuard

Recently the developer of LibrePCB asked me about a C++ pattern to undo parts of an action if an exception gets thrown in the middle of it.

Of course he basically described the main usage of a ScopeGuard. I went ahead and implemented one based on a talk by Andrei Alexandrescu.

Basic use of a ScopeGuard

The ScopeGuard allows to write transactional code that will undo previous parts if later code throws an exception.

myVector.push_back(item);
auto guard = scopeGuard([&]() { myVector.pop_back() });

// Do stuff that may throw
database.add(item);

// everything worked, so don't undo
guard.dismiss();

So if database.add(item) throws, the item in myVector will be removed. For more about the motivation behind ScopeGuard see the talk above or this drdobbs article.

Using C++11 and later the implementation is fairly simple:

struct ScopeGuardBase {
    ScopeGuardBase():
        mActive(true)
    { }

    ScopeGuardBase(ScopeGuardBase&& rhs):
        mActive(rhs.mActive)
    { rhs.dismiss(); }

    void dismiss() noexcept
    { mActive = false; }

protected:
    ~ScopeGuardBase() = default;
    bool mActive;
};

template<class Fun>
struct ScopeGuard: public ScopeGuardBase {
    ScopeGuard() = delete;
    ScopeGuard(const ScopeGuard&) = delete;

    ScopeGuard(Fun f) noexcept:
        ScopeGuardBase(),
        mF(std::move(f))
        { }

    ScopeGuard(ScopeGuard&& rhs) noexcept :
        ScopeGuardBase(std::move(rhs)),
        mF(std::move(rhs.mF))
        { }

    ~ScopeGuard() noexcept {
        if (mActive) {
            try { mF(); } catch(...) {}
        }
    }

    ScopeGuard& operator=(const ScopeGuard&) = delete;

private:
    Fun mF;
};

template<class Fun> ScopeGuard<Fun> scopeGuard(Fun f) {
    return ScopeGuard<Fun>(std::move(f));
}

Why a ScopeGuardList?

If you have a transaction that consists of a lot of steps that may throw and need to be undone it leads to code like the following:

doThing1();
auto guard = makeGuard([&]() { undoThing1(); });
doThing2();
auto guard = makeGuard([&]() { undoThing2(); });
doThing3();
auto guard = makeGuard([&]() { undoThing3(); });

// Do stuff that may trow

guard1.dismiss();
guard2.dismiss();
guard3.dismiss();

Things get even worse when doing something in a loop:

for (int i=0; i<10; ++i) {
    doStuff(i);
    // how do we create a guard for every operation?
}

To avoid that repetition and the potential error of a missing call to dismiss() we came up with a ScopeGuardList.

Implementation with std::function<>

A simple implementation just contains a list of std::function<>:

#include "scopeguard.h"
#include <vector>
#include <utility>
#include <functional>

struct ScopeGuardList : public ScopeGuardBase {
    ScopeGuardList() = default;

    ScopeGuardList(ScopeGuardList&& rhs):
        ScopeGuardBase(std::move(rhs)),
        mScopeGuards(std::move(rhs.mScopeGuards))
    { }

    ~ScopeGuardList() {
        if (mActive) {
            for (auto& scopeGuard : mScopeGuards) {
                scopeGuard();
            }
        }
    }

    template<class Fun> void add(Fun f)
    { mScopeGuards.emplace_back(std::move(f)); }

private:
    std::vector<std::function<void()>> mScopeGuards;
};

This implementation uses the type erasure of std::function to store several undo functions. While being fairly simple it performs worse compared to the above ScopeGuard implementation.

A quick benchmark showed that the ScopeGuard performs ~14 times faster than ScopeGuardList:

#include <chrono>
#include <iostream>

#include "scopeguardlist.h"

void testScopeGuard() {
    std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
    // make volatile to avoid optimizations
    volatile int setByGuard0 = 0;
    start = std::chrono::high_resolution_clock::now();
    for (int n=0; n<10000000; ++n) {
        auto guard0 = scopeGuard([&]{ setByGuard0 += 1; });
    }
    end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "Needed " << elapsed_seconds.count() << "s for " << setByGuard0 << " loops\n";
}

void testScopeGuardList() {
    std::chrono::time_point<std::chrono::high_resolution_clock> start, end;
    // make volatile to avoid optimizations
    volatile int setByGuard0 = 0;
    start = std::chrono::high_resolution_clock::now();
    for (int n=0; n<10000000; ++n) {
        auto guardList = ScopeGuardList();
        guardList.add([&]{ setByGuard0 += 1; });
    }
    end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "Needed " << elapsed_seconds.count() << "s for " << setByGuard0 << " loops\n";
}

int main() {
    testScopeGuard();
    testScopeGuardList();
}
Benchmark with different optimization levels
-O$N Time[s] ScopeGuard Time[s] ScopGuardList
0 0.178144 2.61144
1 0.0180427 0.286136
2 0.0170452 0.283548
3 0.0170423 0.289958

Since the ScopeGuardList isn't used in any performance critical part in LibrePCB we didn't look further for a better performing way without type erasure.

Do you have any comments, found a bug or an error? Please leave a note on Reddit.

blogroll

social