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(); }
-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.