-
Notifications
You must be signed in to change notification settings - Fork 186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Get exception ptr CPO #249
base: main
Are you sure you want to change the base?
Get exception ptr CPO #249
Conversation
2beb643
to
859347c
Compare
- add get_exception_ptr_test
- add set_error(exception_ptr_convertible) overload in _awaitable_base
1a7c082
to
32ba5da
Compare
- std::exception_ptr forwarding - std::exception -> std::exception_ptr conversion - tag_invoke(get_exception_ptr, ...) customization points
- make any_sender_of use get_exception_ptr CPO - make async_scope use get_exception_ptr CPO
32ba5da
to
e4cc6f6
Compare
- some formatting - other pr comments
- update as_exception_ptr_test
0b20553
to
532a343
Compare
- set_error now uses as_exception_ptr to transform Errors to exception_ptr - rollback: - any_sender_of.hpp - async_scope.hpp - await_transform.hpp - cleanup tag_invocable/nothrow_tag_invocable concepts - file_coroutine_test does not used __FILE__ macro - sync_wait now requires only one set_error(exception_ptr) method
include/unifex/as_exception_ptr.hpp
Outdated
tag_invoke(tag_t<as_exception_ptr>, std::error_code&& error) noexcept { | ||
return make_exception_ptr( | ||
std::system_error{std::forward<std::error_code>(error)}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if this were an overload in _fn
rather than a tag_invoke
overload at namespace scope.
include/unifex/receiver_concepts.hpp
Outdated
(requires (!tag_invocable<_set_error_fn, Receiver, Error>) && | ||
tag_invocable<decltype(as_exception_ptr), Error>) | ||
auto operator()(Receiver&& r, Error&& error) const noexcept | ||
-> _result_t<Receiver, std::exception_ptr> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to update the _result_t
alias to account for this overload. Look how it is done elsewhere with if constexpr
in other customization points.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a _select
function (reproduced from connect
CPO), I hope it is fine.
include/unifex/receiver_concepts.hpp
Outdated
(requires (!tag_invocable<_set_error_fn, Receiver, Error>) && | ||
tag_invocable<decltype(as_exception_ptr), Error>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way you have constrained this requirement means that it will always convert the error to an exception_ptr if it can and the set_error() method can be invoked with an exception_ptr.
I think that what we want instead is to try to invoke set_error() with the provided type and only if there is no tag_invoke() or member function set_error() overload that accepts that type then, if the error type can be converted to exception_ptr and you can invoke set_error (either by tag_invoke or member function) with an exception_ptr then do that as a fallback.
So the order of resolution would be:
- tag_invoke w/ original error type
- set_error member fn w/ original error type
- tag_invoke w/ exception_ptr AND error type is convertible to exception_ptr
- set_error member fn w/ exception_ptr AND error type is convertible to exception_ptr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I missed it.
I added a test case to validate this.
- taking new overload into account
- add as_exception_ptr.set_error test case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for taking so long to get back to this.
A couple of additional comments.
If you don't have bandwidth for this just let me know and I'll fix it up.
} | ||
|
||
// default std::error_code to std::exception_ptr conversion | ||
std::exception_ptr operator()(std::error_code&& error) const noexcept { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the std::error_code
can be passed by-value here (it is cheap to copy, and passing by-value allows callers to pass lvalues to this function)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::exception_ptr operator()(std::error_code&& error) const noexcept { | |
std::exception_ptr operator()(std::error_code error) const noexcept { |
(requires (!tag_invocable<_set_error_fn, Receiver, Error>) AND | ||
std::invocable<decltype(&Receiver::set_error), Receiver, Error>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that there is another case that still needs to be handled in here.
At the moment this code has the following order of preference in which it resolves the implementation of unifex::set_error(r, err)
:
tag_invoke(set_error, r, err)
if that is well-formed; otherwiser.set_error(err)
if that is well-formed; otherwiser.set_error(as_exception_ptr(err))
if that is well-formed; otherwiseset_error(r, err)
call is ill-formed
I think that we probably need to have an extra case inserted into here to handle the case where tag_invoke(set_err, r, err)
is ill-formed but tag_invoke(set_err, r, as_exception_ptr(err))
is well-formed.
I think this should probably sit in between cases 2 and 3 above, although it's possible you could argue it should go between cases 1 and 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could possibly be handled with a two-stage resolution process using a helper.
struct set_error_fn {
private:
template(typename Receiver, typename Error)
(requires tag_invocable<set_error_fn, Receiver, Error>)
static void dispatch(Receiver&& r, Error&& err) noexcept {
static_assert(is_nothrow_tag_invocable_v<set_error_fn, Receiver, Error>);
static_assert(std::is_void_v<tag_invoke_result_t<set_error_fn, Receiver, Error>>);
tag_invoke(set_error_fn{}, (Receiver&&)r, (Error&&)err);
}
template(typename Receiver, typename Error>
(requires (!tag_invocable<set_error_fn, Receiver, Error> AND
has_member_set_error<Receiver, Error>)
static void dispatch(Receiver&& r, Error&& err) noexcept {
static_cast<Receiver&&>(r).set_error(static_cast<Error&&>(err));
}
template<typename Receiver, typename Error, typename = void>
static constexpr bool can_dispatch_v = false;
template<typename Receiver, typename Error>
static constexpr bool can_dispatch_v<Receiver, Error, std::void_t<decltype(dispatch(std::declval<Receiver>(), std::declval<Error>()))>> = true;
public:
template(typename Receiver, typename Error)
(requires can_dispatch_v<Receiver, Error>)
void operator()(Receiver&& r, Error&& err) const noexcept {
dispatch((Receiver&&)r, (Error&&)err);
}
template(typename Receiver, typename Error)
(requires (!can_dispatch_v<Receiver, Error>) AND
callable<decltype(as_exception_ptr), Error> AND
can_dispatch_v<Receiver, std::exception_ptr>)
void operator()(Receiver&& r, Error&& err) const noexcept {
dispatch((Receiver&&)r, as_exception_ptr((Error&&)err));
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we allowed to use syntax like if constexpr ( requires { is_this_ill_formed(error); }) {...} else {...}
? Wich is OK in GCC11 and latest MSVC. It would greatly simplify this kind of dispatching stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After double-check this syntax does not work on MSVC (I was actualy using clang-cl)
UNIFEX_CONCEPT nothrow_tag_invocable = | ||
(is_nothrow_tag_invocable_v<CPO, Args...>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually the nothrow type-trait queries haven't been represented as concepts but rather as boolean type-traits.
I'm not sure exactly why that is, though. Maybe @ericniebler has some thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Precedence for one. The fact that it's all syntax and no semantics for another. Neither of which are particularly compelling reasons.
} | ||
|
||
// default std::error_code to std::exception_ptr conversion | ||
std::exception_ptr operator()(std::error_code&& error) const noexcept { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::exception_ptr operator()(std::error_code&& error) const noexcept { | |
std::exception_ptr operator()(std::error_code error) const noexcept { |
template(typename Receiver, typename Error) | ||
(requires (!tag_invocable<_set_error_fn, Receiver, Error>)) | ||
(requires (!tag_invocable<_set_error_fn, Receiver, Error>) AND | ||
std::invocable<decltype(&Receiver::set_error), Receiver, Error>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::invocable<decltype(&Receiver::set_error), Receiver, Error>
This is not the right way to check for a .set_error(Error)
member function. Consider that Receiver::set_error
could be a template. In that case, &Receiver::set_error
is ill-formed. Check how such constraints are checked elsewhere; you'll need to test the validity of the expression rec.set_error(err)
in a SFINAE context (or with a concept) to get a correct answer.
template(typename Receiver, typename Error) | ||
(requires (!tag_invocable<_set_error_fn, Receiver, Error>) AND | ||
(!std::invocable<decltype(&Receiver::set_error), Receiver, Error>) AND | ||
std::invocable<decltype(&Receiver::set_error), Receiver, std::exception_ptr> AND |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here about testing for a .set_error(e)
member function.
Create new
get_exception_ptr
CPO allowing users to control how any kind of error type will be internally converted to anstd::exception_ptr
.