Re: [boost] [scope] Scope review starts on November 26th
Thanks for proposing this library. Scope guards are a natural and much
needed addition to Boost. The following is my review of the proposed
Boost.Scope.
The first thing I looked for in this library was a convenience SCOPE_EXIT {
... }; macro that would provide the functionality in the form of a control
flow construct. I've managed to quickly find it, but it wasn't exactly what
I was expecting.
BOOST_SCOPE_FINAL []
{
std::cout << "Hello world!" << std::endl;
};
I found it rather odd that the capture list is not included in the macro. I
looked a bit further into the documentation to find this:
BOOST_SCOPE_FINAL std::ref(cleanup_func);
Is this use case the reason why the capture list is required? It seems like
a very rare one that shouldn't pessimize the interface for 99% of use
cases. Performance shouldn't be a concern because the compiler sees all
this code and inlines it anyway. In my view, this just makes the interface
worse and less pleasant to use.
scope_success doesn't seem terribly useful. I have yet to see a legitimate
use case of this type. I haven't found a motivating example in the
documentation. There should either be one, or this type should be removed
from the library.
There is no BOOST_SCOPE_FAIL macro. Why is it missing?
I found the scope_fail example pretty bad:
class collection
{
std::set< std::shared_ptr< object > > objects;
public:
void add_object(std::shared_ptr< object > const& obj)
{
// Create a deactivated scope guard initially
std::set< std::shared_ptr< object > >::iterator it;
boost::scope::scope_fail rollback_guard{[&, this]
{
objects.erase(it);
},
false};
bool inserted;
std::tie(it, inserted) = objects.insert(obj);
if (inserted)
{
// Activate rollback guard
rollback_guard.set_active(true);
}
obj->on_added_to_collection(*this);
}
};
It could be simply rewritten to not use set_active (which should be
discouraged, because it makes code less declarative and harder to follow):
class collection
{
std::set< std::shared_ptr< object > > objects;
public:
void add_object(std::shared_ptr< object > const& obj)
{
// Create a deactivated scope guard initially
std::set< std::shared_ptr< object > >::iterator it;
bool inserted;
std::tie(it, inserted) = objects.insert(obj);
if (inserted)
{
boost::scope::scope_fail rollback_guard{[&, this]
{
objects.erase(it);
}};
obj->on_added_to_collection(*this);
}
}
};
Could be made even cleaner by using an early return: if (!inserted) return;
All destructor declarations in the documentation lack noexcept
specification, which by default means that they're noexcept, but they're
actually not if you look at the Throws section, or the implementation.
Also, the question presents itself: why are these destructors allowed to
throw? I feel like it is common knowledge today that destructors should not
throw, and this is heavily frowned upon. I can see the want to "be generic"
here, but I don't see why a library should entertain broken code.
I also noticed that certain scope guards are move constructible. Why? I
cannot imagine myself ever approving of someone moving a scope guard on
code review. Scope guards are to be used exclusively as local variables and
don't need to be moved. This just opens up room for misuse.
I don't see any compelling use cases for scope_exit::set_active in the
documentation. The only example I find motivating is this one:
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);
}
This could as well have used rollback_guard.release();. This matches my
personal experience: only dismiss/release/cancel is necessary for real use
cases ("commiting" something and thus disabling the "rollback" action).
absl::Cleanup for example, supports only Cancel(). There is value in
preventing users from shooting themselves in the foot.
I don't like the name scope_exit::release personally. I feel like "dismiss"
or "cancel" would be much better. I know that std::experimental::scope_exit
calls it "release", but we shouldn't appeal to false authority.
I did not like the locked_write_string example. Reading this code locally:
// Lock the file
while (flock(fd, LOCK_EX) < 0)
{
err = errno;
if (err != EINTR)
return;
}
it's hard to get what's happening, why we're setting err and how big of an
impact that has on function behavior. A better way to write this code would
be to simply extract this piece into its own function:
err = errno;
if (err != EINTR)
return;
together with the ec = std::error_code(err, std::generic_category()), which
doesn't require a scope guard.
fd_deleter and unique_fd provide some nice handling for EINTR, which wasn't
required but I think it's nice to have and it's good that it's a part of
this library!
I think make_unique_resource_checked approaches the problem from the wrong
direction. This code:
// Create a unique resource for a file
auto file = boost::scope::make_unique_resource_checked(
open(filename.c_str(), O_CREAT | O_WRONLY)
-1,
fd_deleter());
if (!file.allocated())
{
int err = errno;
throw std::system_error(err, std::generic_category(), "Failed to
open file " + filename);
}
could be this:
// Create a unique resource for a file
int file = open(filename.c_str(), O_CREAT | O_WRONLY);
boost::scope::scope_final close_file(fd_deleter{});
if (file != -1)
{
int err = errno;
throw std::system_error(err, std::generic_category(), "Failed to
open file " + filename);
}
This has the advantage of being able to use `file` directly for the rest of
the code instead of `file.get()`.
I think a similar utility would be more useful in form of a class template,
for example:
resource
On 11/29/23 13:14, Janko Dedic via Boost wrote:
The first thing I looked for in this library was a convenience SCOPE_EXIT { ... }; macro that would provide the functionality in the form of a control flow construct. I've managed to quickly find it, but it wasn't exactly what I was expecting.
BOOST_SCOPE_FINAL [] { std::cout << "Hello world!" << std::endl; };
I found it rather odd that the capture list is not included in the macro. I looked a bit further into the documentation to find this:
BOOST_SCOPE_FINAL std::ref(cleanup_func);
Is this use case the reason why the capture list is required? It seems like a very rare one that shouldn't pessimize the interface for 99% of use cases. Performance shouldn't be a concern because the compiler sees all this code and inlines it anyway. In my view, this just makes the interface worse and less pleasant to use.
You can think of BOOST_SCOPE_FINAL as a keyword that begins the declaration of a scope guard. The scope guard action then is what follows this keyword, and indeed it is not limited to just lambda functions. I find this a useful ability, as this allows to reuse code in user-defined function objects. In the example you quoted above, cleanup_func could be written once and then used in multiple scope guards without having to wrap each of them in a useless lambda. I don't see the current definition of BOOST_SCOPE_FINAL as a deficiency of the interface compared to passing the capture list in the macro arguments. The latter doesn't offer any advantages compared to the current definition, and in fact is more limiting as it only allows for lambda functions for the scope guard action. Besides, note that the lambda definition syntax has evolved over time and may now include additional elements such as template parameters, noexcept and mutable qualifiers and the trailing return type. There may be new syntax additions in the future, which could be incompatible with the macro definition. Although some of the syntax elements are useless for the purpose of scope guards, at least noexcept and mutable are meaningful and would look awkward if BOOST_SCOPE_FINAL accepted the capture list in its parameters.
scope_success doesn't seem terribly useful. I have yet to see a legitimate use case of this type. I haven't found a motivating example in the documentation. There should either be one, or this type should be removed from the library.
scope_success isn't as widely useful as e.g. scope_fail, but you do occasionally want it to schedule some commit steps when the enclosing function succeeds, if there are multiple exit points and a lot of the function-local state that is difficult or inconvenient to pass to a separate function. You can sometimes work around it with a goto, and scope_success is just another, possibly cleaner way to do this. Also, and this pertains to some other questions you asked in your review, Boost.Scope tries to provide components that are defined in the Library Fundamentals TS with a compatible interface. scope_success is defined in the TS, so Boost.Scope also provides it.
There is no BOOST_SCOPE_FAIL macro. Why is it missing?
If you mean BOOST_SCOPE_FAIL to be equivalent to BOOST_SCOPE_FINAL but only creating scope_fail then I didn't see enough motivation to add it. scope_exit/fail/success support scope guard cancellation and also a condition function object. This means users these scope guard types often want to interact with the scope guard object (to activate/deactivate it), and also that the scope guard construction may be more elaborate. Note that scope_exit/fail/success constructors are explicit and may accept multiple arguments. This makes them incompatible with the syntax used by BOOST_SCOPE_FAIL.
I found the scope_fail example pretty bad:
class collection { std::set< std::shared_ptr< object > > objects;
public: void add_object(std::shared_ptr< object > const& obj) { // Create a deactivated scope guard initially std::set< std::shared_ptr< object > >::iterator it; boost::scope::scope_fail rollback_guard{[&, this] { objects.erase(it); }, false};
bool inserted; std::tie(it, inserted) = objects.insert(obj); if (inserted) { // Activate rollback guard rollback_guard.set_active(true); }
obj->on_added_to_collection(*this); } };
It could be simply rewritten to not use set_active (which should be discouraged, because it makes code less declarative and harder to follow):
class collection { std::set< std::shared_ptr< object > > objects;
public: void add_object(std::shared_ptr< object > const& obj) { // Create a deactivated scope guard initially std::set< std::shared_ptr< object > >::iterator it;
bool inserted; std::tie(it, inserted) = objects.insert(obj); if (inserted) { boost::scope::scope_fail rollback_guard{[&, this] { objects.erase(it); }}; obj->on_added_to_collection(*this); } } };
Could be made even cleaner by using an early return: if (!inserted) return;
Your rewritten code is not equivalent to the original, as it doesn't call on_added_to_collection if inserted is false. The whole point of the example is to show that the scope guard can be used to *conditionally* invoke its action in case of failure. Some examples in the docs could probably be written differently. Their point is to demonstrate how a feature could be used in a certain very simplified context.
All destructor declarations in the documentation lack noexcept specification, which by default means that they're noexcept, but they're actually not if you look at the Throws section, or the implementation.
Unfortunately, this is how Doxygen works, I can't do anything about it. The noexcept specification is present in the code.
Also, the question presents itself: why are these destructors allowed to throw? I feel like it is common knowledge today that destructors should not throw, and this is heavily frowned upon. I can see the want to "be generic" here, but I don't see why a library should entertain broken code.
It is discouraged to throw from a destructor because it can cause program termination, if the destructor is called during another exception propagation. If your code knows that this can't happen (e.g. in a scope_success, or a different scope guard, where you know in runtime that the action may only throw if there is no other exception in flight), throwing from a destructor is not a problem, as long as the destructor is marked with noexcept(false). Which it is, for scope guards. If you want to enforce that your action never throws, you can declare its operator() as noexcept: BOOST_SCOPE_FINAL []() noexcept { /* I guarantee I won't throw */ };
I also noticed that certain scope guards are move constructible. Why? I cannot imagine myself ever approving of someone moving a scope guard on code review. Scope guards are to be used exclusively as local variables and don't need to be moved. This just opens up room for misuse.
Movability is necessary for the factory functions, at the very least.
I don't see any compelling use cases for scope_exit::set_active in the documentation. The only example I find motivating is this one:
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); }
This could as well have used rollback_guard.release();. This matches my personal experience: only dismiss/release/cancel is necessary for real use cases ("commiting" something and thus disabling the "rollback" action). absl::Cleanup for example, supports only Cancel(). There is value in preventing users from shooting themselves in the foot.
You can call release(), if you prefer, it's equivalent to set_active(false). You want set_active to be able to activate an inactive scope guard. One example of that you have quoted yourself above.
I don't like the name scope_exit::release personally. I feel like "dismiss" or "cancel" would be much better. I know that std::experimental::scope_exit calls it "release", but we shouldn't appeal to false authority.
Yes, I'm not fond of release() either, but I'll still provide it for compatibility with the TS. My preferred alternative is set_active(). And I do specifically prefer set_active() over a pair of methods like activate()/deactivate() because passing a bool is fundamentally more flexible than having two distinct methods. You can receive that bool from elsewhere and just forward to the method instead of testing it yourself. Like in the example above, you could have written it like this: void add_object(std::shared_ptr< object > const& obj) { // Create a deactivated scope guard initially std::set< std::shared_ptr< object > >::iterator it; boost::scope::scope_fail rollback_guard{[&, this] { objects.erase(it); }, false}; bool inserted; std::tie(it, inserted) = objects.insert(obj); // Activate rollback guard, if needed rollback_guard.set_active(inserted); obj->on_added_to_collection(*this); } which wouldn't be as simple with two different methods.
I did not like the locked_write_string example. Reading this code locally:
// Lock the file while (flock(fd, LOCK_EX) < 0) { err = errno; if (err != EINTR) return; }
it's hard to get what's happening, why we're setting err and how big of an impact that has on function behavior. A better way to write this code would be to simply extract this piece into its own function:
err = errno; if (err != EINTR) return;
together with the ec = std::error_code(err, std::generic_category()), which doesn't require a scope guard.
Sorry, I don't understand your suggestion. How would this help with other return points of the function?
I think make_unique_resource_checked approaches the problem from the wrong direction. This code:
// Create a unique resource for a file auto file = boost::scope::make_unique_resource_checked( open(filename.c_str(), O_CREAT | O_WRONLY) -1, fd_deleter()); if (!file.allocated()) { int err = errno; throw std::system_error(err, std::generic_category(), "Failed to open file " + filename); }
could be this:
// Create a unique resource for a file int file = open(filename.c_str(), O_CREAT | O_WRONLY); boost::scope::scope_final close_file(fd_deleter{}); if (file != -1) { int err = errno; throw std::system_error(err, std::generic_category(), "Failed to open file " + filename); }
This has the advantage of being able to use `file` directly for the rest of the code instead of `file.get()`.
The rewritten piece of code is incorrect and is exactly why make_unique_resource_checked exists. Besides not binding the fd to invoke fd_deleter on, it creates the scope guard before checking the fd for validity, which means it will call close(-1) if open() fails. You could modify the code to fix those issues, but you're still missing the point of unique_resource, which is to bundle the resource with its deleter. Similarly to how you bundle a pointer with its deleter by using a unique_ptr. As I said earlier in the discussion, I'm also not fond of make_unique_resource_checked as I think the necessity to use this helper is different from most other facilities and unnecessarily verbose. I think, resource traits that are supported by Boost.Scope's unique_resource are a better solution, as they not only eliminate the need to use this helper, but they also allow for a more optimal implementation of unique_resource. But I still provide make_unique_resource_checked for compatibility with the TS.
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle; This would be very useful for concisely implementing RAII wrappers over C handles (you don't have to manually write destructors, move constructors and move assignment operators).
This would require C++17, if I'm not mistaken (or when were the auto non-type template parameters introduced?). I don't think it offers much advantage compared to the current unique_resource with resource traits.
On 11/29/23 18:56, Andrey Semashev wrote:
On 11/29/23 13:14, Janko Dedic via Boost wrote:
There is no BOOST_SCOPE_FAIL macro. Why is it missing?
If you mean BOOST_SCOPE_FAIL to be equivalent to BOOST_SCOPE_FINAL but only creating scope_fail then I didn't see enough motivation to add it. scope_exit/fail/success support scope guard cancellation and also a condition function object. This means users these scope guard types often want to interact with the scope guard object (to activate/deactivate it), and also that the scope guard construction may be more elaborate.
Note that scope_exit/fail/success constructors are explicit and may accept multiple arguments. This makes them incompatible with the syntax used by BOOST_SCOPE_FAIL.
The last sentence should end with BOOST_SCOPE_FINAL.
Andrey Semashev wrote:
You can think of BOOST_SCOPE_FINAL as a keyword that begins the declaration of a scope guard. The scope guard action then is what follows this keyword, and indeed it is not limited to just lambda functions.
Other languages call this "defer". E.g. defer fclose( fp ); or defer { fclose( fp ); }
On Wed, Nov 29, 2023 at 4:56 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I don't see the current definition of BOOST_SCOPE_FINAL as a deficiency of the interface compared to passing the capture list in the macro arguments. The latter doesn't offer any advantages compared to the current definition, and in fact is more limiting as it only allows for lambda functions for the scope guard action.
It offers better syntax.
Besides, note that the lambda definition syntax has evolved over time and may now include additional elements such as template parameters, noexcept and mutable qualifiers and the trailing return type. There may be new syntax additions in the future, which could be incompatible with the macro definition. Although some of the syntax elements are useless for the purpose of scope guards, at least noexcept and mutable are meaningful and would look awkward if BOOST_SCOPE_FINAL accepted the capture list in its parameters.
I only suggested adding [&] to the macro, or adding a macro that includes the [&].
scope_success isn't as widely useful as e.g. scope_fail, but you do occasionally want it to schedule some commit steps when the enclosing function succeeds, if there are multiple exit points and a lot of the function-local state that is difficult or inconvenient to pass to a separate function. You can sometimes work around it with a goto, and scope_success is just another, possibly cleaner way to do this.
Do you have a code example? I'm really curious about this use case.
Also, and this pertains to some other questions you asked in your review, Boost.Scope tries to provide components that are defined in the Library Fundamentals TS with a compatible interface. scope_success is defined in the TS, so Boost.Scope also provides it.
I very much dislike this reasoning. Why should Boost copy std even when std is making a mistake? Boost should rely on its review process to judge libraries. "std does the same" is not a valid argument (especially in the light of recent discussions on the list about the quality of the WG21 "review process"). I can understand if it's the stated goal of the library to copy std, but (1) scope guards are not even standardized yet and (2) for copying std there is Boost.Compat. At the end of the day, users are robbed of better libraries because "std has already done it this way."
Also, the question presents itself: why are these destructors allowed to throw? I feel like it is common knowledge today that destructors should not throw, and this is heavily frowned upon. I can see the want to "be generic" here, but I don't see why a library should entertain broken code.
It is discouraged to throw from a destructor because it can cause program termination, if the destructor is called during another exception propagation. If your code knows that this can't happen (e.g. in a scope_success, or a different scope guard, where you know in runtime that the action may only throw if there is no other exception in flight), throwing from a destructor is not a problem, as long as the destructor is marked with noexcept(false). Which it is, for scope guards.
Even when you know that no other exception is in flight (scope_success), I would consider code that throws from the scope guard highly dubious.
If you want to enforce that your action never throws, you can declare its operator() as noexcept:
BOOST_SCOPE_FINAL []() noexcept { /* I guarantee I won't throw */ };
Does anyone really want to write code like this?
I also noticed that certain scope guards are move constructible. Why? I cannot imagine myself ever approving of someone moving a scope guard on code review. Scope guards are to be used exclusively as local variables and don't need to be moved. This just opens up room for misuse.
Movability is necessary for the factory functions, at the very least.
I can see why. C++17 fixes this "defect". You can technically rely on reference lifetime extension, but that doesn't look as good. auto&& guard = make_scope_exit(...);
And I do specifically prefer set_active() over a pair of methods like activate()/deactivate() because passing a bool is fundamentally more flexible than having two distinct methods. You can receive that bool from elsewhere and just forward to the method instead of testing it yourself. Like in the example above, you could have written it like this:
void add_object(std::shared_ptr< object > const& obj) { // Create a deactivated scope guard initially std::set< std::shared_ptr< object > >::iterator it; boost::scope::scope_fail rollback_guard{[&, this] { objects.erase(it); }, false};
bool inserted; std::tie(it, inserted) = objects.insert(obj);
// Activate rollback guard, if needed rollback_guard.set_active(inserted);
obj->on_added_to_collection(*this); }
which wouldn't be as simple with two different methods.
This looks much better honestly. Why isn't this variant used for the example?
I did not like the locked_write_string example. Reading this code locally:
// Lock the file while (flock(fd, LOCK_EX) < 0) { err = errno; if (err != EINTR) return; }
it's hard to get what's happening, why we're setting err and how big of an impact that has on function behavior. A better way to write this code would be to simply extract this piece into its own function:
err = errno; if (err != EINTR) return;
together with the ec = std::error_code(err, std::generic_category()), which doesn't require a scope guard.
Sorry, I don't understand your suggestion. How would this help with other return points of the function?
It helps with all the return points in the example function.
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle; This would be very useful for concisely implementing RAII wrappers over C handles (you don't have to manually write destructors, move constructors and move assignment operators).
This would require C++17, if I'm not mistaken (or when were the auto non-type template parameters introduced?). I don't think it offers much advantage compared to the current unique_resource with resource traits.
The advantage is doing the same thing in 1 line instead of 20 lines of code, at which point writing the destructor and moves manually has a better ROI than implementing traits classes.
On 11/30/23 00:35, Janko Dedic via Boost wrote:
On Wed, Nov 29, 2023 at 4:56 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I don't see the current definition of BOOST_SCOPE_FINAL as a deficiency of the interface compared to passing the capture list in the macro arguments. The latter doesn't offer any advantages compared to the current definition, and in fact is more limiting as it only allows for lambda functions for the scope guard action.
It offers better syntax.
Better in what way, other than your personal preference? Can you describe objective advantages of your proposed syntax and why the current BOOST_SCOPE_FINAL doesn't work for you?
Besides, note that the lambda definition syntax has evolved over time and may now include additional elements such as template parameters, noexcept and mutable qualifiers and the trailing return type. There may be new syntax additions in the future, which could be incompatible with the macro definition. Although some of the syntax elements are useless for the purpose of scope guards, at least noexcept and mutable are meaningful and would look awkward if BOOST_SCOPE_FINAL accepted the capture list in its parameters.
I only suggested adding [&] to the macro, or adding a macro that includes the [&].
Sorry, but no. That would limit user's options for no reason, and binding variables by value is not uncommon. Users of Boost.Scope should have the full range of function object definition ways that is permitted by the language.
scope_success isn't as widely useful as e.g. scope_fail, but you do occasionally want it to schedule some commit steps when the enclosing function succeeds, if there are multiple exit points and a lot of the function-local state that is difficult or inconvenient to pass to a separate function. You can sometimes work around it with a goto, and scope_success is just another, possibly cleaner way to do this.
Do you have a code example? I'm really curious about this use case.
I had something like this in mind: void foo() { auto private_data = do_something(); scope_success commit_guard{[&private_data] { commit_data(private_data); }}; if (condition1) { do_something_on_condition1(private_data); return; } if (condition2) { do_something_on_condition2(private_data); return; } bool succeeded = complete_processing(private_data); if (!succeeded) { commit_guard.set_active(false); return; } } In the above pseudocode, private_data may be any number of data items of various types, including those defined locally in foo(), and function calls like commit_data, do_something_on_condition1, etc. would be pieces of code working on that and other data.
Also, and this pertains to some other questions you asked in your review, Boost.Scope tries to provide components that are defined in the Library Fundamentals TS with a compatible interface. scope_success is defined in the TS, so Boost.Scope also provides it.
I very much dislike this reasoning. Why should Boost copy std even when std is making a mistake? Boost should rely on its review process to judge libraries. "std does the same" is not a valid argument (especially in the light of recent discussions on the list about the quality of the WG21 "review process"). I can understand if it's the stated goal of the library to copy std, but (1) scope guards are not even standardized yet and (2) for copying std there is Boost.Compat.
At the end of the day, users are robbed of better libraries because "std has already done it this way."
If you read earlier commits in this review, you'll see that there are people who would prefer Boost.Scope to *exactly* reflect the TS with no extensions or deviations. Although I disagree with that point of view, I still think there is value in providing interface that is *compatible* with the TS, if possible, so that users, at least potentially, have the option to switch between Boost.Scope and std, should they need to for whatever reason. Another reason is that this compatibility reduces the element of surprise and makes learning the library (whether Boost.Scope or std) easier. If this review shows that no such compatibility is desired by the community, I can remove some elements of the library that are deemed unwanted or detrimental to its quality.
If you want to enforce that your action never throws, you can declare its operator() as noexcept:
BOOST_SCOPE_FINAL []() noexcept { /* I guarantee I won't throw */ };
Does anyone really want to write code like this?
I see nothing wrong with this code. I actually wrote code like this, where the no-throw guarantee was important.
Movability is necessary for the factory functions, at the very least.
I can see why. C++17 fixes this "defect". You can technically rely on reference lifetime extension, but that doesn't look as good.
auto&& guard = make_scope_exit(...);
Reference lifetime extension has nothing to do with it. Without RVO, in order to return the scope guard from the factory function by value, its copy or move constructor needs to be invoked. Besides, TS scope guards are move-constructible.
And I do specifically prefer set_active() over a pair of methods like activate()/deactivate() because passing a bool is fundamentally more flexible than having two distinct methods. You can receive that bool from elsewhere and just forward to the method instead of testing it yourself. Like in the example above, you could have written it like this:
void add_object(std::shared_ptr< object > const& obj) { // Create a deactivated scope guard initially std::set< std::shared_ptr< object > >::iterator it; boost::scope::scope_fail rollback_guard{[&, this] { objects.erase(it); }, false};
bool inserted; std::tie(it, inserted) = objects.insert(obj);
// Activate rollback guard, if needed rollback_guard.set_active(inserted);
obj->on_added_to_collection(*this); }
which wouldn't be as simple with two different methods.
This looks much better honestly. Why isn't this variant used for the example?
I can update the example in the docs.
I did not like the locked_write_string example. Reading this code locally:
// Lock the file while (flock(fd, LOCK_EX) < 0) { err = errno; if (err != EINTR) return; }
it's hard to get what's happening, why we're setting err and how big of an impact that has on function behavior. A better way to write this code would be to simply extract this piece into its own function:
err = errno; if (err != EINTR) return;
together with the ec = std::error_code(err, std::generic_category()), which doesn't require a scope guard.
Sorry, I don't understand your suggestion. How would this help with other return points of the function?
It helps with all the return points in the example function.
I don't see how. Can you provide a code example? Note that the checks for EINTR are made in different contexts. Some of them cause return, other - restart loop iteration, third - affect error_code initialization.
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle; This would be very useful for concisely implementing RAII wrappers over C handles (you don't have to manually write destructors, move constructors and move assignment operators).
This would require C++17, if I'm not mistaken (or when were the auto non-type template parameters introduced?). I don't think it offers much advantage compared to the current unique_resource with resource traits.
The advantage is doing the same thing in 1 line instead of 20 lines of code, at which point writing the destructor and moves manually has a better ROI than implementing traits classes.
This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
Andrey Semashev wrote:
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle;
That's
template
This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
It doesn't. The two operations required from R are construction from Def and comparing to Def. https://godbolt.org/z/h7z3qGzvs Note that the type of Def doesn't have to be R.
On 11/30/23 19:02, Peter Dimov via Boost wrote:
Andrey Semashev wrote:
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle; That's
template
class resource; This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
It doesn't. The two operations required from R are construction from Def and comparing to Def.
https://godbolt.org/z/h7z3qGzvs
Note that the type of Def doesn't have to be R.
What I meant is something like this won't work: https://godbolt.org/z/hczoP4WMx And if you're suggesting to define some magic placeholder type that can be used as Def and automatically converts to R then that looks like a more contrived usage to me than just defining resource traits.
Andrey Semashev wrote:
That's
template
class resource; This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
It doesn't. The two operations required from R are construction from Def and comparing to Def.
https://godbolt.org/z/h7z3qGzvs
Note that the type of Def doesn't have to be R.
What I meant is something like this won't work:
That's a common objection but the fix is simple: https://godbolt.org/z/KTM9eb5r8
And if you're suggesting to define some magic placeholder type that can be used as Def and automatically converts to R then that looks like a more contrived usage to me than just defining resource traits.
That's typically not that hard either, although for std::string specifically it doesn't work very well because operator!= is templated. So we're hitting worst case: constexpr struct invalid_t { operator std::string() const noexcept { return "invalid"; } friend bool operator!=( std::string const& s, invalid_t ) { return s != "invalid"; } } invalid; Most of the time it's much simpler, though.
On 11/30/23 22:37, Peter Dimov via Boost wrote:
Andrey Semashev wrote:
That's
template
class resource; This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
It doesn't. The two operations required from R are construction from Def and comparing to Def.
https://godbolt.org/z/h7z3qGzvs
Note that the type of Def doesn't have to be R.
What I meant is something like this won't work:
That's a common objection but the fix is simple:
https://godbolt.org/z/KTM9eb5r8
And if you're suggesting to define some magic placeholder type that can be used as Def and automatically converts to R then that looks like a more contrived usage to me than just defining resource traits.
That's typically not that hard either, although for std::string specifically it doesn't work very well because operator!= is templated. So we're hitting worst case:
constexpr struct invalid_t { operator std::string() const noexcept { return "invalid"; }
Not noexcept, std::string constructor may throw.
friend bool operator!=( std::string const& s, invalid_t ) { return s != "invalid"; } } invalid;
Most of the time it's much simpler, though.
Just for reference, here's how the current implementation would look: struct string_resource_traits { static std::string const& make_default() noexcept { return g_invalid; } static bool is_allocated(std::string const& res) noexcept { return res != g_invalid; } static const std::string g_invalid; }; const std::string string_resource_traits::g_invalid{ "invalid" }; struct string_deleter { void operator()(std::string const& res) noexcept { close(res); } }; using fd = unique_resource< std::string, string_deleter, string_resource_traits
;
Andrey Semashev wrote:
constexpr struct invalid_t { operator std::string() const noexcept { return "invalid"; }
Not noexcept, std::string constructor may throw.
Yeah, make that operator char const* () const noexcept { return "invalid"; }
On Thu, Nov 30, 2023 at 4:52 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
On 11/30/23 00:35, Janko Dedic via Boost wrote:
On Wed, Nov 29, 2023 at 4:56 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I don't see the current definition of BOOST_SCOPE_FINAL as a deficiency of the interface compared to passing the capture list in the macro arguments. The latter doesn't offer any advantages compared to the current definition, and in fact is more limiting as it only allows for lambda functions for the scope guard action.
It offers better syntax.
Better in what way, other than your personal preference? Can you describe objective advantages of your proposed syntax and why the current BOOST_SCOPE_FINAL doesn't work for you?
Better because the user is not forced to type the capture list everywhere. Ideally we would not even require a semicolon at the end, but we can't get rid of that. The ideal is a core language construct like Go's defer or D's scope(exit).
Besides, note that the lambda definition syntax has evolved over time and may now include additional elements such as template parameters, noexcept and mutable qualifiers and the trailing return type. There may be new syntax additions in the future, which could be incompatible with the macro definition. Although some of the syntax elements are useless for the purpose of scope guards, at least noexcept and mutable are meaningful and would look awkward if BOOST_SCOPE_FINAL accepted the capture list in its parameters.
I only suggested adding [&] to the macro, or adding a macro that includes the [&].
Sorry, but no. That would limit user's options for no reason, and binding variables by value is not uncommon. Users of Boost.Scope should have the full range of function object definition ways that is permitted by the language.
And why is having these options useful? Note that the user can still use the constructor or the factory function directly to fully customize the behavior (if they want to for whatever reason). I can understand your motivation behind this macro (declaring an anonymous scope guard variable only), but in 99% of cases I would personally end up using BOOST_SCOPE_FINAL [&], and I don't understand why the library wouldn't provide such a macro (Alexandrescu's original SCOPE_EXIT works like this).
If you want to enforce that your action never throws, you can declare its operator() as noexcept:
BOOST_SCOPE_FINAL []() noexcept { /* I guarantee I won't throw */ };
Does anyone really want to write code like this?
I see nothing wrong with this code. I actually wrote code like this, where the no-throw guarantee was important.
It just looks like a weird macro incantation to me personally. SCOPE_EXIT { run(); }; at least tried to model a control flow construct, even though there's the unfortunate semicolon at the end. I strive to have the least amount of clever macros possible in my code.
Movability is necessary for the factory functions, at the very least.
I can see why. C++17 fixes this "defect". You can technically rely on reference lifetime extension, but that doesn't look as good.
auto&& guard = make_scope_exit(...);
Reference lifetime extension has nothing to do with it. Without RVO, in order to return the scope guard from the factory function by value, its copy or move constructor needs to be invoked.
Move constructor can be private to achieve this. Then, lifetime extension works and the user can't move on their side.
I did not like the locked_write_string example. Reading this code locally:
// Lock the file while (flock(fd, LOCK_EX) < 0) { err = errno; if (err != EINTR) return; }
it's hard to get what's happening, why we're setting err and how big of an impact that has on function behavior. A better way to write this code would be to simply extract this piece into its own function:
err = errno; if (err != EINTR) return;
together with the ec = std::error_code(err, std::generic_category()), which doesn't require a scope guard.
Sorry, I don't understand your suggestion. How would this help with other return points of the function?
It helps with all the return points in the example function.
I don't see how. Can you provide a code example?
Note that the checks for EINTR are made in different contexts. Some of them cause return, other - restart loop iteration, third - affect error_code initialization.
As far as I can tell, they're all the same. The last loop can be rewritten to be like the rest.
I think a similar utility would be more useful in form of a class template, for example:
resource
windows_handle; resource sdl_window_handle; This would be very useful for concisely implementing RAII wrappers over C handles (you don't have to manually write destructors, move constructors and move assignment operators).
This would require C++17, if I'm not mistaken (or when were the auto non-type template parameters introduced?). I don't think it offers much advantage compared to the current unique_resource with resource traits.
The advantage is doing the same thing in 1 line instead of 20 lines of code, at which point writing the destructor and moves manually has a better ROI than implementing traits classes.
This would also prohibit resource types with non-constexpr constructors, as well as non-default-constructible resources. I don't like this tradeoff.
It's not necessarily a "trade-off." I personally don't find the full flexibility of unique_resource that useful, but I can understand why you'd want to support it. I'm just saying that the work you have to do by providing custom resource traits basically ends up being more than writing destructors and moves manually. I'm also suggesting that perhaps having a type like I've described that solves 99% of cases in one line might be a valuable addition to the library.
On 11/30/23 23:39, Janko Dedic via Boost wrote:
On Thu, Nov 30, 2023 at 4:52 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
On 11/30/23 00:35, Janko Dedic via Boost wrote:
On Wed, Nov 29, 2023 at 4:56 PM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I don't see the current definition of BOOST_SCOPE_FINAL as a deficiency of the interface compared to passing the capture list in the macro arguments. The latter doesn't offer any advantages compared to the current definition, and in fact is more limiting as it only allows for lambda functions for the scope guard action.
It offers better syntax.
Better in what way, other than your personal preference? Can you describe objective advantages of your proposed syntax and why the current BOOST_SCOPE_FINAL doesn't work for you?
Better because the user is not forced to type the capture list everywhere. Ideally we would not even require a semicolon at the end, but we can't get rid of that. The ideal is a core language construct like Go's defer or D's scope(exit).
If typing [&] is a problem, you could just #define SCOPE_GUARD BOOST_SCOPE_FINAL [&] and use that. Personally, I have no problem with the capture list, and quite often I actually use capture lists more elaborate than [&].
I only suggested adding [&] to the macro, or adding a macro that includes the [&].
Sorry, but no. That would limit user's options for no reason, and binding variables by value is not uncommon. Users of Boost.Scope should have the full range of function object definition ways that is permitted by the language.
And why is having these options useful?
Because I found it useful in my practice. I do regularly capture variables by value and use initializers in capture lists. I already mentioned noexcept and specifying non-lambda function objects as a way to reduce code duplication.
Note that the user can still use the constructor or the factory function directly to fully customize the behavior (if they want to for whatever reason). I can understand your motivation behind this macro (declaring an anonymous scope guard variable only), but in 99% of cases I would personally end up using BOOST_SCOPE_FINAL [&], and I don't understand why the library wouldn't provide such a macro (Alexandrescu's original SCOPE_EXIT works like this).
Because, IMO, the cost of typing [&] does not outweigh the cost of losing other features that I mentioned. If that is not the case for you, you can always define your own macro.
If you want to enforce that your action never throws, you can declare its operator() as noexcept:
BOOST_SCOPE_FINAL []() noexcept { /* I guarantee I won't throw */ };
Does anyone really want to write code like this?
I see nothing wrong with this code. I actually wrote code like this, where the no-throw guarantee was important.
It just looks like a weird macro incantation to me personally. SCOPE_EXIT { run(); }; at least tried to model a control flow construct, even though there's the unfortunate semicolon at the end. I strive to have the least amount of clever macros possible in my code.
I'm not trying to pretend that scope guards are a control flow structure. Because they are not, they are a declaration. At least, in current C++.
Andrey Semashev wrote:
If typing [&] is a problem, you could just
#define SCOPE_GUARD BOOST_SCOPE_FINAL [&]
and use that. Personally, I have no problem with the capture list, and quite often I actually use capture lists more elaborate than [&].
I _think_ that it might be possible to make the capture list optional by using BOOST_SCOPE_FINAL { }; and BOOST_SCOPE_FINAL(&) { }; but I'm not 100% sure. (And maybe rename it to BOOST_SCOPE_DEFER.)
Andrey Semashev wrote:
If typing [&] is a problem, you could just
#define SCOPE_GUARD BOOST_SCOPE_FINAL [&]
and use that. Personally, I have no problem with the capture list, and quite often I actually use capture lists more elaborate than [&].
I _think_ that it might be possible to make the capture list optional by using
BOOST_SCOPE_FINAL { };
and
BOOST_SCOPE_FINAL(&) { };
but I'm not 100% sure.
Scratch that, I can't make it work. :-)
On 12/2/23 17:30, Peter Dimov via Boost wrote:
(And maybe rename it to BOOST_SCOPE_DEFER.)
Would you also propose to rename the scope_final class template? What name would you suggest? Interestingly, there seems to be a proposal to add `defer` to C: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2895.htm So I guess, we should avoid taking the word `defer` in case it becomes a keyword some day. I'm not opposed to renaming, as long as the new name is short and not misleading. I'm not familiar with Go (presumably, from which the name comes), so I can't tell how similar the semantics of the feature in Go is to the proposed library. FWIW, in BOOST_SCOPE_FINAL, "final" is partly a reference to `__try`/`__finally` in MSVC and `try`/`finally` in Java, and partly emphasizes that the scope guard is "final" after construction, i.e. cannot be modified or cancelled. It also reads almost like "finale of the scope". I like it.
Andrey Semashev wrote:
I'm not opposed to renaming, as long as the new name is short and not misleading. I'm not familiar with Go (presumably, from which the name comes), so I can't tell how similar the semantics of the feature in Go is to the proposed library.
In Go, defer takes a function call expression: defer fclose( fp ); and captures the arguments as-if by value, evaluating them at the point of defer, not at the point where the function is called. It's like BOOST_SCOPE_FINAL std::bind( fclose, fp ); In Swift, defer takes a block: defer { fclose( fp ); } I have no idea what the capture semantics are there.
Andrey Semashev wrote:
Interestingly, there seems to be a proposal to add `defer` to C:
Interestingly, the proposed `defer` is literally your BOOST_SCOPE_FINAL, complete with the mandatory [&]. So this is both an argument for retaining the explicit [&] and an argument in favor of renaming it to BOOST_SCOPE_DEFER. It's not clear what are the chances of N2895 in WG14, of course.
On 11/29/23 18:56, Andrey Semashev wrote:
On 11/29/23 13:14, Janko Dedic via Boost wrote:
All destructor declarations in the documentation lack noexcept specification, which by default means that they're noexcept, but they're actually not if you look at the Throws section, or the implementation.
Unfortunately, this is how Doxygen works, I can't do anything about it. The noexcept specification is present in the code.
Turns out, this is not Doxygen's fault after all. The problem was in our Boostbook stylesheets, and it should be fixed by this PR: https://github.com/boostorg/boostbook/pull/23
participants (3)
-
Andrey Semashev
-
Janko Dedic
-
Peter Dimov