Part of the Design Patterns series:

Feature image

The single most important feature of the C++ language is the end scope curly brace }. C++ does not have a garbage collector and doesn’t utilize automatic reference counting (other than for specific library features) and therefore requires the programmer to ensure memory gets cleaned up throughout the lifecycle of the program. The high price of putting the burden on the engineer to manage their own memory comes with the beneficial tradeoff that you can write much more efficient and predictable programs. In order to help developers manage memory, C++ uses an idiom called RAII (resource acquisition is initialization), which involves moving memory ownership to objects that will automatically clean up that memory in its destructor once that object falls out of scope. Falling out of scope means the program reaches the ending } in which that object was defined. Modern C++ leans very heavily upon RAII to ensure both performant software and memory-safe programs.

RAII is a powerful tool that engineers can use for more than just ensuring memory safety. It can also be leveraged to help produce more robust, error-free software. A design pattern that leverages RAII to hedge against runtime errors is the scopeguard. I will be borrowing an example from:

Design Patterns With C++ Author: Fedor G. Pikus

Our example involves some code representing storing records in a database:

C++
 1class Record { ... };
 2class Database {
 3  class Storage { ... };
 4  Storage S;
 5  class Index { ... };
 6  Index I;
 7  public:
 8    void insert (const Record& r);
 9};
10
11void Database::insert(const Record& r) {
12  S.insert(r);
13  I.insert(r);
14}

We have a database class, as well as an Index and Storage class. When a record is inserted into the database, we need to insert it both into our index and storage class. The problem is that the insert for either of these functions can fail and will throw. If the failure happens on the storage insert, our function cleanly fails, throws, and we can successfully handle that throw elsewhere. But what if the failure happens on the index insert? In this case, we throw out of the function but now we have a problem since the insert on the storage class has already happened and we now do not have synchronization between these two classes.

The obvious answer to this problem is to wrap the entire thing in a try-catch and build a mechanism to revert an insert:

C++
1void Database::insert(const Record& r) {
2  S.insert(r);
3  try {
4    I.insert(r);
5  } catch (...) {
6    S.undo();
7    throw;
8  }
9}

This works in this use case, but does not scale as more error-prone functionality gets built into the control flow. Even adding one more function to this insert call now requires adding additional complexity that begins to make reasoning about the code difficult. Even something like adding a required finalize function makes things messier:

C++
 1void Database::insert(const Record& r) {
 2  S.insert(r);
 3  try {
 4    I.insert(r);
 5  } catch (...) {
 6    S.undo();
 7    I.finalize();
 8    throw;
 9  }
10  I.finalize();
11}

Modern C++ RAII to the rescue. Let’s build a scopeguard to clean up this function and ensure we are robust to failure:

C++
 1class ScopeGuardBase {
 2  public:
 3    ScopeGuardBase() : commit_(false) {}
 4    void commit() const noexcept { commit_ = true; }
 5  protected:
 6    ScopeGuardBase(ScopeGuardBase&& other) : commit_(other.commit_) {
 7      other.commit();
 8    }
 9    ~ScopeGuardBase() {}
10    mutable bool commit_;
11  private:
12    ScopeGuardBase& operator=(const ScopeGuardBase&) = delete;
13};
14template <typename Func>
15class ScopeGuard : public ScopeGuardBase {
16  public:
17    ScopeGuard(Func&& func) : func_(func) {}
18    ScopeGuard(const Func& func) : func_(func) {}
19    ~ScopeGuard() { if (!commit_) func_(); }
20    ScopeGuard(ScopeGuard&& other) : ScopeGuardBase(std::move(other)), func_(other.func_) {}
21  private:
22    Func func_;
23};
24template <typename Func>
25ScopeGuard<Func> MakeGuard(Func&& func) {
26  return ScopeGuard<Func>(std::forward<Func>(func));
27}

We now have a scopeguard that is by default “armed,” which means unless we call the commit() to disarm the scopeguard, it will call the provided function when it goes out of scope. This is perfect for our needs above:

C++
1void Database::insert(const Record& r) {
2  S.insert(r);
3  ScopeGuard sg = MakeGuard([&]() { S.undo(); });
4  I.insert(r);
5  sg.commit();
6}

Walking through what happens here, we first attempt to insert into Storage. If that fails, no worries, we throw and move on. But if it succeeds, we need to create our scopeguard, passing in our lambda that will call the undo() function on our Storage class. The storage class is by default armed (the class default constructor sets commit to false) so that when the scopeguard goes out of scope, the destructor will be called and the lambda (S.undo()) will be called. This works well since if I.insert() fails and the function throws, the scopeguard is immediately out of scope, the destructor is called and S gets rolled back. But if I.insert() succeeds, we now need to be sure we disarm our scopeguard since we no longer want to roll back the insert on our Storage class when we get out of scope. By calling sg.commit(), we disarm our scopeguard and ensure no rollback occurs.

The syntax is clean and allows for easy additions and modifications, like adding the finalize() back into our function:

C++
1void Database::insert(const Record& r) {
2  S.insert(r);
3  ScopeGuard fg = MakeGuard([&]() { S.finalize(); });
4  ScopeGuard sg = MakeGuard([&]() { S.undo(); });
5  I.insert(r);
6  sg.commit();
7}

Here we want fg to always be called after a determination is made on sg since the stack always unwinds from bottom to top once we reach the end of scope. No matter what happens after we create the fg scopeguard, we can rest assured that S.finalize() will be called.

Because we relegate the logic of a scopeguard to the destructor, we need to take a few more precautions than normal. In C++, two exceptions cannot be propagated at the same time, otherwise the program will exit. Because an exception will trigger the destructor of the scope guard, any action we attempt that itself throws an error will possibly bring about a scenario where two exceptions propagate at the same time. To protect against this, we can build a shielded scopeguard:

C++
 1template <typename Func>
 2class ScopeGuard : public ScopeGuardBase {
 3  public:
 4    ScopeGuard(Func&& func) : func_(func) {}
 5    ScopeGuard(const Func& func) : func_(func) {}
 6    ~ScopeGuard() { 
 7      if (!commit_) {
 8        try { func_(); } catch (...) {}
 9      }
10    }
11    ScopeGuard(ScopeGuard&& other) : ScopeGuardBase(std::move(other)), func_(other.func_) {}
12  private:
13    Func func_;
14};

If our ScopeGuard destructor throws, we catch it and ensure our program does not prematurely exit.

We can also implement specific logic in the scopeguard destructor based on if its destructor is being triggered due to an exception or not. If we prefer that our scopeguard only implement its function if there are no exceptions propagating the scope, we can do that like this:

C++
 1class UncaughtExceptionDetector {
 2  public:
 3    UncaughtExceptionDetector() : count_(std::uncaught_exceptions()) {}
 4    operator bool() const noexcept { return std::uncaught_exceptions() > count_; }
 5  private:
 6    const int count_;
 7};
 8template <typename Func, bool on_success, bool on_failure>
 9class ScopeGuard : public ScopeGuardBase {
10  public:
11    ScopeGuard(Func&& func) : func_(func) {}
12    ScopeGuard(const Func& func) : func_(func) {}
13    ~ScopeGuard() { 
14      if ((on_success && !detector) || (on_failure && detector)) {
15        func_();
16      }
17    }
18    ScopeGuard(ScopeGuard&& other) : ScopeGuardBase(std::move(other)), func_(other.func_) {}
19  private:
20    UncaughtExceptionDetector detector_;
21    Func func_;
22};

std::uncaught_exceptions() returns a number for how many exceptions are currently propagating in the stack. We can conditionally check within the destructor of the scopeguard and modify our behavior accordingly.