...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
scope_exit
, scope_success
and scope_fail
defer_guard
A scope guard is an object that invokes an arbitrary action function object on destruction. Scope guards are useful for implementing actions that need to be reliably performed upon control leaving an execution scope (for example, when returning from a function), which is especially helpful for handling exceptions.
The wrapped action function object is specified on the scope guard construction and cannot be changed afterwards. Its type must be one of:
operator()
taking no arguments, or
Note that if the wrapped function is a reference to a function object, that object must be stored externally to the scope guard and must remain valid for the entire lifetime of the scope guard.
Some scope guards also support specifying an additional condition function object, which allows for customizing the conditions in which the action function object must be called. Condition function objects are discussed in more detail in a later section.
Boost.Scope provides four kinds of scope guards, differing in their features and conditions upon which the action function object is called, summarised in the table below.
Table 1.2. Scope guard comparison
Feature |
||||
---|---|---|---|---|
Supports a condition function? |
Yes |
Yes |
Yes |
No |
Invokes action on normal scope exit? |
Yes, by default. Depends on condition. |
Yes |
No |
Yes |
Invokes action on scope exit due to failure? |
Yes, by default. Depends on condition. |
No |
Yes |
Yes |
Action may throw? |
Yes, by default. Depends on condition. |
Typically yes |
Typically no |
Yes |
Can be (de)activated? |
Yes |
Yes |
Yes |
No |
Move-constructible? (requires function objects to be move-constructible) |
Yes |
Yes |
Yes |
No |
Has factory function? (C++11-friendly) |
Yes |
Yes |
Yes |
No |
In the table above, the term "failure" is used broadly. What constitutes a failure in a given context is specified by user in the form of the condition function object. Most often, a thrown exception is taken as an indication of a failure, but it can be changed to, for example, a check for an error code that is being returned from the enclosing function.
For scope_exit
, there
is no notion of "success" or "failure". Rather, the scope
guard invokes the action function object if the condition returns true
. By default, if the condition function
object is not specified by user, the scope guard operates as if the condition
always returns true
, which means,
for example, that it will invoke the action whether the control leaves the
scope normally or due to an exception.
Although it is possible to specify arbitrary condition function objects, the
intended use case for scope_success
is to invoke its action when the scope is left normally (i.e. not because of
a failure) and for scope_fail
- to invoke its action to handle errors, including exceptions. For this reason,
action functions that are used with scope_fail
are typically not allowed to throw, as this may cause the program to terminate.
This is also a concern with other scope guards, such as scope_exit
and defer_guard
, which
also may invoke their actions due to an exception. It is user's responsibility
to ensure that scope guard actions don't throw if another exception is being
propagated. Generally, it is recommended to use scope guards to implement actions
that cannot throw and move all operations that may fail to the normal code
flow.
#include <boost/scope/scope_exit.hpp
> #include <boost/scope/scope_success.hpp
> #include <boost/scope/scope_fail.hpp
>
The scope_exit
, scope_success
, and scope_fail
scope guards have a
lot of similarities in interfaces and capabilities and differ in conditions
when they invoke the action function object. As shown in the table above,
these scope guards support specifying an additional condition function object.
By default, scope_success
will invoke its action if it is destroyed normally, scope_fail
- if it is destroyed due to an exception being thrown. scope_exit
by default operates as if the condition always allows executing the action.
![]() |
Tip |
---|---|
Condition function objects will be discussed in more detail in the next section. Note that there are caveats with detecting whether an exception has been thrown, also discussed in the next section. |
Given the different default condition function objects, the different scope guard types are typically useful for different purposes:
scope_exit
is useful
for general cleanup tasks, which need to be performed regardless of success
or failure of the enclosing function.
scope_success
can be used for common finalization code that needs to be performed on
successful completion of the function.
scope_fail
is intended
for handling errors, for example, to revert a partially complete operation
to the previous valid state.
All scope guards are especially useful if there are multiple return points (both normal and via exception), and when the scope guard action is not easy to extract as a function.
Let's consider an example of a class that receives network requests, validates them and passes to session objects to which each request corresponds. After receiving a request, the class must send a response - either a successful one, with the results of processing the request, or an error.
// A network request receiver class receiver { // A network connection connection m_conn; // A collection of sessions, searchable by session id std::map< std::string, session > m_sessions; public: // Processes a network request and sends a response void process_request(request const& req) { const auto start_time = std::chrono::steady_clock::now(); boost::scope::scope_exit time_guard([start_time] { // Log the amount of time it took to process the request const auto finish_time = std::chrono::steady_clock::now(); std::clog << "Request processed in " << std::chrono::duration_cast< std::chrono::milliseconds >(finish_time - start_time).count() << " ms" << std::endl; }); response resp; boost::scope::scope_fail failure_guard([&] { // Disconnect in case of exceptions m_conn.disconnect(); // doesn't throw }); boost::scope::scope_success success_guard([&] { // Send a response that was populated while processing the request m_conn.send_message(resp); }); // Validate the request and populate the response based on the contents of the request if (req.start_line().version > max_supported_version) { resp.start_line().version = max_supported_version; resp.start_line().set_status(protocol_version_not_supported); return; } resp.start_line().version = req.start_line().version; auto it_cseq = req.headers().find(cseq_header); if (it_cseq == req.headers().end()) { resp.start_line().set_status(bad_request); resp.set_body(mime::text_plain, "CSeq not specified"); return; } resp.headers().add(cseq_header, *it_cseq); auto it_session_id = req.headers().find(session_id_header); if (it_session_id == req.headers().end()) { resp.start_line().set_status(bad_request); resp.set_body(mime::text_plain, "Session id not specified"); return; } resp.headers().add(session_id_header, *it_session_id); // Find the session to forward the request to auto it_session = m_sessions.find(*it_session_id); if (it_session == m_sessions.end()) { resp.start_line().set_status(session_not_found); return; } // Process the request in the session and complete populating the response it_session->second.process_request(req, resp); } };
In the example above, the scope guards automatically perform their respective
actions regardless of the return point of the process_request
method. success_guard
makes
sure the response is sent, whether it is a successful response after handling
the request in the target session object, or one of the error responses.
failure_guard
handles the
case when the processing fails with an exception and terminates the connection
in this case. Note that as failure_guard
is destroyed after success_guard
,
it also covers errors that might occur while sending the response. Finally,
time_guard
logs the time
it took to process the request, whether successfully or with an exception,
for diagnostic purposes.
Each of the scope guards described in this section supports active and inactive
states. The action function object will only be called if the scope guard
is in active state while being destroyed. By default, scope guards are created
in the active state, but this can be changed by passing false
as the last argument for the constructor. Scope guards can also be deactivated
or activated during their lifetime by calling the set_active
method. Deactivating the scope guard is often used as a way to "commit"
the effects of the block of code between the scope guard creation and deactivation.
Manual activation of a scope guard can sometimes be useful if the scope guard
needs to be created before it becomes known whether the scope guard action
needs to be executed (e.g. when the scope guard needs to be created at an
outer scope compared to where the decision on whether it should be enabled
is made).
class collection { std::map< int, object > objects; public: // Finds an existing object by key or creates a new one and returns a reference to it object& obtain_object(int key) { // Find the object or the position where it should be auto it = objects.lower_bound(key); // Create an inactive scope guard initially. Needs to be created // at this scope to cover the on_requested() call below. boost::scope::scope_fail rollback_guard{[&] { // Remove the object that was just inserted objects.erase(it); }, false}; if (it == objects.end() || it->first != key) { // Insert a new object that correspond to the specified key it = objects.emplace_hint(it, key, object{}); // Activate rollback guard to revert the insertion in case of error rollback_guard.set_active(true); // Initialize the inserted object. May throw. it->second.initialize(*this, key); } // Notify the object that is has been requested. May throw. it->second.on_requested(); return it->second; } };
The code samples above rely on C++17 class template argument deduction (CTAD) for the scope guard types to deduce the function object type (which is the lambda). If this feature is not available, the scope guard construction can be rewritten using a factory function, like this:
auto rollback_guard = boost::scope::make_scope_fail([&] { objects.erase(it); }, false);
Factory functions are provided for each of the three scope guards described
in this section and are compatible with C++11. The factory functions are
named as make_<scope_guard>
and accept the same arguments as the corresponding scope guard's constructor.
Scope guards described in this section are move-constructible (but not assignable), which requires the wrapped function objects to be move- or copy-constructible as well. After moving, the moved-from scope guard becomes inactive. If a moved-from scope guard is active on destruction, the behavior is undefined.
#include <boost/scope/exception_checker.hpp
> #include <boost/scope/error_code_checker.hpp
>
As discussed before, scope_exit
,
scope_success
and
scope_fail
support
specifying an additional condition function object that will be called to
decide whether the scope guard should invoke the action. A condition function
object must satisfy the following requirements:
operator()
taking no arguments, or
bool
, and
For all the three scope guards, the condition function object is optional.
If not specified, scope_exit
operates as if the condition function object always returns true
and will invoke its action function object
as long as the scope guard is active. Otherwise, the scope guard only invokes
its action if the condition returns true
.
For scope_success
and scope_fail
, the
exception_checker
condition function is used by default. It works by capturing the number of
uncaught exceptions on construction (which happens at the point of construction
of the scope guard) and then comparing the captured value with the number
of uncaught exceptions when it is called (which happens when the scope guard
is destroyed). If the number has increased, it is taken as a sign that a
new exception is in flight, and the predicate returns true
;
otherwise, the predicate returns false
.
![]() |
Note |
---|---|
By design, |
scope_success
invokes
its action when the condition returns false
(i.e. when the failure is not detected) and scope_fail
- when the condition returns true
(i.e. when the failure is detected). In relation to these two scope guard
types, the condition function object is sometimes called the failure condition
because it indicates whether a failure has happened.
You may notice that scope_exit
behavior (with a user-specified condition function object) is similar to
scope_fail
and opposite
to scope_success
in terms of their usage of condition function objects. The main difference
is a semantic one: scope_exit
does not have a success/failure connotation and may be used with arbitrary
condition functions. On the other hand, scope_success
and scope_fail
typically
correspond to their respective intended use cases, and the default condition
function makes them equivalent to the scope guards defined in <experimental/scope>
.
It is possible to emulate each of the scope guards described in this section
by using scope_exit
,
either with a custom condition function object, with the condition function
embedded into the action function, or by activating or deactivating the scope
guard after construction. For example, the following four pieces of code
have the same effect:
Table 1.3. Comparison of scope_fail
and scope_exit
|
|
|
|
---|---|---|---|
void push_back(int x, std::vector< int >& vec1, std::vector< int >& vec2) { vec1.push_back(x); // Revert vec1 modification on failure boost::scope::scope_fail rollback_guard{[&] { vec1.pop_back(); }}; vec2.push_back(x); } |
void push_back(int x, std::vector< int >& vec1, std::vector< int >& vec2) { vec1.push_back(x); // Revert vec1 modification on failure boost::scope::scope_exit rollback_guard { [&] { vec1.pop_back(); }, boost::scope::exception_checker() }; vec2.push_back(x); } |
void push_back(int x, std::vector< int >& vec1, std::vector< int >& vec2) { vec1.push_back(x); // Revert vec1 modification on failure boost::scope::scope_exit rollback_guard { [&, uncaught_count = std::uncaught_exceptions()] { if (std::uncaught_exceptions() > uncaught_count) vec1.pop_back(); } }; vec2.push_back(x); } |
void push_back(int x, std::vector< int >& vec1, std::vector< int >& vec2) { vec1.push_back(x); // Revert vec1 modification on failure boost::scope::scope_exit rollback_guard{[&] { vec1.pop_back(); }}; vec2.push_back(x); // Commit vec1 modification rollback_guard.set_active(false); } |
The main benefit of using scope_fail
in this example is reducing code size and complexity. You can see that adding
the check for the number of uncaught exceptions to the scope guard action
makes it notably more verbose, and if such a check needs to be performed
in multiple scope guards, this code would have to be duplicated. Explicitly
deactivating the scope guard also has its downsides, as it may be prone to
errors if it has to be performed in multiple places of the code (for example,
when there are multiple return points, where the transaction needs to be
committed). This goes against the intended purpose of the scope guard, as
it was supposed to automate the correct execution of its action without the
user having to manually ensure this at every possible point of leaving the
scope.
Another benefit of scope_fail
is improved code readability. For a reader, scope_fail
immediately indicates an error handler, whereas scope_exit
does not have such connotation and may contain a general cleanup code.
That being said, there may be reasons to still use one of the techniques
demonstrated above instead of scope_success
/scope_fail
, especially when paired
with exception_checker
.
As noted above, exception_checker
is incompatible with coroutines and similar facilities, so using a manually
managed scope_exit
can be a solution. Another consideration is performance, specifically in
relation with exception_checker
.
This predicate has to invoke runtime functions to obtain the number of uncaught
exceptions, and has to do this twice - on scope guard construction and destruction.
Although these functions are usually cheap, these calls are typically not
optimized away by the compiler, even if no exception is thrown, and deactivating
a scope guard is cheaper still. So, if a scope guard is used in a tight loop
where its performance overhead may be significant, preferring scope_exit
with manual deactivation
may be a reasonable choice.
scope_exit
, scope_success
and scope_fail
accept the condition
function object as the second argument for the constructor or factory function,
after the action function object. If the condition function object is default-constructible
and the default constructor doesn't throw, the function object may be omitted
from the scope guard constructor arguments.
// Writes a string to a file using a file descriptor, with file locking. // Reports errors via an error_code parameter. void locked_write_string(int fd, std::string const& str, std::error_code& ec) { int err = 0; // Automatically transform errors to error_code on exit boost::scope::scope_fail fail_guard { // Action function object [&] { ec = std::error_code(err, std::generic_category()); }, // Condition function object [&err]() noexcept { return err != 0; } }; // Performs a lock operation on the file descriptor, returns error code or 0 on success auto lock_operation = [fd](int operation) { while (flock(fd, operation) < 0) { int err = errno; if (err != EINTR) return err; } return 0; }; // Lock the file err = lock_operation(LOCK_EX); if (err != 0) return; // Unlock the file on exit BOOST_SCOPE_DEFER [&] { lock_operation(LOCK_UN); }; // Write data const char* p = str.data(); std::size_t size_left = str.size(); while (size_left > 0) { ssize_t written = write(fd, p, size_left); if (written < 0) { err = errno; if (err == EINTR) { err = 0; // we do not want to report EINTR to the caller continue; } return; } p += written; size_left -= written; } }
Besides exception_checker
,
the library also provides error_code_checker
that can be used as a condition function object. On construction, error_code_checker
captures
a reference to an external error code object and checks it for an error indication
when being called. This implies that the error code object must remain valid
for the entire lifetime duration of the error_code_checker
predicate.
An object ec
can be used
as an error code object if:
!ec
is valid, never throws, and returns a value contextually convertible
to bool
, and
!ec
produces a value contextually convertible to true
when there is no error and false
otherwise.
That is, for an error code object ec
,
invoking error_code_checker
results in a value equivalent to !!ec
. This makes error_code_checker
compatible with a wide variety of error code types, including:
std::error_code
or boost::system::error_code
from Boost.System,
std::expected
, boost::outcome_v2::basic_outcome
or boost::outcome_v2::basic_result
from Boost.Outcome,
int
, where the value of
0 indicates no error,
bool
, where the value of
false
indicates no error,
T*
,
where a null pointer indicates no error.
For C++11 compilers, the library also provides a factory function check_error_code
. For example, our previous
example could use this condition function object like this:
// Writes a string to a file using a file descriptor, with file locking. // Reports errors via an error_code parameter. void locked_write_string(int fd, std::string const& str, std::error_code& ec) { int err = 0; // Automatically transform errors to error_code on exit boost::scope::scope_fail fail_guard { // Action function object [&] { ec = std::error_code(err, std::generic_category()); }, // Condition function object boost::scope::check_error_code(err) }; // ... }
#include <boost/scope/defer.hpp
>
The defer_guard
scope
guard is similar to scope_exit
without a user-specified condition function object in terms of when it invokes
the action function. But it does not support custom condition functions and
lacks support for moveability and activation/deactivation - this scope guard
is always active upon construction. This allows for a more efficient implementation
when these features are not needed.
![]() |
Note |
---|---|
|
Since defer_guard
effectively provides no interface to interact with after construction, it
is better suited for anonymous "set up and forget" kind of scope
guards. To emphasize this affinity, the library provides a BOOST_SCOPE_DEFER
macro, which acts as
a keyword defining a uniquely named defer_guard
scope guard. The macro should be followed by the function object to be invoked
on scope exit.
BOOST_SCOPE_DEFER [] { std::cout << "Hello world!" << std::endl; };
![]() |
Note |
---|---|
|
As you can see, BOOST_SCOPE_DEFER
offers a few syntax improvements over the other scope guard declarations:
However, it should be noted that the use of the BOOST_SCOPE_DEFER
macro is entirely optional. Users are free to use defer_guard
directly.
boost::scope::defer_guard guard{[] { std::cout << "Hello world!" << std::endl; }};
When using scope guards, users should make sure that all variables captured
by reference are still in a valid state upon the scope guard destruction.
This especially pertains to lambda functions that capture variables by reference
by default (i.e. where the capture clause starts with &
).
Consider the following example:
void bad(std::unique_ptr< int > ptr) { assert(ptr != nullptr); boost::scope::scope_exit guard{[&] { std::cout << "processed: " << *ptr << std::endl; }}; process(std::move(ptr)); // may consume ptr, leaving it null for the scope guard action }
Or a slightly less obvious version of it, involving implicit move of the returned value:
std::unique_ptr< int > bad(std::unique_ptr< int > ptr) { assert(ptr != nullptr); boost::scope::scope_exit guard{[&] { std::cout << "processed: " << *ptr << std::endl; }}; process(*ptr); return ptr; // moves from ptr, leaving it null for the scope guard action }
In cases like this consider capturing by value in the function object or moving the scope guard to a deeper scope, so that it executes its action before the variables captured by reference become invalid.
std::unique_ptr< int > good(std::unique_ptr< int > ptr) { assert(ptr != nullptr); { boost::scope::scope_exit guard{[&] { std::cout << "processed: " << *ptr << std::endl; }}; process(*ptr); } // <- scope guard action runs here, before ptr is moved-from return ptr; }
It is possible to use scope guard classes to implement scope exit actions
that are initialized at run time. One way to do this is to use a function
object wrapper such as std::function
together with the scope guard to schedule the function call. For example:
using cleanup_func_t = std::function< void() >; cleanup_func_t cleanup_func; // Create an inactive scope guard first, since the cleanup function is not set yet boost::scope::scope_exit< cleanup_func_t& > cleanup(cleanup_func, false); // Later in the program, initialize the cleanup function with the function selected at run time if (cond) { cleanup_func = [] { std::cout << "cond is true" << std::endl; }; } else { cleanup_func = [] { std::cout << "cond is false" << std::endl; }; } // Activate the scope guard once the cleanup function is initialized cleanup.set_active(true);
It is also possible to do this with BOOST_SCOPE_DEFER
,
although it eliminates one of the advantages provided by this macro, namely
not having to invent a variable name. Also note that the function wrapper
must be valid at all times once the scope guard is constructed.
// Create a non-empty function wrapper that does nothing std::function< void() > cleanup_func = [] {}; // Create a scope guard that refers to the function wrapper BOOST_SCOPE_DEFER std::ref(cleanup_func); // Later in the program, initialize the function wrapper if (cond) { cleanup_func = [] { std::cout << "cond is true" << std::endl; }; } else { cleanup_func = [] { std::cout << "cond is false" << std::endl; }; }
However, users should be aware that function wrappers typically use dynamic
memory allocation internally and copy the function object data, which may
involve calling copy constructors that may also fail with an exception. Although
many standard library implementations use small object optimization for
std::function
, and this technique is also used
in other implementations like Boost.Function,
it is generally not guaranteed that initializing the function wrapper with
a given function object will not throw. Because of this, using std::function
and similar wrappers is usually not recommended.
If setting up the scope exit action needs to be a non-throwing operation (for example, if the scope guard is supposed to revert the effects of the immediately preceding operation), it is recommended to initialize inactive scope guards beforehand and only activate one of them at a later point in the program.
// Create inactive scope guards for both branches boost::scope::scope_exit cleanup_true([] { std::cout << "cond is true" << std::endl; }, false); boost::scope::scope_exit cleanup_false([] { std::cout << "cond is false" << std::endl; }, false); // Later in the program, activate one of the scope guards. // This won't throw. if (cond) cleanup_true.set_active(true); else cleanup_false.set_active(true);
Alternatively, one could implement the selection within the scope guard action itself.
// Create a single inactive scope guard that implements both branches bool cond; boost::scope::scope_exit cleanup([&cond] { if (cond) std::cout << "cond is true" << std::endl; else std::cout << "cond is false" << std::endl; }, false); // Later in the program, select the branch to perform // and activate the scope guard. cond = select_branch(); cleanup.set_active(true);
Boost.ScopeExit defines a set of macros for defining code blocks to be executed at scope exit. Scope guards provided by Boost.Scope provide similar functionality, but with simpler syntax and new features. Differences between libraries are summarized in the table below.
Table 1.4. Boost.ScopeExit and Boost.Scope comparison
Feature |
Boost.Scope |
|
---|---|---|
Minimum and recommended C++ version. |
C++03 minimum, C++11 recommended for some features. |
C++11 minimum, C++17 recommended for some features. |
Basic functionality. |
BOOST_SCOPE_EXIT(void) { std::cout << "Hello, World!" << std::endl; } BOOST_SCOPE_EXIT_END; |
boost::scope::scope_exit guard([] { std::cout << "Hello, World!" << std::endl; }); Or: BOOST_SCOPE_DEFER [] { std::cout << "Hello, World!" << std::endl; }; |
Capturing variables. |
Yes, both by value and by reference. int x; std::string str; BOOST_SCOPE_EXIT(x, &str) { std::cout << "x = " << x << ", str = " << str << std::endl; } BOOST_SCOPE_EXIT_END; In C++03 mode without variadic macros support, Boost.Preprocessor sequence should be used to list captured variables. |
Yes, by means of the standard C++ lambda captures. int x; std::string str; boost::scope::scope_exit guard([x, &str] { std::cout << "x = " << x << ", str = " << str << std::endl; }); Or: int x; std::string str; BOOST_SCOPE_DEFER [x, &str] { std::cout << "x = " << x << ", str = " << str << std::endl; }; |
Capturing all variables. |
Yes, requires C++11. int x; std::string str; BOOST_SCOPE_EXIT_ALL(&) { std::cout << "x = " << x << ", str = " << str << std::endl; };
Note that no |
Yes, by means of the standard C++ lambda captures. int x; std::string str; boost::scope::scope_exit guard([&] { std::cout << "x = " << x << ", str = " << str << std::endl; }); Or: int x; std::string str; BOOST_SCOPE_DEFER [&] { std::cout << "x = " << x << ", str = " << str << std::endl; }; |
Capturing |
Yes, with a special class my_class { private: int x; std::string str; public: void foo() { BOOST_SCOPE_EXIT(this_) { std::cout << "x = " << this_->x << ", str = " << this_->str << std::endl; } BOOST_SCOPE_EXIT_END; } }; Note that the keyword must be explicitly used to reference members of the class within the scope exit block.
Capturing class my_class { private: int x; std::string str; public: void foo() { BOOST_SCOPE_EXIT_ALL(this) { std::cout << "x = " << x << ", str = " << str << std::endl; }; } }; |
Yes, by means of the standard C++ lambda captures. class my_class { private: int x; std::string str; public: void foo() { boost::scope::scope_exit guard([this] { std::cout << "x = " << x << ", str = " << str << std::endl; }; } }; Or: class my_class { private: int x; std::string str; public: void foo() { BOOST_SCOPE_DEFER [this] { std::cout << "x = " << x << ", str = " << str << std::endl; }; } }; |
Capturing specific members of a class. |
Yes. class my_class { private: int x; std::string str; public: void foo() { BOOST_SCOPE_EXIT(x, &str) { std::cout << "x = " << x << ", str = " << str << std::endl; } BOOST_SCOPE_EXIT_END; } }; |
No. Capture class my_class { private: int x; std::string str; public: void foo() { BOOST_SCOPE_DEFER [x = x, str = std::ref(str)] { std::cout << "x = " << x << ", str = " << str.get() << std::endl; }; } }; |
Support for scope guards in templates. |
Yes, using a special macro. template< typename T > void foo(T const& param) { BOOST_SCOPE_EXIT_TPL(¶m) { std::cout << "Param: " << param << std::endl; } BOOST_SCOPE_EXIT_END; } |
Yes, with no special syntax. template< typename T > void foo(T const& param) { BOOST_SCOPE_DEFER [¶m] { std::cout << "Param: " << param << std::endl; }; } |
Support for variadic templates and argument packs. |
Yes, using the template< typename... Args > void foo(Args&&... args) { BOOST_SCOPE_EXIT_ALL(&args...) { std::cout << "Params: " << boost::tuples::tie(args...) << std::endl; }; } |
Yes, by means of the standard C++ lambda captures. template< typename... Args > void foo(Args&&... args) { BOOST_SCOPE_DEFER [&args...] { std::cout << "Params: " << boost::tuples::tie(args...) << std::endl; }; } |
Native support for activation/deactivation of the scope guard. |
No, the activation/deactivation logic needs to be embedded into the action code block. bool active = false; BOOST_SCOPE_EXIT(&active) { if (active) std::cout << "Hello, World!" << std::endl; } BOOST_SCOPE_EXIT_END; active = true; |
Yes, with boost::scope::scope_exit guard([] { std::cout << "Hello, World!" << std::endl; }, false); guard.set_active(true); |
Support for custom conditions for executing the action. |
No, the condition needs to be embedded into the action code block. const auto uncaught_count = std::uncaught_exceptions(); BOOST_SCOPE_EXIT(uncaught_count) { if (std::uncaught_exceptions() > uncaught_count) std::cout << "Exception thrown!" << std::endl; } BOOST_SCOPE_EXIT_END; |
Yes, with boost::scope::scope_exit guard( [] { std::cout << "Exception thrown!" << std::endl; }, [uncaught_count = std::uncaught_exceptions()] { return std::uncaught_exceptions() > uncaught_count; }); Or simply: boost::scope::scope_fail guard([] { std::cout << "Exception thrown!" << std::endl; }); |
Utilities for checking exceptions and error codes as criteria for invoking the action. |
No, the condition needs to be embedded into the action code block. const unsigned int uncaught_count = boost::core::uncaught_exceptions(); BOOST_SCOPE_EXIT(uncaught_count) { if (uncaught_count < boost::core::uncaught_exceptions()) std::cout << "Exception thrown!" << std::endl; } BOOST_SCOPE_EXIT_END; Or: std::error_code ec; BOOST_SCOPE_EXIT(&ec) { if (ec) std::cout << "Execution failed!" << std::endl; } BOOST_SCOPE_EXIT_END; |
Yes, // scope_fail by default checks for an exception boost::scope::scope_fail guard([] { std::cout << "Exception thrown!" << std::endl; }); Or: std::error_code ec; boost::scope::scope_fail guard([] { std::cout << "Execution failed!" << std::endl; }, boost::scope::check_error_code(ec)); |
Named scope guards. |
No, the scope guard is declared as an unnamed code block. (The scope guard is internally implemented as an object with a unique name on the stack, but this is not part of the public interface.) |
Yes, scope guards are defined as objects. Also, |
Anonymous scope guards. |
Yes. |
Yes, using the |
Macro-less usage. |
No. |
Yes. |
In the table above, you may notice that a significant amount of feature parity
between Boost.ScopeExit
and Boost.Scope is achieved by using the BOOST_SCOPE_EXIT_ALL
macro in the former. This macro is implemented using C++11 lambda functions
internally, which explains a lot of similarities between the libraries in
this case. Users can also define BOOST_SCOPE_EXIT_CONFIG_USE_LAMBDAS
to force other Boost.ScopeExit
macros to use C++11 lambdas internally, which will change the syntax and
variable capture rules accordingly. However, there are a few caveats one
needs to keep in mind when using the library in this mode (or the BOOST_SCOPE_EXIT_ALL
macro in any mode):
boost::function
object internally. This means
that creating the scope guard may require dynamic memory allocation and
can fail with an exception. Boost.ScopeExit
does not document behavior in this case, but in practice, as of Boost
1.83.0, the scope exit block does not get executed in this case. This
means that Boost.ScopeExit
in this mode cannot be safely used in non-throwing contexts, and cannot
guarantee execution of the scope exit block. There is also performance
overhead associated both with construction and execution of the scope
guard.
In comparison, Boost.Scope scope guards do not use function object wrappers,
such as boost::function
or std::function
,
to store the function objects. This means that the scope guards do not perform
dynamic memory allocation (unless the function objects themselves do) and
do not have the associated exception safety issues and performance overhead.
Additionally, the scope guards will execute the action if initializing the
scope guard fails with an exception.
The C++
Extensions for Library Fundamentals TS defines three scope guard
types, scope_exit
, scope_success
and scope_fail
,
which have similar semantics to the same-named scope guards provided by this
library. There are some differences and extensions that are summarised below.
All three of the scope_exit
,
scope_success
and scope_fail
scope
guards provided by Boost.Scope support an optional condition
function that is used to decide whether the scope guard should invoke
its wrapped action function. The scope guards defined by the TS do not
have this feature, the behavior of each of the three scope guards is predefined
by the specification.
The default condition functions used in the Boost.Scope scope guards make
the behavior equivalent to the TS, so if the user doesn't use custom condition
functions, the scope guard usage is equivalent between the two. If custom
condition functions are used, those can be emulated in TS scope guards
by embedding the condition function into the action called by std::experimental::scope_exit
. For example, if the user's
code uses error codes as the failure indication with scope_fail
,
the equivalent TS code could look like this:
Table 1.5. Using custom condition functions
Boost.Scope |
Library Fundamentals TS |
---|---|
std::error_code ec; boost::scope::scope_fail guard { [] { handle_error(); }, boost::scope::check_error_code(ec) }; |
std::error_code ec; std::experimental::scope_exit guard { [&ec] { if (ec) handle_error(); } }; |
Note that we had to switch from scope_fail
to scope_exit
in case of
TS.
Scope guards defined in the TS are always constructed active and can only
be deactivated by calling release()
method. Boost.Scope supports creating
initially inactive scope guards, as well as activating and deactivating
them after creation. This allows for more flexible placement of the scope
guards in the user's code, which is useful if the decision on whether to
execute the scope guard action needs to be made after the scope guard is
constructed.
This feature can be emulated in TS using additional state that the scope guard action checks to see if the action needs to be performed.
Table 1.6. Explicitly activating scope guards
Boost.Scope |
Library Fundamentals TS |
---|---|
// Insert value into both containers and call on_inserted() afterwards. // pv may be null, in which case only insert the value into the set. // Maintain both containers intact if an exception is thrown. void insert_two(std::vector< int >* pv, std::set< int >& set, int value) { // Initially inactive scope guard boost::scope::scope_fail pv_rollback([pv] { pv->pop_back(); }, false); if (pv) { pv->push_back(value); // may throw pv_rollback.set_active(true); // activate the scope guard } auto [it, inserted] = set.insert(value); // may throw // Initially active scope guard, if a new element was inserted, otherwise inactive boost::scope::scope_fail set_rollback([&set, &it] { set.erase(it); }, inserted); on_inserted(); // may throw } |
// Insert value into both containers and call on_inserted() afterwards. // pv may be null, in which case only insert the value into the set. // Maintain both containers intact if an exception is thrown. void insert_two(std::vector< int >* pv, std::set< int >& set, int value) { bool pv_inserted = false; // Initially active scope guard, but blocked by pv_inserted == false std::experimental::scope_exit pv_rollback([pv, &pv_inserted] { if (pv_inserted) pv->pop_back(); }); if (pv) { pv->push_back(value); // may throw pv_inserted = true; // unblock the scope guard } auto [it, inserted] = set.insert(value); // may throw // Initially active scope guard, which we immediately deactivate if no element was inserted std::experimental::scope_exit set_rollback([&set, &it] { set.erase(it); }); if (!inserted) set_rollback.release(); on_inserted(); // may throw } |
Note that Boost.Scope guards don't provide release()
member function and instead provide
set_active()
with a bool
argument, where
release()
is equivalent to set_active(false)
.
This allows for both activating and deactivating the scope guard. The
bool
argument is more convenient
than two distinct methods for activating and deactivating the scope guard
when the indication of whether the scope guard needs to be active is expressed
as a run time value. For example, the insertion into set
above could be written like this:
std::set< int >::iterator it; // Initially create an inactive scope guard to revert the insertion boost::scope::scope_fail set_rollback([&set, &it] { set.erase(it); }, false); // Try insering a new element into the set bool inserted; std::tie(it, inserted) = set.insert(value); // may throw // Activate the scope guard only if the element was inserted set_rollback.set_active(inserted);
![]() |
Note |
---|---|
At least one person during the Boost.Scope review noted
that |
In addition to set_active()
, Boost.Scope scope guards can be queried
whether they are active via the active()
method. This feature has no equivalent
in the TS.
In the Library Fundamentals TS, only scope_success
action is allowed to throw exceptions, while scope_fail
and scope_exit
destructors
are unconditionally marked noexcept
.
Since the TS does not support custom condition functions, scope_fail
can only invoke its action
while an exception is unwinding the stack, so throwing an exception from
the scope guard action would cause the program to terminate. In case of
scope_exit
the justification
is less definitive, as this scope guard may invoke its action when no exception
is in flight. However, the situation when the action is called due to an
exception is still possible, and the TS takes a conservative approach by
prohibiting the action to throw exceptions.
Boost.Scope takes a different approach. Since all three scope guards support
custom condition functions, each of them may invoke its action whether
or not an exception is propagating through the stack. The notion of "failure"
is no longer equivalent to "exception", so even for a scope_fail
scope guard with
a custom failure condition it may make sense to allow throwing exceptions.
Therefore, for all scope guards provided by Boost.Scope, their destructors
are noexcept
only if their
action and condition functions are both marked noexcept
.
It is user's responsibity to ensure that the scope guard actions and conditions
do not throw exceptions when another exception is in flight. The user may
even perform a runtime check with std::uncaught_exceptions
,
if needed. If the user is willing to enforce the non-throwing guarantee
for his scope guards, he is able to do this by marking his action functions
noexcept
:
boost::scope::scope_exit guard([&]() noexcept { /* I promise I won't throw */ });
While this allows more scope guards to throw exceptions, it makes no difference
in behavior when the action throws during an exception propagation. Whether
the scope guard's destructor is marked noexcept
or not, the program will terminate either way in this case.
This Boost.Scope feature cannot be emulated with TS scope guards (other
than when the use case fits scope_success
).