Destructors: Scope Guard
Using scope guard to perform one-off cleanups
Introduction
The name scope guard refers to a technique for performing one-off cleanups developed originally for C++ by Petru Marginean and popularized by Andrei Alexandrescu.
Like RAII, it uses destructors to perform cleanup on scope exit. Multiple cleanups are performed in the reverse order of declaration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "scope_exit.h"
#include <iostream>
void foo()
{
std::cout << "Action 1\n";
auto scope_1 = make_scope_exit([](){
std::cout << "Cleanup 1\n";
});
std::cout << "Action 2\n";
auto scope_2 = make_scope_exit([](){
std::cout << "Cleanup 2\n";
});
std::cout << "Finished\n";
}
// Prints:
// Action 1
// Action 2
// Finished
// Cleanup 2
// Cleanup 1
//
In addition to the version that always cleans up on scope exit there is a version that only calls the cleanup in a failure case. The definition of failure is: if the scope is exited with an exception (stack unwinding in C++ legalese) then it’s a failure, otherwise it’s success and the cleanup will not be performed. The cleanup is a rollback in this case.
There is a third version that calls cleanup only for successful scope exit as opposed to failure. The cleanup is a commit in this case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "scope_failure.h"
#include "scope_success.h"
#include <iostream>
void foo()
{
std::cout << "Action 1\n";
auto scope_1 = make_scope_failure([](){
std::cout << "Rollback 1\n";
});
auto scope_2 = make_scope_success([](){
std::cout << "Commit 1\n";
});
// for the sake of example throw an exception
// to trigger failure handling
throw std::runtime_error("Failure");
}
// Prints:
// Action 1
// Rollback 1
//
// foo exits with std::runtime_error("Failure")
Implementation gist
In 2018, using a modern dialect of C++, it’s easy to implement scope guards, though bear in mind that the idiom arose in about 2000 when it was not so easy: for a start lambdas were not available, macros were used as substitutes.
scope_exit.h
Here is a possible implementation of a scope_exit
class. It calls a function
when the scope is exited. The function is stored in the constructor and called
in the destructor. A helper function is used to deduce the exact type of the
function (e.g. it could be a functor, a function object, or a lambda, or just a
function).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#pragma once
#include <type_traits> // for remove_reference
#include <utility> // for move and forward
template<typename F>
class scope_exit
{
F f_;
public:
// take function by copy
explicit scope_exit(const F & f) :
f_{ f }
{
}
// take function by move
explicit scope_exit(F && f) :
f_{ std::move(f) }
{
}
// call provided function on destructor (scope exit)
~scope_exit()
{
// this simple implementation assumes f_() does
// not throw (see below)
f_();
}
// can't copy (or move), destructor called just once
scope_exit(const scope_exit &) = delete;
scope_exit & operator=(const scope_exit &) = delete;
};
// maker function, to auto-deduce provided function/functor's type
template<typename F>
auto make_scope_exit(F && f)
{
// - the function/functor's type is used to instantiate the right scope_exit
// - std::remove_reference gets the actual type without &
// - due to copy elision can return even if scope_exit can't be copied or move
return scope_exit<std::remove_reference_t<F>>(std::forward<F>(f));
}
stack_unwinding.h
In order to support scope guard scenarios std::uncaught_exception
(returning
a bool
) was initially added to the standard, but it had
issues so then it was deprecated.
std::uncaught_exceptions
(with s
for plural, returning an int
) returning
the number of exceptions in flight can then be used to detect stack unwinding.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once
#include <exception> // for uncaught_exceptions
class stack_unwinding
{
int uncaught_exceptions_{ std::uncaught_exceptions() };
public:
bool operator() {
{
// if the number of exceptions in flight
// is larger than when instance was constucted
// this function is called as part of stack unwinding
// for an exception.
return (uncaught_exceptions_ != std::uncaught_exceptions());
}
stack_unwinding(const stack_unwinding &) = delete;
stack_unwinding & operator=(const stack_unwinding &) = delete;
};
scope_failure.h
Here is a possible implementation of a scope_failure
class. It calls the
cleanup function only if exiting with stack unwinding.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#pragma once
#include "stack_unwinding.h"
#include <type_traits> // for remove_reference
#include <utility> // for move and forward
template<typename F>
class scope_failure
{
F f_;
stack_unwinding stack_unwinding_;
public:
explicit scope_failure(const F & f) :
f_{ f }
{
}
explicit scope_failure(F && f) :
f_{ std::move(f) }
{
}
~scope_failure()
{
if (stack_unwinding_())
{
// this simple implementation assumes f_() does
// not throw (see below)
f_();
}
}
scope_failure(const scope_failure &) = delete;
scope_failure & operator=(const scope_failure &) = delete;
};
template<typename F>
auto make_scope_failure(F && f)
{
return scope_failure<std::remove_reference_t<F>>(std::forward<F>(f));
}
scope_success
scope_success
is dubious. One one side it compliments scope_failure
and
allows commit code to stay close to the related action. But using a destructor
to call a function when no exception is thrown is convoluted compared with just
calling the function.
Comparing approaches
Compared with RAII
The RAII approach handcrafts a RAII class for each specific cleanup, it
provides benefits when it’s reused. Compare the scope_exit
usage above with
the code below. It is true that the message_on_exit
RAII class takes 12
additional lines, but we save a bit on each reuse, because std::cout <<
is
not repeated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
struct message_on_exit
{
const char * msg;
~message_on_exit()
{
std::cout << msg;
}
message_on_exit(const message_on_exit &) = delete;
message_on_exit & operator=(const message_on_exit &) = delete;
};
int main()
{
std::cout << "Action 1\n";
message_on_exit scope_1{ "Cleanup 1\n" };
std::cout << "Action 2\n";
message_on_exit scope_2{ "Cleanup 2\n" };
std::cout << "Finished\n";
}
Compared with using try-catch blocks
Should you have a situation where there is an action with cleanup, rollback and
commit the try
catch
approach takes the following shape (assuming that
cleanup, rollback and commit functions do not throw, otherwise more code is
required).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
void bar()
{
// trigger failure in this example
throw std::runtime_error("Failure");
}
void foo()
{
std::cout << "Action 1\n";
try
{
bar();
std::cout << "Commit 1\n";
std::cout << "Cleanup 1\n";
}
catch (...)
{
std::cout << "Rollback 1\n";
std::cout << "Cleanup 1\n";
throw;
}
}
// Prints:
// Action 1
// Rollback 1
// Cleanup 1
//
// foo exits with std::runtime_error("Failure")
The try
catch
block approach becomes unreadable for anything but the
simplest cases.
Compared with D
Andrei Alexandrescu went to work on the D laguage where the scope guard technique was built in the language, here is a sample usage in D.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import std.stdio : writeln;
void foo()
{
writeln("In foo");
throw new Exception("Foo exception");
}
void bar()
{
writeln("Action 1");
scope(exit) writeln("Cleanup 1");
scope(failure) writeln("Rollback 1");
scope(success) writeln("Commit 1");
foo();
}
// Prints
// Action 1
// In foo
// Rollback 1
// Cleanup 1
//
// Bar exits with Foo exception
Exceptions
While cleaning up
One approach is to ensure that in cleanup (including rollback and commit work) exceptions are not thrown.
But if an exception is thrown, then the simple approach we took will terminate
the program because of the implicit noexcept
for destructors. This would be a
another approach.
Another approach is the one took for by the D language, there cleanups can throw. To emulate this behaviour while still staying within the C++ language rules we can catch and ignore exceptions if stack unwinding and propagate them if not stack unwinding. The destructors for the scope guards need to then look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
~scope_exit() noexcept(false)
{
if (stack_unwinding_())
{
try { f_(); } catch (...) { }
}
else
{
f_();
}
}
~scope_failure()
{
if (stack_unwinding_())
{
try { f_(); } catch (...) { }
}
}
~scope_success() noexcept(false)
{
if (!stack_unwinding_())
{
f_();
}
}
With this change the code below will first execute a commit which throws triggering the failure scenario and hence subsequent rollback.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "scope_failure.h"
#include "scope_success.h"
#include <iostream>
void foo()
{
std::cout << "Action 1\n";
auto scope_1 = make_scope_failure([](){
std::cout << "Rollback 1\n";
// for the sake of example throw an exception
throw std::runtime_error("Another failure");
});
auto scope_2 = make_scope_success([](){
std::cout << "Commit 1\n";
// for the sake of example throw an exception
throw std::runtime_error("Failure");
});
std::cout << "Success\n";
}
// Prints:
// Action 1
// Success
// Commit 1
// Rollback 1
//
// foo exits with std::runtime_error("Failure")
In the example above, faced with two exceptions while cleaning up, the function
foo
exits with the first exception. However the D equivalent code exits with
the second exception.
While constructing scope objects
Another issue to consider is the possibility of failure to construct the scope guard objects. One such situation could be if the lambda we provide captures something by value and that copy throws. Obviously in that case the cleanup will not be performed.
Conclusion
When performing cleanup, using a try
catch
manual approach will not scale.
RAII surely wins when a cleanup is used in many places in code, releasing resources such as freeing heap allocated memory, closing handles, unlocking mutexes.
For cleanup actions that are one-offs, i.e. once or low single digit numbers, involving a external cleanup that succeeds most of the time, the scope guard approach saves some code compared with the RAII approach.
However scope guards have issues:
- Yet another idiom with subtle behaviours around exception handling that users need to understand (in particular which are the choices made by the specific scope guards used)
- Equating exceptions with failures makes it suitable for some environments only
References
Generic: Change the Way You Write Exception-Safe Code — Forever
http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758
Herb Sutter on the issues with uncaught_exception
http://www.gotw.ca/gotw/047.htm
Andrei Alexandrescu: Three Unlikely Successful Features of D
https://www.youtube.com/watch?v=1BRTX86DfOY
D language exception safety
https://dlang.org/articles/exception-safe.html