2
0
mirror of https://github.com/boostorg/scope.git synced 2026-01-19 16:52:08 +00:00
Files
scope/doc/scope_guards.qbk
2025-05-13 01:18:07 +03:00

1366 lines
50 KiB
Plaintext

[/
/ Copyright 2023-2024 Andrey Semashev
/
/ Distributed under the Boost Software License, Version 1.0.
/ (See accompanying file LICENSE_1_0.txt or copy at
/ https://www.boost.org/LICENSE_1_0.txt)
/
/ This document is a part of Boost.Scope library documentation.
/]
[section:scope_guards Scope guards]
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:
* a user-defined class with a public `operator()` taking no arguments, or
* an lvalue reference to such class, or
* an lvalue reference or pointer to a function taking no arguments.
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
[link scope.scope_guards.condition_functions 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 Scope guard comparison
[[Feature] [[class_scope_scope_exit]] [[class_scope_scope_success]] [[class_scope_scope_fail]] [[class_scope_defer_guard]]]
[[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 [class_scope_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 [class_scope_scope_success]
is to invoke its action when the scope is left normally (i.e. not because of a failure) and for [class_scope_scope_fail] - to
invoke its action to handle errors, including exceptions. For this reason, action functions that are used with [class_scope_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 [class_scope_scope_exit] and [class_scope_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.
[section:conditional Conditional scope guards: `scope_exit`, `scope_success` and `scope_fail`]
#include <``[boost_scope_scope_exit_hpp]``>
#include <``[boost_scope_scope_success_hpp]``>
#include <``[boost_scope_scope_fail_hpp]``>
The [class_scope_scope_exit], [class_scope_scope_success], and [class_scope_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, [class_scope_scope_success] will
invoke its action if it is destroyed normally, [class_scope_scope_fail] - if it is destroyed due to an exception being thrown.
[class_scope_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 [link scope.scope_guards.condition_functions 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:
* [class_scope_scope_exit] is useful for general cleanup tasks, which need to be performed regardless of success or failure of the
enclosing function.
* [class_scope_scope_success] can be used for common finalization code that needs to be performed on successful completion of the
function.
* [class_scope_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 [@https://en.cppreference.com/w/cpp/language/class_template_argument_deduction 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.
[endsect]
[section:condition_functions Scope guard condition functions]
#include <``[boost_scope_exception_checker_hpp]``>
#include <``[boost_scope_error_code_checker_hpp]``>
As discussed before, [class_scope_scope_exit], [class_scope_scope_success] and [class_scope_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:
* the type of a condition function object must be one of:
* a user-defined class with a public `operator()` taking no arguments, or
* an lvalue reference to such class, or
* an lvalue reference or pointer to a function taking no arguments;
* invoking the condition function object must return a value contextually convertible to `bool`, and
* invoking the condition function object must not throw exceptions.
For all the three scope guards, the condition function object is optional. If not specified, [class_scope_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 [class_scope_scope_success] and [class_scope_scope_fail], the [class_scope_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, [class_scope_exception_checker] is intended for a specific use case with scope guards created on the stack. It is incompatible
with C++20 coroutines and similar facilities (e.g. fibers and userspace context switching), where the thread of execution may be suspended after
the predicate captures the number of uncaught exceptions and then resumed in a different context, where the number of uncaught exceptions
has changed. Similarly, it is incompatible with usage patterns where the predicate is cached after construction and is invoked after the
thread has left the scope where the predicate was constructed (e.g. when the predicate or the associated scope guard is stored as a class data
member or a namespace-scope variable).]
[class_scope_scope_success] invokes its action when the condition returns `false` (i.e. when the failure is not detected) and
[class_scope_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 [class_scope_scope_exit] behavior (with a user-specified condition function object) is similar to [class_scope_scope_fail]
and opposite to [class_scope_scope_success] in terms of their usage of condition function objects. The main difference is a semantic one:
[class_scope_scope_exit] does not have a success/failure connotation and may be used with arbitrary condition functions. On the other hand,
[class_scope_scope_success] and [class_scope_scope_fail] typically correspond to their respective intended use cases, and the default condition
function makes them equivalent to the scope guards defined in [@https://cplusplus.github.io/fundamentals-ts/v3.html#scope.syn
`<experimental/scope>`].
It is possible to emulate each of the scope guards described in this section by using [class_scope_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 Comparison of `scope_fail` and `scope_exit`
[[`scope_fail`][`scope_exit` with a condition][`scope_exit` with embedded condition][`scope_exit` with manual deactivation]]
[[
```
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 [class_scope_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 [class_scope_scope_fail] is improved code readability. For a reader, [class_scope_scope_fail] immediately indicates
an error handler, whereas [class_scope_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
[class_scope_scope_success]/[class_scope_scope_fail], especially when paired with [class_scope_exception_checker]. As noted above,
[class_scope_exception_checker] is incompatible with coroutines and similar facilities, so using a manually managed [class_scope_scope_exit]
can be a solution. Another consideration is performance, specifically in relation with [class_scope_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 [class_scope_scope_exit] with manual deactivation may be a reasonable choice.
[class_scope_scope_exit], [class_scope_scope_success] and [class_scope_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 [class_scope_exception_checker], the library also provides [class_scope_error_code_checker] that can be used as a condition
function object. On construction, [class_scope_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 [class_scope_error_code_checker] predicate.
An object `ec` can be used as an error code object if:
* the expression `!ec` is valid, never throws, and returns a value contextually convertible to `bool`, and
* the expression `!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 [class_scope_error_code_checker] results in a value equivalent to `!!ec`. This makes
[class_scope_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)
};
// ...
}
[endsect]
[section:unconditional Unconditional scope guard: `defer_guard`]
#include <``[boost_scope_defer_hpp]``>
The [class_scope_defer_guard] scope guard is similar to [class_scope_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 [class_scope_defer_guard] is a more lightweight version of [class_scope_scope_exit], similar to how `std::lock_guard` is a more
lightweight version of `std::unique_lock`.]
Since [class_scope_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 [class_scope_defer_guard] object. 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 `BOOST_SCOPE_DEFER` requires support for C++17 [@https://en.cppreference.com/w/cpp/language/class_template_argument_deduction CTAD].
The [class_scope_defer_guard] class itself is compatible with C++11, but given that there is no factory function for it, C++17 support is
very much desired.]
As you can see, `BOOST_SCOPE_DEFER` offers a few syntax improvements over the other scope guard declarations:
* The declaration does not name a scope guard variable, meaning one does not need to invent one and there is no possibility to accidentally
omit one or clash with other variables.
* The declaration is generally shorter to type and easier to spot.
* There are no extra parenthesis or curly braces around the function object.
However, it should be noted that the use of the `BOOST_SCOPE_DEFER` macro is entirely optional. Users are free to use [class_scope_defer_guard]
directly.
boost::scope::defer_guard guard{[]
{
std::cout << "Hello world!" << std::endl;
}};
[endsect]
[section:capture_by_reference_caveats Caveats of capturing by reference]
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;
}
[endsect]
[section:runtime_defined Setting up scope exit actions at run time]
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);
[endsect]
[section:comparison_with_boost_scope_exit Comparison with Boost.ScopeExit library]
__boost_scope_exit__ 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 __boost_scope_exit__ and Boost.Scope comparison
[[Feature] [__boost_scope_exit__] [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 `BOOST_SCOPE_EXIT_END` is used in this case. See below for `BOOST_SCOPE_EXIT_ALL` caveats.
]
[
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 `this`.
]
[
Yes, with a special `this_` keyword.
```
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 `this` is also possible using `BOOST_SCOPE_EXIT_ALL` (with the caveats described below).
```
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 `this` or use lambda capture initializers (requires C++14) instead.
```
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(&param)
{
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 [&param]
{
std::cout << "Param: " << param << std::endl;
};
}
```
]]
[[
Support for variadic templates and argument packs.
]
[
Yes, using the `BOOST_SCOPE_EXIT_ALL` macro (see the caveats below).
```
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 [class_scope_scope_exit], [class_scope_scope_success] and [class_scope_scope_fail]. This includes the ability to delay activation
of the initially inactive scope guards.
```
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 [class_scope_scope_exit], [class_scope_scope_success] and [class_scope_scope_fail].
```
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, [class_scope_exception_checker] and [class_scope_error_code_checker].
```
// 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, [class_scope_scope_exit], [class_scope_scope_success] and [class_scope_scope_fail] are
move-constructible and can be returned from functions.
]]
[[
Anonymous scope guards.
]
[
Yes.
]
[
Yes, using the `BOOST_SCOPE_DEFER` macro.
]]
[[
Macro-less usage.
]
[
No.
]
[
Yes.
]]
]
In the table above, you may notice that a significant amount of feature parity between __boost_scope_exit__ 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_scope_exit__ 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):
* Since it relies on C++11 lambda functions, it is not available in C++03 mode.
* __boost_scope_exit__ scope guards that are implemented using C++11 lambdas store the lambda function in a `boost::function` object
internally. This means that creating the scope guard may require dynamic memory allocation and can fail with an exception. __boost_scope_exit__
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_scope_exit__ 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.
[endsect]
[section:comparison_with_library_fundamentals_ts Comparison with scope guards defined in C++ Extensions for Library Fundamentals]
The [@https://cplusplus.github.io/fundamentals-ts/v3.html#scopeguard.exit 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.
[section:condition_functions Condition functions]
All three of the [class_scope_scope_exit], [class_scope_scope_success] and [class_scope_scope_fail] scope guards provided by Boost.Scope
support an optional [link scope.scope_guards.condition_functions 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 [class_scope_scope_fail], the equivalent TS code could look like this:
[table 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.
[endsect]
[section:activation Flexible activation and deactivation of scope guards]
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 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 [@https://lists.boost.org/Archives/boost/2023/11/255424.php noted] that
`release()` is a poor name in the context of scope guards. This, and the fact that a method with a `bool` argument is more flexible
and provides the same functionality, is why Boost.Scope does not provide `release()`.]
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.
[endsect]
[section:relaxed_dtor_noexcept Relaxed `noexcept` specification on destructors]
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 [class_scope_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`).
[endsect]
[endsect]
[endsect]