diff --git a/include/exec/detail/shared.hpp b/include/exec/detail/shared.hpp index 58407d93a..604635928 100644 --- a/include/exec/detail/shared.hpp +++ b/include/exec/detail/shared.hpp @@ -27,14 +27,15 @@ #include "../../stdexec/__detail/__optional.hpp" #include "../../stdexec/__detail/__queries.hpp" #include "../../stdexec/__detail/__receivers.hpp" -#include "../../stdexec/__detail/__storage.hpp" +#include "../../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../../stdexec/__detail/__transform_completion_signatures.hpp" #include "../../stdexec/__detail/__tuple.hpp" #include "../../stdexec/__detail/__variant.hpp" // IWYU pragma: keep #include "../../stdexec/stop_token.hpp" -#include #include +#include +#include #include //////////////////////////////////////////////////////////////////////////// @@ -69,7 +70,7 @@ namespace experimental::execution::__shared { using namespace STDEXEC; - template + template struct __shared_state_base; template @@ -79,7 +80,7 @@ namespace experimental::execution::__shared using __env_t = __join_env_t, _Env>; //////////////////////////////////////////////////////////////////////////////////////// - template + template struct __receiver { using receiver_concept = receiver_tag; @@ -110,14 +111,23 @@ namespace experimental::execution::__shared } // The receiver does not hold a reference to the shared state. - __shared_state_base<_Env, _Variant>* __sh_state_; + __shared_state_base<_Env, _Storage>* __sh_state_; }; //////////////////////////////////////////////////////////////////////////////////////// template - using __result_variant_t = - __mapply<__mbind_front_q<__results_storage, set_stopped_t(), set_error_t(std::exception_ptr)>, - __completion_signatures_of_t<_CvSender, _Env>>; + using __result_storage_t = storage_for_completion_signatures< + __transform_completion_signatures_of_t<_CvSender, + __env_t<_Env>, + completion_signatures>>; + + // Owned results from split are replayed by const reference. Results that are already + // references retain the type advertised by the child. ensure_started consumes the cache, so + // it replays every result with its stored type. + template + using __replayed_result_t = + __minvoke<__if_c<__same_as<_Tag, split_t> && !std::is_reference_v<_Result>, __cpclr, __cp>, + _Result>; //////////////////////////////////////////////////////////////////////////////////////// template @@ -241,13 +251,37 @@ namespace experimental::execution::__shared // __local_state::operator(). constexpr void __notify() noexcept final { - // The split algorithm sends by T const&. ensure_started sends by T&&. - constexpr bool __is_split = __std::same_as; - using __variant_t = decltype(__sh_state_->__results_); - using __cv_variant_t = __if_c<__is_split, __variant_t const &, __variant_t>; - __on_stop_.reset(); - static_cast<__cv_variant_t&&>(__sh_state_->__results_).__complete(__rcvr_); + if constexpr (__std::same_as) + { + STDEXEC::visit_stored_completion( + STDEXEC::__overload{ + []() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&]( + storage_for_completion_signature<_SetTag(_Results...)> const & __completion) noexcept + -> void + { + STDEXEC::__apply( + [&](auto&&... __results) noexcept + { + _SetTag{}(static_cast<_Receiver&&>(__rcvr_), + static_cast<__replayed_result_t>(__results)...); + }, + __completion.__forward_arguments()); + }}, + std::as_const(__sh_state_->__results_)); + } + else + { + bool const __completed = + std::move(__sh_state_->__results_).complete(static_cast<_Receiver&&>(__rcvr_)); + STDEXEC_ASSERT(__completed); + (void) __completed; + } } _Receiver __rcvr_; @@ -257,7 +291,7 @@ namespace experimental::execution::__shared //////////////////////////////////////////////////////////////////////////////////////// //! Base class for heap-allocatable shared state for `split` and `ensure_started`. - template + template struct __shared_state_base : __local_state_base { using __waiters_list_t = __intrusive_slist<&__local_state_base::__next_>; @@ -265,9 +299,7 @@ namespace experimental::execution::__shared constexpr explicit __shared_state_base(_Env __env) : __env_(__env::__join(prop{get_stop_token, __stop_source_.get_token()}, static_cast<_Env&&>(__env))) - { - __results_.template emplace<__tuple>(); - } + {} virtual ~__shared_state_base() = 0; @@ -276,20 +308,7 @@ namespace experimental::execution::__shared template void __complete(_Tag, _As&&... __as) noexcept { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Tag, _As...>; - __results_.template emplace<__tuple_t>(_Tag(), static_cast<_As&&>(__as)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_As...>) - { - using __tuple_t = __decayed_tuple; - __results_.template emplace<__tuple_t>(set_error, std::current_exception()); - } - } - + __results_.arrive(_Tag(), static_cast<_As&&>(__as)...); __notify_waiters(); } @@ -328,20 +347,20 @@ namespace experimental::execution::__shared __waiters_list_t __waiters_{}; inplace_stop_source __stop_source_{}; __env_t<_Env> __env_; - _Variant __results_; // Initialized to the "set_stopped" state in the ctor. + _Storage __results_; }; - template - __shared_state_base<_Env, _Variant>::~__shared_state_base() = default; + template + __shared_state_base<_Env, _Storage>::~__shared_state_base() = default; //////////////////////////////////////////////////////////////////////////////////////// //! Heap-allocatable shared state for `stdexec::split` and `stdexec::ensure_started`. template struct STDEXEC_ATTRIBUTE(empty_bases) __shared_state final : std::enable_shared_from_this<__shared_state<_CvSender, _Env>> - , __shared_state_base<_Env, __result_variant_t<_CvSender, _Env>> + , __shared_state_base<_Env, __result_storage_t<_CvSender, _Env>> { - using __receiver_t = __receiver<_Env, __result_variant_t<_CvSender, _Env>>; + using __receiver_t = __receiver<_Env, __result_storage_t<_CvSender, _Env>>; using __waiters_list_t = __shared_state::__shared_state_base::__waiters_list_t; constexpr explicit __shared_state(_CvSender&& __sndr, _Env __env) @@ -380,7 +399,7 @@ namespace experimental::execution::__shared // 1. Sets __waiters_ to a known "tombstone" value. // 2. Notifies all the waiters that the operation has stopped. // 3. Sets the "is running" bit in the ref count to 0. - this->__notify_waiters(); + this->__complete(set_stopped); } else { @@ -437,19 +456,6 @@ namespace experimental::execution::__shared connect_result_t<_CvSender, __receiver_t> __shared_op_; }; - template - using __make_completions_t = __transform_completion_signatures_of_t< - _CvSender, - __env_t<_Env>, - completion_signatures), set_stopped_t()>, - __mtransform<_Cv, __mcompose<__qq, __qf>>::template __f, - __mtransform<_Cv, __mcompose<__qq, __qf>>::template __f>; - - // split completes with const T&. ensure_started completes with T&&. - template - using __cvref_results_t = - __mcompose<__if_c<__same_as<_Tag, split_t>, __cpclr, __cp>, __q1<__decay_t>>; - template struct __impls : __sexpr_defaults { @@ -460,7 +466,26 @@ namespace experimental::execution::__shared // TODO: update this for constant evaluation if constexpr (__decay_copyable<_CvChild>) { - return __make_completions_t<__cvref_results_t<_Tag>, _CvChild, _Env>(); + auto __child_completions = STDEXEC::get_completion_signatures<_CvChild, __env_t<_Env>>(); + STDEXEC_IF_OK(__child_completions) + { + auto __input_completions = + STDEXEC::__concat_completion_signatures(__child_completions, + completion_signatures{}); + using __storage_t = storage_for_completion_signatures; + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return STDEXEC::__transform_completion_signatures( + __stored_completions, + []() + { + return completion_signatures...)>{}; + }, + []() + { return completion_signatures)>{}; }); + } + } } else { diff --git a/include/exec/fork_join.hpp b/include/exec/fork_join.hpp index 33fa9b495..eb5f67eff 100644 --- a/include/exec/fork_join.hpp +++ b/include/exec/fork_join.hpp @@ -16,18 +16,13 @@ #pragma once #include "../stdexec/__detail/__receiver_ref.hpp" -#include "../stdexec/__detail/__storage.hpp" +#include "../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../stdexec/execution.hpp" -#include - namespace experimental::execution { struct fork_join_t; - struct PREDECESSOR_RESULTS_ARE_NOT_DECAY_COPYABLE - {}; - struct INVALID_ARGUMENTS_TO_FORK_JOIN {}; @@ -44,7 +39,7 @@ namespace experimental::execution }; template - using _variant_t = STDEXEC::__mapply_q; + using _storage_t = STDEXEC::storage_for_completion_signatures; template struct _env_t @@ -70,7 +65,14 @@ namespace experimental::execution using _cache_sndr_completions_t = STDEXEC::completion_signatures, AsyncResults>...>; - template + template + consteval auto _cache_sndr_completions(STDEXEC::completion_signatures) + -> _cache_sndr_completions_t + { + return {}; + } + + template struct _cache_sndr_t { using sender_concept = STDEXEC::sender_tag; @@ -82,18 +84,39 @@ namespace experimental::execution STDEXEC_ATTRIBUTE(host, device) void start() noexcept { - std::as_const(*_results_).__complete(_rcvr_); + STDEXEC::visit_stored_completion( + STDEXEC::__overload{[]() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto const & __completion) noexcept -> void + { + STDEXEC::__apply( + [&](auto&&... __args) noexcept + { + __completion.tag()(static_cast(_rcvr_), + static_cast( + __args)...); + }, + __completion.__forward_arguments()); + }}, + *_results_); } Rcvr _rcvr_; - Variant const * _results_; + Storage const * _results_; }; template STDEXEC_ATTRIBUTE(host, device) static consteval auto get_completion_signatures() { - return STDEXEC::__mapply, Variant>{}; + auto _stored_completions = Storage::get_completion_signatures(); + STDEXEC_IF_OK(_stored_completions) + { + return _cache_sndr_completions(_stored_completions); + } } template @@ -109,14 +132,14 @@ namespace experimental::execution return {}; } - Variant const * _results_; + Storage const * _results_; }; template using _when_all_sndr_t = STDEXEC::__apply_result_t<_mk_when_all_fn, Closures, - _cache_sndr_t<_variant_t, Domain>>; + _cache_sndr_t<_storage_t, Domain>>; template struct _opstate_t @@ -131,7 +154,7 @@ namespace experimental::execution STDEXEC::connect_result_t>; using _fork_opstate_t = STDEXEC::connect_result_t<_when_all_sndr_t, STDEXEC::__receiver_ref>; - using _cache_sndr_t = _fork_join::_cache_sndr_t<_variant_t<_child_completions_t>, _domain_t>; + using _cache_sndr_t = _fork_join::_cache_sndr_t<_storage_t<_child_completions_t>, _domain_t>; STDEXEC_ATTRIBUTE(host, device) constexpr explicit _opstate_t(Sndr&& sndr, Closures&& closures, Rcvr rcvr) noexcept @@ -152,7 +175,7 @@ namespace experimental::execution constexpr ~_opstate_t() { // If this opstate was never started, we must explicitly destroy the _child_opstate_. - if (_cache_.__is_valueless()) + if (!_cache_.has_completion()) { _child_opstate_.__destroy(); } @@ -168,20 +191,7 @@ namespace experimental::execution STDEXEC_ATTRIBUTE(host, device) constexpr void _complete(Tag, Args&&... args) noexcept { - STDEXEC_TRY - { - using _tuple_t = STDEXEC::__decayed_tuple; - _cache_.template emplace<_tuple_t>(Tag{}, static_cast(args)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!STDEXEC::__nothrow_decay_copyable) - { - using _tuple_t = STDEXEC::__tuple; - _cache_._results_.template emplace<_tuple_t>(STDEXEC::set_error, - ::std::current_exception()); - } - } + _cache_.arrive(Tag{}, static_cast(args)...); _child_opstate_.__destroy(); STDEXEC::start(_fork_opstate_); } @@ -213,7 +223,7 @@ namespace experimental::execution } Rcvr _rcvr_; - _variant_t<_child_completions_t> _cache_; + _storage_t<_child_completions_t> _cache_; STDEXEC::__manual_lifetime<_child_opstate_t> _child_opstate_{}; _fork_opstate_t _fork_opstate_; }; @@ -238,18 +248,10 @@ namespace experimental::execution using _domain_t = __completion_domain_of_t; using _child_t = __copy_cvref_t; using _child_completions_t = __completion_signatures_of_t<_child_t, __fwd_env_t...>; - using __decay_copyable_results_t = __decay_copyable_results_t<_child_completions_t>; - if constexpr (!__valid_completion_signatures<_child_completions_t>) { return _child_completions_t{}; } - else if constexpr (!__decay_copyable_results_t::value) - { - return STDEXEC::__throw_compile_time_error< // - _WHAT_(PREDECESSOR_RESULTS_ARE_NOT_DECAY_COPYABLE), - _IN_ALGORITHM_(exec::fork_join_t)>(); - } else { using _sndr_t = diff --git a/include/exec/sequence/ignore_all_values.hpp b/include/exec/sequence/ignore_all_values.hpp index 0ca0e9ddb..98469fba5 100644 --- a/include/exec/sequence/ignore_all_values.hpp +++ b/include/exec/sequence/ignore_all_values.hpp @@ -20,7 +20,7 @@ // include these after execution.hpp #include "../../stdexec/__detail/__senders.hpp" -#include "../../stdexec/__detail/__tuple.hpp" +#include "../../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../sender_for.hpp" #include "../sequence_senders.hpp" @@ -28,46 +28,20 @@ namespace experimental::execution { - template - concept __variant_emplaceable = requires(_Variant& __var, _Args&&... __args) { - __var.template emplace<_Type>(static_cast<_Args&&>(__args)...); - }; - - template - concept __nothrow_variant_emplaceable = requires(_Variant& __var, _Args&&... __args) { - { __var.template emplace<_Type>(static_cast<_Args&&>(__args)...) } noexcept; - }; - namespace __ignore_all_values { using namespace STDEXEC; - constexpr auto __complete_fn = - [](_Receiver&& __rcvr, - _Tag, - _Args&&... __args) noexcept - { - _Tag()(static_cast<_Receiver&&>(__rcvr), static_cast<_Args&&>(__args)...); - }; - - constexpr auto __visit_fn = - [](_Receiver&& __rcvr, _Tuple&& __tupl) noexcept - { - STDEXEC::__apply(__complete_fn, - static_cast<_Tuple&&>(__tupl), - static_cast<_Receiver&&>(__rcvr)); - }; - template struct __result_type { - template - void __emplace(_Args&&... __args) noexcept + template + void __arrive(_Tag __tag, _Args&&... __args) noexcept { int __expected = 0; if (__emplaced_.compare_exchange_strong(__expected, 1, __std::memory_order_relaxed)) { - __result_.template emplace<__decayed_tuple<_Args...>>(static_cast<_Args&&>(__args)...); + __result_.arrive(__tag, static_cast<_Args&&>(__args)...); __emplaced_.store(2, __std::memory_order_release); } } @@ -83,9 +57,12 @@ namespace experimental::execution { STDEXEC::set_value(static_cast<_Receiver&&>(__rcvr)); } - else if constexpr (STDEXEC::__mapply::value != 0) + else { - static_cast<_ResultsStorage&&>(__result_).__complete(__rcvr); + bool const __completed = static_cast<_ResultsStorage&&>(__result_).complete( + static_cast<_Receiver&&>(__rcvr)); + STDEXEC_ASSERT(__completed); + (void) __completed; } } @@ -114,26 +91,22 @@ namespace experimental::execution } template - requires __variant_emplaceable<_ResultsStorage, - __decayed_tuple, - set_error_t, - _Error> - && __callable + requires requires(_ResultsStorage& __storage, _Error&& __error) { + __storage.arrive(set_error, static_cast<_Error&&>(__error)); + } && __callable void set_error(_Error&& __error) noexcept { // store error and signal stop - __op_->__result_->__emplace(set_error_t(), static_cast<_Error&&>(__error)); + __op_->__result_->__arrive(set_error_t(), static_cast<_Error&&>(__error)); STDEXEC::set_stopped(static_cast<_ItemReceiver&&>(__op_->__rcvr_)); } void set_stopped() noexcept - requires __variant_emplaceable<_ResultsStorage, - __decayed_tuple, - set_stopped_t> + requires requires(_ResultsStorage& __storage) { __storage.arrive(set_stopped); } && __callable { // stop without error - __op_->__result_->__emplace(set_stopped_t()); + __op_->__result_->__arrive(set_stopped_t()); STDEXEC::set_stopped(static_cast<_ItemReceiver&&>(__op_->__rcvr_)); } @@ -251,19 +224,23 @@ namespace experimental::execution using __is_set_value_signature_t = __mbool<__same_as>>; + template + using __nonvalue_completion_signatures_t = + __mapply<__mremove_if<__q1<__is_set_value_signature_t>, __qq>, + _CompletionSignatures>; + // Storage for the non-set_value completions template - using __result_variant_t = - __mapply<__mremove_if<__q1<__is_set_value_signature_t>, __qq<__results_storage>>, - __sequence_completion_signatures_of_t<_Sender, _Env>>; + using __result_storage_t = storage_for_completion_signatures< + __nonvalue_completion_signatures_t<__sequence_completion_signatures_of_t<_Sender, _Env>>>; template struct __operation - : __operation_base<_Receiver, __result_variant_t<_Sender, env_of_t<_Receiver>>> + : __operation_base<_Receiver, __result_storage_t<_Sender, env_of_t<_Receiver>>> { - using __variant_t = __result_variant_t<_Sender, env_of_t<_Receiver>>; - using __base_t = __operation_base<_Receiver, __variant_t>; - using __receiver_t = __receiver<_Receiver, __variant_t>; + using __storage_t = __result_storage_t<_Sender, env_of_t<_Receiver>>; + using __base_t = __operation_base<_Receiver, __storage_t>; + using __receiver_t = __receiver<_Receiver, __storage_t>; explicit __operation(_Sender&& __sndr, _Receiver __rcvr) noexcept(__nothrow_subscribable<_Sender, __receiver_t>) @@ -317,7 +294,18 @@ namespace experimental::execution static consteval auto __get_completion_signatures() { static_assert(sender_for<_Sender, ignore_all_values_t>); - return __sequence_completion_signatures_of<__child_of<_Sender>, _Env...>(); + auto __completions = __sequence_completion_signatures_of<__child_of<_Sender>, _Env...>(); + STDEXEC_IF_OK(__completions) + { + using __storage_t = storage_for_completion_signatures< + __nonvalue_completion_signatures_t>; + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return STDEXEC::__concat_completion_signatures(completion_signatures{}, + __stored_completions); + } + } } static constexpr auto __connect = diff --git a/include/exec/when_any.hpp b/include/exec/when_any.hpp index 94d8e6918..6664d5b09 100644 --- a/include/exec/when_any.hpp +++ b/include/exec/when_any.hpp @@ -16,7 +16,7 @@ */ #pragma once -#include "../stdexec/__detail/__storage.hpp" +#include "../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../stdexec/execution.hpp" #include "../stdexec/stop_token.hpp" @@ -34,46 +34,22 @@ namespace experimental::execution template using __env_t = __join_env_t, _Env>; - template - using __nothrow_decay_copyable_and_move_constructible_t = __mbool<( - (__nothrow_decay_copyable<_Ts> && __nothrow_move_constructible<__decay_t<_Ts>>) && ...)>; - - template - using __as_rvalues = set_value_t (*)(__decay_t...); - - template - using __as_error = set_error_t (*)(E...); - - // Here we convert all set_value(Args...) to set_value(__decay_t...). Note, we keep all - // error types as they are and unconditionally add set_stopped(). The indirection through the - // __completions_fn is to avoid a pack expansion bug in nvc++. + // We unconditionally add set_stopped(). The indirection through the __completions_fn is to + // avoid a pack expansion bug in nvc++. template struct __completions_fn { - template - using __all_value_args_nothrow_decay_copyable = - __minvoke_q<__mand_t, - __value_types_t<__completion_signatures_of_t<_CvSenders, __env_t<_Env>...>, - __qq<__nothrow_decay_copyable_and_move_constructible_t>, - __qq<__mand_t>>...>; - template using __f = __mtry_q<__concat_completion_signatures_t>::__f< - __eptr_completion_unless_t<__all_value_args_nothrow_decay_copyable<_CvSenders...>>, completion_signatures, - __transform_reduce_completion_signatures_t< - __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>, - __as_rvalues, - __as_error, - set_stopped_t (*)(), - __completion_signature_ptrs_t>...>; + __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>...>; }; template using __results_storage_t = - __mapply_q<__results_storage, __minvoke<__completions_fn<_Env>, _CvSenders...>>; + storage_for_completion_signatures<__minvoke<__completions_fn<_Env>, _CvSenders...>>; - template + template struct __opstate_base { __opstate_base(_Receiver&& __rcvr, std::size_t __n_senders) @@ -112,35 +88,19 @@ namespace experimental::execution __std::atomic __count_{}; _Receiver __rcvr_; - _ResultVariant __result_; + _ResultStorage __result_; template void notify(_Tag, _Args&&... __args) noexcept { - using __result_t = __decayed_tuple<_Tag, _Args...>; - bool __expect = false; + bool __expect = false; if (__emplaced_.compare_exchange_strong(__expect, true, __std::memory_order_relaxed, __std::memory_order_relaxed)) { - // This emplacement can happen only once - if constexpr ((__nothrow_decay_copyable<_Args> && ...)) - { - __result_.template emplace<__result_t>(_Tag{}, static_cast<_Args&&>(__args)...); - } - else - { - STDEXEC_TRY - { - __result_.template emplace<__result_t>(_Tag{}, static_cast<_Args&&>(__args)...); - } - STDEXEC_CATCH_ALL - { - using __error_t = __tuple; - __result_.template emplace<__error_t>(set_error_t{}, std::current_exception()); - } - } + // This arrival can happen only once. + __result_.arrive(_Tag{}, static_cast<_Args&&>(__args)...); // stop pending operations __stop_source_.request_stop(); } @@ -160,18 +120,18 @@ namespace experimental::execution STDEXEC::set_stopped(static_cast<_Receiver&&>(__rcvr_)); return; } - std::move(__result_).__complete(__rcvr_); + std::move(__result_).complete(static_cast<_Receiver&&>(__rcvr_)); } } }; - template + template struct __receiver { public: using receiver_concept = STDEXEC::receiver_tag; - explicit __receiver(__opstate_base<_Receiver, _ResultVariant>* __op) noexcept + explicit __receiver(__opstate_base<_Receiver, _ResultStorage>* __op) noexcept : __op_{__op} {} @@ -199,7 +159,7 @@ namespace experimental::execution } private: - __opstate_base<_Receiver, _ResultVariant>* __op_; + __opstate_base<_Receiver, _ResultStorage>* __op_; }; template @@ -249,6 +209,9 @@ namespace experimental::execution template using __completions_t = __minvoke<__completions_fn<_Env...>, __copy_cvref_t<_Self, _Senders>...>; + + template + using __storage_t = storage_for_completion_signatures<__completions_t<_Self, _Env...>>; public: using sender_concept = STDEXEC::sender_tag; @@ -274,7 +237,7 @@ namespace experimental::execution template <__decay_copyable _Self, class... _Env> static consteval auto get_completion_signatures() { - return __completions_t<_Self, _Env...>{}; + return __storage_t<_Self, _Env...>::get_completion_signatures(); } template diff --git a/include/stdexec/__detail/__as_awaitable.hpp b/include/stdexec/__detail/__as_awaitable.hpp index 304118b11..6a3254381 100644 --- a/include/stdexec/__detail/__as_awaitable.hpp +++ b/include/stdexec/__detail/__as_awaitable.hpp @@ -32,17 +32,21 @@ import stdexec; # include "__completion_signatures_of.hpp" # include "__concepts.hpp" # include "__connect.hpp" +# include "__manual_lifetime.hpp" # include "__meta.hpp" # include "__queries.hpp" +# include "__scope.hpp" # include "__spin_loop_pause.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__type_traits.hpp" -# include "__variant.hpp" # if !STDEXEC_USE_MODULES() # include # include // for std::identity # include # include +# include +# include # endif # include "__prologue.hpp" @@ -58,8 +62,11 @@ namespace STDEXEC namespace __as_awaitable { + template + using __value_tuple_t = std::tuple<_Values...>; + template - extern __q<__decayed_std_tuple> const __as_single; + extern __q<__value_tuple_t> const __as_single; template <> inline constexpr __q<__midentity> __as_single<1>; @@ -71,8 +78,8 @@ namespace STDEXEC using __single_value_t = __minvoke), _Values...>; template - using __value_t = __decay_t< - __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value_t>, __msingle_or>>; + using __value_t = + __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value_t>, __msingle_or>; inline constexpr auto __get_await_completion_adaptor = __with_default{get_await_completion_adaptor, std::identity{}}; @@ -91,14 +98,46 @@ namespace STDEXEC using __adapted_sender_t = __remove_rvalue_reference_t<__call_result_t<__adapt_completion_t<_Sender>, _Sender>>; - struct __void - {}; + template + using __set_value_sig_t = completion_signatures; - template - using __value_or_void_t = __if_c<__same_as<_Value, void>, __void, _Value>; + template + using __value_signatures_t = + __value_types_of_t<_Sender, + env_of_t<_Promise&>, + __q<__set_value_sig_t>, + __msingle_or>>; + + template + using __expected_t = storage_for_completion_signatures< + __concat_completion_signatures_t<__value_signatures_t<_Sender, _Promise>, + completion_signatures>>; template - using __expected_t = __variant<__value_or_void_t<_Value>, std::exception_ptr>; + struct __await_result + { + template + constexpr auto operator()(_Arg&& __arg) const -> _Value + { + return static_cast<_Arg&&>(__arg); + } + }; + + template <> + struct __await_result + { + constexpr void operator()() const noexcept {} + }; + + template + struct __await_result> + { + template + constexpr auto operator()(_Args&&... __args) const -> std::tuple<_Values...> + { + return std::tuple<_Values...>{static_cast<_Args&&>(__args)...}; + } + }; using __connect_await::__has_as_awaitable_member; @@ -111,11 +150,11 @@ namespace STDEXEC && __completes_inline_for && __completes_inline_for; - template + template struct __sender_awaiter_base; - template - struct __sender_awaiter_base<_Value, true> + template + struct __sender_awaiter_base<_Sender, _Promise, _Value, true> { static constexpr auto await_ready() noexcept -> bool { @@ -124,27 +163,65 @@ namespace STDEXEC constexpr auto await_resume() -> _Value { - // If the operation completed with set_stopped (as denoted by the result variant - // being valueless), we should not be resuming this coroutine at all. - STDEXEC_ASSERT(!__result_.__is_valueless()); - if (__result_.index() == 1) - { - // The operation completed with set_error, so we need to rethrow the exception. - std::rethrow_exception(std::move(__var::__get<1>(__result_))); - } + __manual_lifetime<_Value> __value; + bool const __has_result = visit_stored_completion( + __overload{ + []() -> bool + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto&& __completion) -> bool + { + using __completion_t = std::remove_cvref_t; + auto __args = static_cast(__completion).forward_arguments(); + if constexpr (__same_as) + { + static_assert(std::tuple_size_v == 1); + std::apply([](auto&& __err) -> void + { std::rethrow_exception(static_cast(__err)); }, + static_cast(__args)); + } + else + { + std::apply( + [&](auto&&... __as) -> void + { + __value.__construct_from(__await_result<_Value>{}, + static_cast(__as)...); + }, + static_cast(__args)); + } + return true; + }}, + static_cast<__expected_t<_Sender, _Promise>&&>(__result_)); + + // If the operation completed with set_stopped, we should not be resuming this coroutine at all. + STDEXEC_ASSERT(__has_result); // The operation completed with set_value, so we can just return the value, which // may be void. - using __reference_t = std::add_rvalue_reference_t<_Value>; - return static_cast<__reference_t>(__var::__get<0>(__result_)); + if constexpr (__same_as<_Value, void>) + { + return; + } + else if constexpr (std::is_reference_v<_Value>) + { + return __value.__get(); + } + else + { + auto __guard = __scope_guard{[&]() noexcept { __value.__destroy(); }}; + return static_cast<_Value&&>(__value.__get()); + } } [[nodiscard]] constexpr auto __get_continuation() const noexcept -> __std::coroutine_handle<> { - // If the operation was stopped (__result_ is valueless), we should use the + // If the operation was stopped (__result_ is empty), we should use the // unhandled_stopped() continuation. Otherwise, should resume the __continuation_ // as normal. - if (__result_.__is_valueless()) + if (!__result_.has_completion()) { return STDEXEC::__coroutine_unhandled_stopped(__continuation_); } @@ -154,14 +231,15 @@ namespace STDEXEC } } - __coroutine_handle<> __continuation_; - __expected_t<_Value> __result_{__no_init}; + __coroutine_handle<> __continuation_; + __expected_t<_Sender, _Promise> __result_{}; }; // When the sender is not statically known to complete inline, we need to use atomic // state to guard against too many inline completions causing a stack overflow. - template - struct __sender_awaiter_base<_Value, false> : __sender_awaiter_base<_Value, true> + template + struct __sender_awaiter_base<_Sender, _Promise, _Value, false> + : __sender_awaiter_base<_Sender, _Promise, _Value, true> { // This is used to coordinate between await_suspend (T1) and the receiver (T2). // It can hold three different values: @@ -183,7 +261,7 @@ namespace STDEXEC __std::atomic __thread_id_{std::this_thread::get_id()}; }; - template + template struct __receiver_base { using receiver_concept = receiver_tag; @@ -193,11 +271,11 @@ namespace STDEXEC { STDEXEC_TRY { - __awaiter_.__result_.template emplace<0>(static_cast<_Us&&>(__us)...); + __awaiter_.__result_.arrive(set_value_t{}, static_cast<_Us&&>(__us)...); } STDEXEC_CATCH_ALL { - __awaiter_.__result_.template emplace<1>(std::current_exception()); + __awaiter_.__result_.arrive(set_error_t{}, std::current_exception()); } } @@ -205,25 +283,31 @@ namespace STDEXEC void set_error(_Error&& __err) noexcept { if constexpr (__decays_to<_Error, std::exception_ptr>) - __awaiter_.__result_.template emplace<1>(static_cast<_Error&&>(__err)); + { + __awaiter_.__result_.arrive(set_error_t{}, static_cast<_Error&&>(__err)); + } else if constexpr (__decays_to<_Error, std::error_code>) - __awaiter_.__result_.template emplace<1>( - std::make_exception_ptr(std::system_error(__err))); + { + __awaiter_.__result_.arrive(set_error_t{}, + std::make_exception_ptr(std::system_error(__err))); + } else - __awaiter_.__result_.template emplace<1>( - std::make_exception_ptr(static_cast<_Error&&>(__err))); + { + __awaiter_.__result_.arrive(set_error_t{}, + std::make_exception_ptr(static_cast<_Error&&>(__err))); + } } - __sender_awaiter_base<_Value, true>& __awaiter_; + __sender_awaiter_base<_Sender, _Promise, _Value, true>& __awaiter_; }; - template - struct __sync_receiver : __receiver_base<_Value> + template + struct __sync_receiver : __receiver_base<_Sender, _Promise, _Value> { - using __awaiter_t = __sender_awaiter_base<_Value, true>; + using __awaiter_t = __sender_awaiter_base<_Sender, _Promise, _Value, true>; constexpr explicit __sync_receiver(__awaiter_t& __awaiter) noexcept - : __receiver_base<_Value>{__awaiter} + : __receiver_base<_Sender, _Promise, _Value>{__awaiter} {} void set_stopped() noexcept @@ -243,26 +327,26 @@ namespace STDEXEC }; // The receiver type used to connect to senders that could complete asynchronously. - template - struct __async_receiver : __sync_receiver<_Promise, _Value> + template + struct __async_receiver : __sync_receiver<_Sender, _Promise, _Value> { - using __awaiter_t = __sender_awaiter_base<_Value, false>; + using __awaiter_t = __sender_awaiter_base<_Sender, _Promise, _Value, false>; constexpr explicit __async_receiver(__awaiter_t& __awaiter) noexcept - : __sync_receiver<_Promise, _Value>{__awaiter} + : __sync_receiver<_Sender, _Promise, _Value>{__awaiter} {} template void set_value(_Us&&... __us) noexcept { - this->__sync_receiver<_Promise, _Value>::set_value(static_cast<_Us&&>(__us)...); + this->__sync_receiver<_Sender, _Promise, _Value>::set_value(static_cast<_Us&&>(__us)...); __done(); } template void set_error(_Error&& __err) noexcept { - this->__sync_receiver<_Promise, _Value>::set_error(static_cast<_Error&&>(__err)); + this->__sync_receiver<_Sender, _Promise, _Value>::set_error(static_cast<_Error&&>(__err)); __done(); } @@ -306,23 +390,24 @@ namespace STDEXEC }; template - using __sync_receiver_t = __sync_receiver<_Promise, __value_t<_Sender, _Promise>>; + using __sync_receiver_t = __sync_receiver<_Sender, _Promise, __value_t<_Sender, _Promise>>; template - using __async_receiver_t = __async_receiver<_Promise, __value_t<_Sender, _Promise>>; + using __async_receiver_t = __async_receiver<_Sender, _Promise, __value_t<_Sender, _Promise>>; ////////////////////////////////////////////////////////////////////////////////////// // __sender_awaiter: awaitable type returned by as_awaitable when given a sender // that does not have an as_awaitable member function template > _Sender> - struct __sender_awaiter : __sender_awaiter_base<__value_t<_Sender, _Promise>, false> + struct __sender_awaiter + : __sender_awaiter_base<_Sender, _Promise, __value_t<_Sender, _Promise>, false> { using __value_t = __as_awaitable::__value_t<_Sender, _Promise>; constexpr explicit __sender_awaiter(_Sender&& __sndr, __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_connectable<_Sender, __receiver_t>) - : __sender_awaiter_base<__value_t, false>{__hcoro} + : __sender_awaiter_base<_Sender, _Promise, __value_t, false>{__hcoro} , __opstate_(STDEXEC::connect(static_cast<_Sender&&>(__sndr), __receiver_t(*this))) {} @@ -365,14 +450,14 @@ namespace STDEXEC template > _Sender> requires __completes_inline<_Sender, env_of_t<_Promise&>> struct __sender_awaiter<_Promise, _Sender> - : __sender_awaiter_base<__value_t<_Sender, _Promise>, true> + : __sender_awaiter_base<_Sender, _Promise, __value_t<_Sender, _Promise>, true> { using __value_t = __as_awaitable::__value_t<_Sender, _Promise>; constexpr explicit __sender_awaiter(_Sender&& __sndr, __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_move_constructible<_Sender>) - : __sender_awaiter_base<__value_t, true>{__hcoro} + : __sender_awaiter_base<_Sender, _Promise, __value_t, true>{__hcoro} , __sndr_(static_cast<_Sender&&>(__sndr)) {} diff --git a/include/stdexec/__detail/__continues_on.hpp b/include/stdexec/__detail/__continues_on.hpp index d9fb2866b..1cc5e8311 100644 --- a/include/stdexec/__detail/__continues_on.hpp +++ b/include/stdexec/__detail/__continues_on.hpp @@ -27,9 +27,8 @@ #include "__schedulers.hpp" #include "__sender_adaptor_closure.hpp" #include "__senders.hpp" -#include "__storage.hpp" +#include "__storage_for_completion_signatures.hpp" #include "__transform_completion_signatures.hpp" -#include "__tuple.hpp" #include "__utility.hpp" #include "__prologue.hpp" @@ -40,32 +39,14 @@ namespace STDEXEC // [exec.continues.on] namespace __trnsfr { - template - using __decay_value_sig = set_value_t (*)(__decay_t<_Values>...); - - template - using __decay_error_sig = set_error_t (*)(__decay_t<_Error>); - - template - using __completions_impl_t = __mtry_q<__concat_completion_signatures_t>::__f< - __transform_reduce_completion_signatures_t<_Completions, - __decay_value_sig, - __decay_error_sig, - set_stopped_t (*)(), - __completion_signature_ptrs_t>, - __transform_completion_signatures_t< - __completion_signatures_of_t, _Env...>, - __eptr_completion_unless_t<__nothrow_decay_copyable_results_t<_Completions>>, - __mconst>::__f>>; - - template - using __completions_t = - __completions_impl_t<_Scheduler, __completion_signatures_of_t<_CvSender, _Env...>, _Env...>; + template + using __completion_storage_t = + storage_for_completion_signatures<__completion_signatures_of_t<_Sender, _Env...>>; template struct __state_base { - using __storage_t = __storage_for_t<__child_of<_Sexpr>, env_of_t<_Receiver>>; + using __storage_t = __completion_storage_t<__child_of<_Sexpr>, env_of_t<_Receiver>>; _Receiver __rcvr_; __storage_t __data_; @@ -82,7 +63,7 @@ namespace STDEXEC constexpr void set_value() noexcept { - std::move(__state_->__data_).__complete(__state_->__rcvr_); + std::move(__state_->__data_).complete(static_cast<_Receiver&&>(__state_->__rcvr_)); } template @@ -128,26 +109,6 @@ namespace STDEXEC struct __attrs { private: - //! @brief Returns `true` when: - //! - _SetTag is set_error_t, and - //! - _Sender has value completions, and - //! - at least one of the value completions is not nothrow decay-copyable. - //! In that case, error completions can come from the sender's value completions. - template - static consteval bool __has_decay_copy_errors() noexcept - { - if constexpr (__same_as<_SetTag, set_error_t>) - { - if constexpr (__sends...>) - { - return !__cmplsigs::__partitions_of_t...>>::__nothrow_decay_copyable::__values::value; - } - } - return false; - } - _Scheduler __sch_; env_of_t<_Sender> __attrs_; @@ -165,10 +126,9 @@ namespace STDEXEC //! @note If @c _SetTag is @c set_value_t, then we are in the happy path: everything //! succeeded and execution continues on @c _Scheduler. //! - //! Otherwise, if @c _Sender never completes with @c _SetTag, and either @c _SetTag is - //! @c set_stopped_t or decay-copying @c _Sender's value results cannot throw, then a - //! @c _SetTag completion can only come from the scheduler's sender. In this case, return - //! the scheduler's completion scheduler if it has one. + //! Otherwise, if @c _Sender never completes with @c _SetTag, then a @c _SetTag completion + //! can only happen on the scheduler's execution resource. In this case, return the + //! scheduler's completion scheduler if it has one. //! //! Otherwise, if the scheduler's sender never completes with @c _SetTag, then a //! @c _SetTag completion can only come from the original sender, so return the @@ -176,7 +136,6 @@ namespace STDEXEC template requires(__same_as<_SetTag, set_value_t> || __never_sends<_SetTag, _Sender, __fwd_env_t<_Env>...>) - && (!__has_decay_copy_errors<_SetTag, _Env...>()) [[nodiscard]] constexpr auto query(get_completion_scheduler_t<_SetTag>, _Env const &... __env) const noexcept @@ -205,14 +164,8 @@ namespace STDEXEC //! @note If @c _SetTag is @c set_value_t, then we are in the happy path: everything //! succeeded and execution continues on @c _Scheduler. //! - //! Otherwise, if @c _SetTag is @c set_stopped_t or if decay-copying @c _Sender's value - //! results cannot throw, then a @c _SetTag completion can happen on the sender's - //! completion domain (if it has one) or the scheduler's completion domain (if it has - //! one). - //! - //! @note Otherwise, @c _SetTag is @c set_error_t and decay-copying @c _Sender's value - //! results can throw, so error completions can also come from the sender's value - //! completions. + //! Otherwise, a @c _SetTag completion can happen on the sender's completion domain (if it + //! has one) or the scheduler's completion domain (if it has one). template <__same_as _SetTag, class... _Env> [[nodiscard]] constexpr auto @@ -225,7 +178,6 @@ namespace STDEXEC //! @overload template <__one_of _SetTag, class... _Env> - requires(!__has_decay_copy_errors<_SetTag, _Env...>()) [[nodiscard]] constexpr auto query(get_completion_domain_t<_SetTag>, _Env const &...) const noexcept -> __unless_one_of_t< @@ -237,21 +189,6 @@ namespace STDEXEC return {}; } - //! @overload - template - requires(__has_decay_copy_errors<_SetTag, _Env...>()) - [[nodiscard]] - constexpr auto - query(get_completion_domain_t<_SetTag>, _Env const &...) const noexcept -> __unless_one_of_t< - __common_domain_t< - __completion_domain_of_t<_SetTag, _Sender, __fwd_env_t<_Env>...>, - __completion_domain_of_t<_SetTag, schedule_result_t<_Scheduler>, __fwd_env_t<_Env>...>, - __completion_domain_of_t...>>, - indeterminate_domain<>> - { - return {}; - } - //! @brief Queries the completion behavior of the combined sender. //! @tparam _Env The environment to consider when querying for the completion behavior. //! @note The completion behavior is the minimum between the scheduler's sender and @@ -295,12 +232,8 @@ namespace STDEXEC STDEXEC::get_completion_signatures<_Child, __fwd_env_t<_Env>...>(); STDEXEC_IF_OK(__child_completions) { - // continues_on has the completions of the child sender, but with value and - // error result types decayed. - return __transform_completion_signatures( - __child_completions, - __decay_arguments(), - __decay_arguments()); + using __storage_t = __completion_storage_t<_Child, __fwd_env_t<_Env>...>; + return __storage_t::get_completion_signatures(); } } @@ -339,9 +272,7 @@ namespace STDEXEC auto __child_completions = __get_child_completions<__child_t, _Env...>(); return __concat_completion_signatures( __child_completions, - __get_scheduler_completions<__scheduler_t, _Env...>(), - __eptr_completion_unless_t< - __nothrow_decay_copyable_results_t>()); + __get_scheduler_completions<__scheduler_t, _Env...>()); } static constexpr auto __get_state = @@ -364,22 +295,7 @@ namespace STDEXEC { // Write the tag and the args into the operation state so that we can forward the completion // from within the scheduler's execution context. - if constexpr (__nothrow_callable<__mktuple_t, _Tag, _Args...>) - { - __state.__data_.__emplace_from(__mktuple, __tag, static_cast<_Args&&>(__args)...); - } - else - { - STDEXEC_TRY - { - __state.__data_.__emplace_from(__mktuple, __tag, static_cast<_Args&&>(__args)...); - } - STDEXEC_CATCH_ALL - { - STDEXEC::set_error(static_cast<_State&&>(__state).__rcvr_, std::current_exception()); - return; - } - } + __state.__data_.arrive(__tag, static_cast<_Args&&>(__args)...); // Enqueue the schedule operation so the completion happens on the scheduler's execution // context. @@ -423,15 +339,15 @@ namespace STDEXEC //! the sender produced by continues_on(sndr, sched) has the same //! completion signatures as @c sndr, except that a //! @c set_error_t(std::exception_ptr) completion may be added if any of - //! @c sndr's completion datums are not @c nothrow decay-copyable (the - //! datums must be stored across the scheduling hop). + //! @c sndr's completion datums cannot be stored without throwing (the datums + //! must be stored across the scheduling hop). //! //! @c continues_on does *not* alter @c sndr's values or errors; it only //! changes the execution context on which the completion is delivered. //! //! **Exception behavior.** //! - //! If decay-copying a completion datum across the scheduling hop throws, + //! If storing a completion datum across the scheduling hop throws, //! the exception is delivered through @c set_error_t(std::exception_ptr). //! If scheduling onto @c sched fails, an error completion is delivered on //! an unspecified execution agent. @@ -483,7 +399,7 @@ namespace STDEXEC //! //! @returns A sender with the same completion signatures as @c __sndr //! (plus a possible @c set_error_t(std::exception_ptr) for - //! decay-copy failures during the hop). The completions are + //! storage failures during the hop). The completions are //! delivered on @c __sched's execution resource. template constexpr auto diff --git a/include/stdexec/__detail/__finally.hpp b/include/stdexec/__detail/__finally.hpp index 994bcb2c4..1c46ea53f 100644 --- a/include/stdexec/__detail/__finally.hpp +++ b/include/stdexec/__detail/__finally.hpp @@ -24,9 +24,8 @@ #include "__schedulers.hpp" #include "__sender_adaptor_closure.hpp" #include "__senders.hpp" -#include "__storage.hpp" +#include "__storage_for_completion_signatures.hpp" #include "__transform_completion_signatures.hpp" -#include "__tuple.hpp" #include "__utility.hpp" #include "__prologue.hpp" @@ -77,14 +76,15 @@ namespace STDEXEC struct __result_variant_fn { template - using __f = __storage_for_t<_InitialSender, env_of_t<_Receiver>>; + using __f = storage_for_completion_signatures< + __completion_signatures_of_t<_InitialSender, env_of_t<_Receiver>>>; }; template <> struct __result_variant_fn { template - using __f = __results_storage<>; + using __f = storage_for_completion_signatures>; }; // If the final sender has no value completions, then we don't need to store the @@ -101,7 +101,7 @@ namespace STDEXEC _Receiver __rcvr_{}; STDEXEC_ATTRIBUTE(no_unique_address) _Env2 const __env2_{}; - _Storage __result_; // __variant<__tuple, ...> + _Storage __result_; }; using __mk_secondary_env_t = @@ -122,7 +122,7 @@ namespace STDEXEC constexpr void set_value() noexcept { - std::move(__opstate_->__result_).__complete(__opstate_->__rcvr_); + std::move(__opstate_->__result_).complete(static_cast<_Receiver&&>(__opstate_->__rcvr_)); } template @@ -177,21 +177,9 @@ namespace STDEXEC } else { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Args...>; - this->__result_.template emplace<__tuple_t>(static_cast<_Args&&>(__args)...); - (*__cleanup_callback_)(this); - STDEXEC::start(this->__final_opstate_); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_Args...>) - { - (*__cleanup_callback_)(this); - STDEXEC::set_error(static_cast<_Receiver&&>(this->__rcvr_), std::current_exception()); - } - } + this->__result_.arrive(static_cast<_Args&&>(__args)...); + (*__cleanup_callback_)(this); + STDEXEC::start(this->__final_opstate_); } } @@ -301,8 +289,7 @@ namespace STDEXEC { // If the finally sender doesn't have set_value completions, then we // don't need to worry about the initial sender's value types not being - // nothrow decay-copyable, because they won't be propagated to the - // receiver. + // storable, because they won't be propagated to the receiver. return STDEXEC::__transform_completion_signatures(__initial_completions, __ignore_completion(), {}, @@ -323,15 +310,19 @@ namespace STDEXEC } else { - // The finally sender's completion signatures are ... - return STDEXEC::__concat_completion_signatures( - // ... the initial sender's completions with value types decayed ... - STDEXEC::__transform_completion_signatures( - __initial_completions, - __decay_arguments()), - // ... and the final sender's error and stopped completions ... - STDEXEC::__transform_completion_signatures(__final_completions, - __ignore_completion())); + using __initial_storage_t = + storage_for_completion_signatures; + auto __stored_initial_completions = __initial_storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_initial_completions) + { + // The finally sender's completion signatures are ... + return STDEXEC::__concat_completion_signatures( + // ... the initial sender's completions as stored for replay ... + __stored_initial_completions, + // ... and the final sender's error and stopped completions ... + STDEXEC::__transform_completion_signatures(__final_completions, + __ignore_completion())); + } } } } diff --git a/include/stdexec/__detail/__matching_completion_signature.hpp b/include/stdexec/__detail/__matching_completion_signature.hpp new file mode 100644 index 000000000..83d3678be --- /dev/null +++ b/include/stdexec/__detail/__matching_completion_signature.hpp @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2026 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "__completion_signatures.hpp" + +#include +#include + +namespace STDEXEC +{ + + namespace __matching_completion_signature + { + + template + struct __canonicalize; + + template + struct __canonicalize<_Tag(_Args...)> + { + using type = _Tag(_Args&&...); + }; + + template + using __canonicalize_t = typename __canonicalize<_Signature>::type; + + template + inline constexpr bool __is_canonical_match = + std::is_same_v<__canonicalize_t<_Needle>, __canonicalize_t<_Signature>>; + + template + struct __canonical_match_count; + + template + struct __canonical_match_count<_Needle, completion_signatures<_Signatures...>> + : std::integral_constant + ... + 0)> + {}; + + template + struct __find_canonical_match; + + template + struct __find_canonical_match<_Needle> + {}; + + template + struct __find_canonical_match<_Needle, _Signature, _Rest...> + : std::conditional_t<__is_canonical_match<_Needle, _Signature>, + std::type_identity<_Signature>, + __find_canonical_match<_Needle, _Rest...>> + {}; + + template ::value> + struct __matching_completion_signature + {}; + + template + struct __matching_completion_signature, _Signature, 1> + : __find_canonical_match<_Signature, _Signatures...> + {}; + + } // namespace __matching_completion_signature + + template + using matching_completion_signature_t = + typename __matching_completion_signature::__matching_completion_signature<_Signatures, + _Tag(_Args...)>::type; + + template + inline constexpr bool has_matching_completion_signature_v = requires { + typename matching_completion_signature_t<_Signatures, _Tag, _Args...>; + }; + + template + struct matching_completion_signature + { + using type = matching_completion_signature_t<_Signatures, _Tag, _Args...>; + }; + + template + struct has_matching_completion_signature + : std::bool_constant> + {}; + +} // namespace STDEXEC diff --git a/include/stdexec/__detail/__spawn_future.hpp b/include/stdexec/__detail/__spawn_future.hpp index 624025cbe..48e2fe654 100644 --- a/include/stdexec/__detail/__spawn_future.hpp +++ b/include/stdexec/__detail/__spawn_future.hpp @@ -35,8 +35,7 @@ import stdexec; # include "__scope_concepts.hpp" # include "__senders.hpp" # include "__spawn_common.hpp" -# include "__storage.hpp" -# include "__tuple.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__type_traits.hpp" # if !STDEXEC_USE_MODULES() @@ -49,13 +48,6 @@ import stdexec; namespace STDEXEC { - template - using __decayed_signature_t = __mapply<__mtransform<__q1<__decay_t>, __q<__fn_t>>, _Signature>; - - template - inline constexpr bool __nothrow_storable_signature = - __mapply<__qq<__nothrow_decay_copyable_t>, _Signature>::value; - ///////////////////////////////////////////////////////////////////////////// // [exec.spawn.future] namespace __spawn_future @@ -96,24 +88,41 @@ namespace STDEXEC template struct __future_completions { - // this case handles _NothrowSyncWork == true - // this case is applicable when both conditions are true: - // - the results of all possible completions can be decay-copied into the decayed-tuple that - // stores the results for later consumption by the future; and - // - the stop token provided by the future's receiver can no-throw-construct a stop callback - // in the future's operation state - using type = __mcall<__munique<__qq>, - set_stopped_t(), - __decayed_signature_t<_Sigs>...>; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + // This case handles _NothrowSyncWork == true, which means the stop token provided by the + // future's receiver can no-throw-construct a stop callback in the future's operation state. + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; + using type = typename __storage_t::completion_signatures; + + static consteval auto get_completion_signatures() + { + return __storage_t::get_completion_signatures(); + } }; template struct __future_completions { - using type = __mcall<__munique<__qq>, - set_stopped_t(), - set_error_t(std::exception_ptr), - __decayed_signature_t<_Sigs>...>; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; + using type = + __concat_completion_signatures_t>; + + static consteval auto get_completion_signatures() + { + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return __concat_completion_signatures( + __stored_completions, + completion_signatures{}); + } + } }; template @@ -124,9 +133,7 @@ namespace STDEXEC template using __future_completions_t = - __future_completions<__stop_callback_is_nothrow_constructible<_Env> - && (__nothrow_storable_signature<_Sigs> && ...), - _Sigs...>::type; + __future_completions<__stop_callback_is_nothrow_constructible<_Env>, _Sigs...>::type; // [exec.spawn.future] paragraph 3 template @@ -135,11 +142,21 @@ namespace STDEXEC template struct __spawn_future_state_base> : __try_cancelable { - using __variant_t = __results_storage; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; template using __completions_t = __future_completions_t<_Env, _Sigs...>; - __variant_t __result_; + template + static consteval auto __get_completion_signatures() + { + return __future_completions<__stop_callback_is_nothrow_constructible<_Env>, + _Sigs...>::get_completion_signatures(); + } + + __storage_t __result_; explicit __spawn_future_state_base( void (*__try_cancel)(__try_cancelable*) noexcept, @@ -191,20 +208,7 @@ namespace STDEXEC template void __set_complete(_Ts&&... __ts) noexcept { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Tag, _Ts...>; - __state_->__result_.template emplace<__tuple_t>(_Tag{}, static_cast<_Ts&&>(__ts)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_Ts...>) - { - using tuple_t = __decayed_tuple; - __state_->__result_.template emplace(set_error_t{}, std::current_exception()); - } - } - + __state_->__result_.arrive(_Tag{}, static_cast<_Ts&&>(__ts)...); __state_->__complete(); } }; @@ -229,7 +233,7 @@ namespace STDEXEC using __op_t = connect_result_t<__future_spawned_sender<_Sender, _Env>, __receiver_t>; - using __base = __spawn_future_state_base>; + using __base = __spawn_future_state_base<__sigs_t>; __spawn_future_state(_Alloc __alloc, _Sender&& __sndr, _Token __token, _Env __env) : __base(__do_try_cancel, __do_complete) @@ -391,7 +395,7 @@ namespace STDEXEC // NOTE: __rcvr's type is unconstrained because the thing we pass doesn't satisfy receiver void __do_consume(auto& __rcvr) noexcept { - std::move(this->__result_).__complete(__rcvr); + std::move(this->__result_).complete(std::move(__rcvr)); } static void __do_complete(__base* __base_ptr) noexcept @@ -693,10 +697,9 @@ namespace STDEXEC using __completions_t = __spawn_future_state_t<_Sender>::template __completions_t<_Env>; template - static consteval auto __get_completion_signatures() // - -> __completions_t<_Sender, _Env> + static consteval auto __get_completion_signatures() { - return {}; + return __spawn_future_state_t<_Sender>::template __get_completion_signatures<_Env>(); }; static constexpr auto __get_state = diff --git a/include/stdexec/__detail/__storage.hpp b/include/stdexec/__detail/__storage.hpp deleted file mode 100644 index fa83cb490..000000000 --- a/include/stdexec/__detail/__storage.hpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2021-2025 NVIDIA Corporation - * - * Licensed under the Apache License Version 2.0 with LLVM Exceptions - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * https://llvm.org/LICENSE.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#pragma once - -#include "__execution_fwd.hpp" - -#include "__get_completion_signatures.hpp" -#include "__meta.hpp" -#include "__sender_concepts.hpp" -#include "__tuple.hpp" -#include "__variant.hpp" - -#include "__prologue.hpp" - -namespace STDEXEC -{ - namespace __detail - { - struct __applier - { - template - constexpr void operator()(_Receiver& __rcvr, _Tag, _Args&&... __args) noexcept - { - _Tag()(static_cast<_Receiver&&>(__rcvr), static_cast<_Args&&>(__args)...); - } - }; - - struct __visitor - { - template - constexpr void operator()(_Receiver& __rcvr, _Tuple&& __tupl) noexcept - { - __apply(__applier(), static_cast<_Tuple&&>(__tupl), __rcvr); - } - }; - - // Add storage for an exception_ptr if the result datums are not all nothrow - // decay-copyable. - template - using __tuples_for_t = - __if<__nothrow_decay_copyable_t<_Args...>, - __mlist<__decayed_tuple<_Tag, _Args...>>, - __mlist<__decayed_tuple<_Tag, _Args...>, __tuple>>; - } // namespace __detail - - // A variant type that is capable of storing the result datums of the specified - // completion signatures. - template - struct __results_storage - : __mcall<__mconcat<__qq<__uniqued_variant>>, - __mapply_q<__detail::__tuples_for_t, _Signatures>...> - { - constexpr __results_storage() noexcept - : __results_storage::__variant(__no_init) - {} - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr STDEXEC_EXPLICIT_THIS_BEGIN(void __complete)(this _Self&& __self, - _Receiver& __rcvr) noexcept - { - __visit(__detail::__visitor(), static_cast<_Self&&>(__self), __rcvr); - } - STDEXEC_EXPLICIT_THIS_END(__complete) - }; - - template - requires sender_in<_CvSender, _Env> - using __storage_for_t = - __mapply_q<__results_storage, __completion_signatures_of_t<_CvSender, _Env>>; -} // namespace STDEXEC - -#include "__epilogue.hpp" diff --git a/include/stdexec/__detail/__storage_for_completion_signatures.hpp b/include/stdexec/__detail/__storage_for_completion_signatures.hpp new file mode 100644 index 000000000..2e215d90d --- /dev/null +++ b/include/stdexec/__detail/__storage_for_completion_signatures.hpp @@ -0,0 +1,734 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../functional.hpp" +#include "__config.hpp" +#include "__matching_completion_signature.hpp" +#include "__receivers.hpp" +#include "__transform_completion_signatures.hpp" +#include "__tuple.hpp" +#include "__variant.hpp" + +#include +#include +#include +#include + +namespace STDEXEC +{ + + template + struct storage_for_completion_signature; + + template + struct storage_for_completion_signature<_Tag(_Args...)> + { + using tag_type = _Tag; + using signature_type = _Tag(_Args...); + using __normalized_signature_t = _Tag(_Args&&...); + + private: + using __tuple_t = __tuple<_Args...>; + + struct __forward_as_tuple_t + { + template + constexpr std::tuple<_Ts&&...> operator()(_Ts&&... __args) const noexcept + { + return {static_cast<_Ts&&>(__args)...}; + } + }; + + struct __forward_as_internal_tuple_t + { + template + STDEXEC_ATTRIBUTE(host, device) + constexpr __tuple<_Ts&&...> operator()(_Ts&&... __args) const noexcept + { + return {static_cast<_Ts&&>(__args)...}; + } + }; + + public: + template + requires(sizeof...(_OtherArgs) == sizeof...(_Args)) + && (std::is_constructible_v<_Args, _OtherArgs> && ...) + STDEXEC_ATTRIBUTE(host, device) + constexpr explicit storage_for_completion_signature(_Tag, _OtherArgs&&... __args) + noexcept((std::is_nothrow_constructible_v<_Args, _OtherArgs> && ...)) + : __args_{static_cast<_OtherArgs&&>(__args)...} + {} + + STDEXEC_ATTRIBUTE(host, device) + static constexpr auto tag() noexcept -> _Tag + { + return {}; + } + + constexpr auto forward_arguments() & noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, __args_); + } + + constexpr auto forward_arguments() const & noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, __args_); + } + + constexpr auto forward_arguments() && noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, static_cast<__tuple_t&&>(__args_)); + } + + constexpr auto forward_arguments() const && noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, static_cast<__tuple_t const &&>(__args_)); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() & noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, __args_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() const & noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, __args_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() && noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, static_cast<__tuple_t&&>(__args_)); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() const && noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, + static_cast<__tuple_t const &&>(__args_)); + } + + private: + __tuple_t __args_; + }; + + namespace __completion_storage + { + + template + using __storage_for_arrival_t = storage_for_completion_signature< + matching_completion_signature_t<_Signatures, _Tag, _Args...>>; + + template + struct __variant_for_signatures; + + template + struct __variant_for_signatures> + { + using __t = __uniqued_variant...>; + }; + + template + struct __nothrow_storable; + + template + struct __nothrow_storable<_Tag(_Args...)> + { + static constexpr bool value = (std::is_nothrow_constructible_v<_Args, _Args> && ...); + }; + + template + struct __arrival_storable; + + template + struct __arrival_storable, _Args...> + { + static constexpr bool value = + sizeof...(_StoredArgs) == sizeof...(_Args) + && ((std::is_reference_v<_StoredArgs> || !std::is_lvalue_reference_v<_Args>) && ...); + }; + + struct __ambiguous_normalized_completion_signatures; + struct __non_persistable_completion_signature; + + template + struct __persistable_completion_signature; + + template + struct __persistable_completion_signature<_Tag(_Args...)> + { + static constexpr bool value = + ((std::is_reference_v<_Args> || std::is_move_constructible_v<_Args>) && ...); + }; + + template + struct __persistable_completion_signatures; + + template + struct __persistable_completion_signatures> + { + static constexpr bool value = (__persistable_completion_signature<_Signatures>::value && ...); + }; + + template + struct __normalized_completion_signatures; + + template + struct __normalized_completion_signatures> + { + using __t = transform_completion_signatures::__normalized_signature_t...>>; + }; + + template + using __normalized_completion_signatures_t = + typename __normalized_completion_signatures<_Signatures>::__t; + + template + inline constexpr bool __has_unique_normalized_completion_signatures = + __mapply<__msize, _Signatures>::value + == __mapply<__msize, __normalized_completion_signatures_t<_Signatures>>::value; + + template + consteval auto __get_completion_signatures() + { + if constexpr (!__has_unique_normalized_completion_signatures<_InputSignatures>) + { + return __throw_compile_time_error<__ambiguous_normalized_completion_signatures>(); + } + else if constexpr (!__persistable_completion_signatures<_InputSignatures>::value) + { + return __throw_compile_time_error<__non_persistable_completion_signature>(); + } + else + { + return _OutputSignatures{}; + } + } + + template + using __variant_for_storage_t = decltype(std::declval<_Storage>().__get_variant()); + + template + struct __variant_alternatives; + + template + struct __variant_alternatives<__variant<_Alternatives...>> + { + template + using __with_cvref_t = __mlist...>; + }; + + template + using __variant_alternatives_t = typename __variant_alternatives< + std::remove_cvref_t<_Variant>>::template __with_cvref_t<_Variant>; + + template + struct __nothrow_visit_stored_completion; + + template + struct __nothrow_visit_stored_completion<_Visitor, __mlist<_Alternatives...>> + { + static constexpr bool value = __nothrow_invocable<_Visitor, _Alternatives...>; + }; + + template + struct __nothrow_visit_stored_completion<_Visitor, + __mlist<_Alternatives...>, + __mlist<_CurrentAlternatives...>, + _Variants...> + { + static constexpr bool value = + (__nothrow_visit_stored_completion<_Visitor, + __mlist<_Alternatives..., _CurrentAlternatives>, + _Variants...>::value + && ...); + }; + + template + constexpr bool __nothrow_visit_stored_completion_for_variants_v = + __nothrow_invocable<_Visitor> + && __nothrow_visit_stored_completion<_Visitor, + __mlist<>, + __variant_alternatives_t<_Variants>...>::value; + + template + constexpr bool __nothrow_visit_stored_completion_v = + __nothrow_visit_stored_completion_for_variants_v<_Visitor, + __variant_for_storage_t<_Storages>...>; + + template + struct __nothrow_visit_stored_completion_r; + + template + struct __nothrow_visit_stored_completion_r<_Result, _Visitor, __mlist<_Alternatives...>> + { + static constexpr bool value = + std::is_nothrow_invocable_r_v<_Result, _Visitor, _Alternatives...>; + }; + + template + struct __nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<_Alternatives...>, + __mlist<_CurrentAlternatives...>, + _Variants...> + { + static constexpr bool value = + (__nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<_Alternatives..., _CurrentAlternatives>, + _Variants...>::value + && ...); + }; + + template + constexpr bool __nothrow_visit_stored_completion_r_for_variants_v = + std::is_nothrow_invocable_r_v<_Result, _Visitor> + && __nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<>, + __variant_alternatives_t<_Variants>...>::value; + + template + constexpr bool __nothrow_visit_stored_completion_r_v = + __nothrow_visit_stored_completion_r_for_variants_v<_Result, + _Visitor, + __variant_for_storage_t<_Storages>...>; + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) __visit_variant_completion(_Visitor&& __visitor, _Variant&& __variant) + { + return STDEXEC::__visit(static_cast<_Visitor&&>(__visitor), + static_cast<_Variant&&>(__variant)); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) + __visit_present_stored_completion(_Visitor&& __visitor, _Storage&& __storage) + { + return __completion_storage::__visit_variant_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage).__get_variant()); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) __visit_present_stored_completion(_Visitor&& __visitor, + _Storage&& __storage, + _Storages&&... __storages) + { + return __completion_storage::__visit_variant_completion( + [&](auto&& __completion) -> decltype(auto) + { + return __completion_storage::__visit_present_stored_completion( + [&](auto&&... __completions) -> decltype(auto) + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor), + static_cast(__completion), + static_cast(__completions)...); + }, + static_cast<_Storages&&>(__storages)...); + }, + static_cast<_Storage&&>(__storage).__get_variant()); + } + + } // namespace __completion_storage + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) + visit_stored_completion(_Visitor&& __visitor, _Storage&& __storage, _Storages&&... __storages) + noexcept( + __completion_storage::__nothrow_visit_stored_completion_v<_Visitor, _Storage, _Storages...>) + { + if (!(__storage.has_completion() && ... && __storages.has_completion())) + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + } + return __completion_storage::__visit_present_stored_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>(__storages)...); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr _Result + visit_stored_completion(_Visitor&& __visitor, _Storage&& __storage, _Storages&&... __storages) + noexcept(__completion_storage::__nothrow_visit_stored_completion_r_v<_Result, + _Visitor, + _Storage, + _Storages...>) + { + if (!(__storage.has_completion() && ... && __storages.has_completion())) + { + if constexpr (std::is_void_v<_Result>) + { + STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + return; + } + else + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + } + } + if constexpr (std::is_void_v<_Result>) + { + __completion_storage::__visit_present_stored_completion(static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>( + __storages)...); + return; + } + else + { + return __completion_storage::__visit_present_stored_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>(__storages)...); + } + } + + enum class storage_for_completion_signatures_error_policy + { + internalize, + propagate + }; + + template + class storage_for_completion_signatures; + + template <> + class storage_for_completion_signatures< + completion_signatures<>, + storage_for_completion_signatures_error_policy::internalize> + { + public: + using completion_signatures = STDEXEC::completion_signatures<>; + static constexpr bool nothrow_arrive = true; + + static consteval auto get_completion_signatures() noexcept + { + return completion_signatures{}; + } + + STDEXEC_ATTRIBUTE(host, device) + static constexpr __variant<> __get_variant() noexcept + { + return __variant<>(__no_init); + } + + STDEXEC_ATTRIBUTE(host, device) + static constexpr bool has_completion() noexcept + { + return false; + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&&) && noexcept + { + return false; + } + }; + + template <> + class storage_for_completion_signatures, + storage_for_completion_signatures_error_policy::propagate> + : public storage_for_completion_signatures< + completion_signatures<>, + storage_for_completion_signatures_error_policy::internalize> + {}; + + template + class storage_for_completion_signatures< + completion_signatures<_Signatures...>, + storage_for_completion_signatures_error_policy::internalize> + { + using __input_completion_signatures_t = + transform_completion_signatures>; + static constexpr auto __nothrow = (__completion_storage::__nothrow_storable<_Signatures>::value + && ...); + using __maybe_throwing_signature_t = + std::conditional_t<__nothrow, + STDEXEC::completion_signatures<>, + STDEXEC::completion_signatures>; + public: + using completion_signatures = transform_completion_signatures<__input_completion_signatures_t, + __maybe_throwing_signature_t>; + static constexpr bool nothrow_arrive = true; + + static consteval auto get_completion_signatures() + { + return __completion_storage::__get_completion_signatures<__input_completion_signatures_t, + completion_signatures>(); + } + + private: + template + using __storage_for_arrival_t = + __completion_storage::__storage_for_arrival_t<__input_completion_signatures_t, _Tag, _Args...>; + using __storage_t = + typename __completion_storage::__variant_for_signatures::__t; + __storage_t __storage_; + public: + STDEXEC_ATTRIBUTE(host, device) + constexpr storage_for_completion_signatures() noexcept + : __storage_(__no_init) + {} + + template + requires std::is_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...> + && __completion_storage::__arrival_storable<__storage_for_arrival_t<_Tag, _Args...>, + _Args...>::value + STDEXEC_ATTRIBUTE(host, device) + constexpr void arrive(_Tag __tag, _Args&&... __args) noexcept + { + STDEXEC_ASSERT(__storage_.__is_valueless()); + constexpr auto __nothrow = + std::is_nothrow_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...>; + auto const __impl = [&]() noexcept(__nothrow) + { + __storage_.template emplace<__storage_for_arrival_t<_Tag, _Args...>>((_Tag&&) __tag, + (_Args&&) __args...); + }; + if constexpr (__nothrow) + { + __impl(); + } + else + { + STDEXEC_TRY + { + __impl(); + } + STDEXEC_CATCH_ALL + { + __storage_ + .template emplace>( + set_error, + std::current_exception()); + } + } + } + STDEXEC_ATTRIBUTE(host, device) + constexpr bool has_completion() const noexcept + { + return !__storage_.__is_valueless(); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t& __get_variant() & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const & __get_variant() const & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t&& __get_variant() && noexcept + { + return static_cast<__storage_t&&>(__storage_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const && __get_variant() const && noexcept + { + return static_cast<__storage_t const &&>(__storage_); + } + + template _Receiver> + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&& __rcvr) && noexcept + { + if (!has_completion()) + { + return false; + } + STDEXEC::__visit( + [&](auto&& __active_alternative) noexcept + { + using __completion_t = std::remove_cvref_t; + constexpr auto __nothrow = + std::is_nothrow_constructible_v<__completion_t, __completion_t&&>; + auto const __complete = [&]() noexcept(__nothrow) + { + auto __completion = __completion_t(std::move(__active_alternative)); + STDEXEC::__apply( + [&](auto&&... __args) noexcept + { + typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...); + }, + // Odds are this is inside an operation state, which means that + // sending the completion signal may end our lifetime, which means + // we shouldn't send references into ourselves, therefore we move + // all the non-references onto the stack + std::move(__completion).__forward_arguments()); + }; + if constexpr (__nothrow) + { + __complete(); + } + else + { + STDEXEC_TRY + { + __complete(); + } + STDEXEC_CATCH_ALL + { + set_error((_Receiver&&) __rcvr, std::current_exception()); + } + } + }, + (__storage_t&&) __storage_); + return true; + } + }; + + template + class storage_for_completion_signatures, + storage_for_completion_signatures_error_policy::propagate> + { + using __input_completion_signatures_t = + transform_completion_signatures>; + public: + using completion_signatures = __input_completion_signatures_t; + static constexpr bool nothrow_arrive = + (__completion_storage::__nothrow_storable<_Signatures>::value && ...); + + static consteval auto get_completion_signatures() + { + return __completion_storage::__get_completion_signatures<__input_completion_signatures_t, + completion_signatures>(); + } + + private: + template + using __storage_for_arrival_t = + __completion_storage::__storage_for_arrival_t<__input_completion_signatures_t, _Tag, _Args...>; + using __storage_t = + typename __completion_storage::__variant_for_signatures::__t; + __storage_t __storage_; + + public: + STDEXEC_ATTRIBUTE(host, device) + constexpr storage_for_completion_signatures() noexcept + : __storage_(__no_init) + {} + + template + requires std::is_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...> + && __completion_storage::__arrival_storable<__storage_for_arrival_t<_Tag, _Args...>, + _Args...>::value + STDEXEC_ATTRIBUTE(host, device) + constexpr void arrive(_Tag __tag, _Args&&... __args) noexcept( + std::is_nothrow_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...>) + { + STDEXEC_ASSERT(__storage_.__is_valueless()); + STDEXEC_TRY + { + __storage_.template emplace<__storage_for_arrival_t<_Tag, _Args...>>((_Tag&&) __tag, + (_Args&&) __args...); + } + STDEXEC_CATCH_ALL + { + STDEXEC_THROW(); + } + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr bool has_completion() const noexcept + { + return !__storage_.__is_valueless(); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t& __get_variant() & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const & __get_variant() const & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t&& __get_variant() && noexcept + { + return static_cast<__storage_t&&>(__storage_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const && __get_variant() const && noexcept + { + return static_cast<__storage_t const &&>(__storage_); + } + + template _Receiver> + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&& __rcvr) && noexcept(nothrow_arrive) + { + if (!has_completion()) + { + return false; + } + STDEXEC::__visit( + [&](auto&& __active_alternative) noexcept(nothrow_arrive) + { + using __completion_t = std::remove_cvref_t; + auto __completion = __completion_t(std::move(__active_alternative)); + STDEXEC::__apply( + [&](auto&&... __args) noexcept( + noexcept(typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...))) + { + typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...); + }, + std::move(__completion).__forward_arguments()); + }, + (__storage_t&&) __storage_); + return true; + } + }; + +} // namespace STDEXEC diff --git a/include/stdexec/__detail/__tuple.hpp b/include/stdexec/__detail/__tuple.hpp index 82255c293..cb96d7964 100644 --- a/include/stdexec/__detail/__tuple.hpp +++ b/include/stdexec/__detail/__tuple.hpp @@ -80,7 +80,8 @@ namespace STDEXEC { return static_cast<_Fn&&>(__fn)( static_cast<_Us&&>(__us)..., - static_cast<_Self&&>(__self).STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value...); + static_cast<__copy_cvref_t<_Self&&, _Ts>>( + static_cast<_Self&&>(__self).STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value)...); } }; @@ -232,7 +233,10 @@ namespace STDEXEC template STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE __tuple(_Ts...) -> __tuple<_Ts...>; -# define STDEXEC_TUPLE_GET(_Idx) , static_cast<_Tuple&&>(__tupl).__val##_Idx +# define STDEXEC_TUPLE_GET(_Idx) \ + , static_cast< \ + __copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val##_Idx)>>( \ + static_cast<_Tuple&&>(__tupl).__val##_Idx) // // __apply(fn, tuple, extra...) @@ -376,35 +380,43 @@ namespace STDEXEC } else if constexpr (_Index == 0) { - return static_cast<_Tuple&&>(__tupl).__val0; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val0)>>( + static_cast<_Tuple&&>(__tupl).__val0); } else if constexpr (_Index == 1) { - return static_cast<_Tuple&&>(__tupl).__val1; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val1)>>( + static_cast<_Tuple&&>(__tupl).__val1); } else if constexpr (_Index == 2) { - return static_cast<_Tuple&&>(__tupl).__val2; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val2)>>( + static_cast<_Tuple&&>(__tupl).__val2); } else if constexpr (_Index == 3) { - return static_cast<_Tuple&&>(__tupl).__val3; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val3)>>( + static_cast<_Tuple&&>(__tupl).__val3); } else if constexpr (_Index == 4) { - return static_cast<_Tuple&&>(__tupl).__val4; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val4)>>( + static_cast<_Tuple&&>(__tupl).__val4); } else if constexpr (_Index == 5) { - return static_cast<_Tuple&&>(__tupl).__val5; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val5)>>( + static_cast<_Tuple&&>(__tupl).__val5); } else if constexpr (_Index == 6) { - return static_cast<_Tuple&&>(__tupl).__val6; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val6)>>( + static_cast<_Tuple&&>(__tupl).__val6); } else if constexpr (_Index == 7) { - return static_cast<_Tuple&&>(__tupl).__val7; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val7)>>( + static_cast<_Tuple&&>(__tupl).__val7); } else { diff --git a/include/stdexec/__detail/__when_all.hpp b/include/stdexec/__detail/__when_all.hpp index 22358a3b7..daec0cdfb 100644 --- a/include/stdexec/__detail/__when_all.hpp +++ b/include/stdexec/__detail/__when_all.hpp @@ -37,6 +37,7 @@ import stdexec; # include "__optional.hpp" # include "__schedulers.hpp" # include "__senders.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__transform_completion_signatures.hpp" # include "__tuple.hpp" # include "__type_traits.hpp" @@ -48,6 +49,7 @@ import stdexec; # include "__atomic.hpp" # if !STDEXEC_USE_MODULES() # include +# include # endif # include "__prologue.hpp" @@ -108,14 +110,15 @@ namespace STDEXEC //! @code{.cpp} //! set_value_t(V1..., V2..., ..., Vn...) // concatenation of every input //! set_error_t(Eij)... // union across all inputs - //! set_error_t(std::exception_ptr) // added if any decay-copy may throw + //! set_error_t(std::exception_ptr) // added if storing a completion may throw //! set_stopped_t() // added if any input has it //! @endcode //! - //! The value datums of each input are decay-copied into the resulting - //! sender's state while it waits for the slowest input to finish; the - //! final tuple is built from those decay-copies. If any decay-copy - //! throws, the operation transitions to the error path. + //! Non-reference value datums are move-constructed into the resulting + //! sender's state while it waits for the slowest input to finish; reference + //! datums remain references. The final tuple is built from those stored + //! values. If storing a value throws, the operation transitions to the error + //! path. //! //! **Concurrency.** //! @@ -439,16 +442,29 @@ namespace STDEXEC __fn_t<_WITH_ENVIRONMENT_, _Env>...>; template - using __set_error_t = completion_signatures)>; + using __set_error_t = completion_signatures; template - using __nothrow_decay_copyable_results_t = - STDEXEC::__nothrow_decay_copyable_results_t<__completion_signatures_of_t<_Sender, _Env...>>; + using __value_signatures_t = decltype(__completion_signatures_of_t<_Sender, _Env...>{}.__select( + set_value)); + + template + using __value_storage_for_signatures_t = + storage_for_completion_signatures<_Signatures, + storage_for_completion_signatures_error_policy::propagate>; + + template + using __value_storage_t = + __value_storage_for_signatures_t<__value_signatures_t<_Sender, _Env...>>; + + template + using __nothrow_storable_values_t = + __mbool<__value_storage_t<_Sender, _Env...>::nothrow_arrive>; template inline constexpr bool __can_fail = !__never_sends || sends_stopped<_Sender, _Env> - || !__nothrow_decay_copyable_results_t<_Sender, _Env>::value; + || !__nothrow_storable_values_t<_Sender, _Env>::value; template inline constexpr bool __uses_stop_source = (__can_fail<_Senders, _Env> || ...); @@ -461,8 +477,8 @@ namespace STDEXEC { // TODO(ericniebler): check that all senders have a common completion domain template - using __all_nothrow_decay_copyable_results_t = - __mand<__nothrow_decay_copyable_results_t<_Senders, _Env...>...>; + using __all_nothrow_storable_values_t = + __mand<__nothrow_storable_values_t<_Senders, _Env...>...>; template using __value_tuple_t = __minvoke<__if_c<(0 == sizeof...(_Rest)), @@ -473,7 +489,7 @@ namespace STDEXEC template using __single_values_of_t = __value_types_t<__completion_signatures_of_t<_Sender, _Env...>, - __mtransform<__q<__decay_t>, __q<__mlist>>, + __qq<__mlist>, __mbind_front_q<__value_tuple_t, _Sender>>; template @@ -482,15 +498,43 @@ namespace STDEXEC __minvoke<__mconcat<__qf>, __single_values_of_t<_Senders>...>>; template - using __f = __minvoke_q< - __concat_completion_signatures_t, - __minvoke_q<__eptr_completion_unless_t, __all_nothrow_decay_copyable_results_t<_Senders...>>, - __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, _Senders...>, + using __nonvalue_input_sigs_t = __concat_completion_signatures_t< + __minvoke_q<__eptr_completion_unless_t, __all_nothrow_storable_values_t<_Senders...>>, __transform_reduce_completion_signatures_t<__completion_signatures_of_t<_Senders, _Env...>, __mconst>::__f, __set_error_t, completion_signatures, __concat_completion_signatures_t>...>; + + template + using __nonvalue_sigs_t = typename storage_for_completion_signatures< + __nonvalue_input_sigs_t<_Senders...>>::completion_signatures; + + template + using __f = __minvoke_q< + __concat_completion_signatures_t, + __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, _Senders...>, + __nonvalue_sigs_t<_Senders...>>; + + template + static consteval auto __get() + { + auto __value_completions = __concat_completion_signatures( + __value_storage_t<_Senders, _Env...>::get_completion_signatures()...); + STDEXEC_IF_OK(__value_completions) + { + using __nonvalue_storage_t = + storage_for_completion_signatures<__nonvalue_input_sigs_t<_Senders...>>; + auto __nonvalue_completions = __nonvalue_storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__nonvalue_completions) + { + return __concat_completion_signatures( + __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, + _Senders...>{}, + __nonvalue_completions); + } + } + } }; template @@ -501,6 +545,12 @@ namespace STDEXEC { template using __f = __completions<>::template __f<_Senders...>; + + template + static consteval auto __get() + { + return __completions<>::template __get<_Senders...>(); + } }; template @@ -508,46 +558,53 @@ namespace STDEXEC { template using __f = __completions<__env_t<_Env, _Senders...>>::template __f<_Senders...>; + + template + static consteval auto __get() + { + return __completions<__env_t<_Env, _Senders...>>::template __get<_Senders...>(); + } }; template constexpr void __set_values(_Receiver& __rcvr, _ValuesTuple& __values) noexcept { + static_assert(__tuple_size_v<_ValuesTuple> != 0); STDEXEC::__apply( - [&](OptTuples&&... __opt_vals) noexcept -> void + [&](auto&... __value_storages) noexcept -> void { - STDEXEC::__cat_apply(__mk_completion_fn(set_value, __rcvr), - *static_cast(__opt_vals)...); + STDEXEC::visit_stored_completion( + STDEXEC::__overload{[]() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto& __completion, auto&... __completions) noexcept -> void + { + STDEXEC::__cat_apply( + __mk_completion_fn(set_value, __rcvr), + std::move(__completion).__forward_arguments(), + std::move(__completions).__forward_arguments()...); + }}, + __value_storages...); }, - static_cast<_ValuesTuple&&>(__values)); + __values); } - template - using __values_opt_tuple_t = value_types_of_t<_Sender, _ChildEnv, __decayed_tuple, __optional>; - template requires(__max1_sender<_Senders, __env_t<_Env, _Senders...>> && ...) struct __traits { using __child_env = __env_t<_Env, _Senders...>; - // tuple>, optional>, ...> - using __values_tuple = - __minvoke<__mwith_default< - __mtransform<__mbind_front_q<__values_opt_tuple_t, __child_env>, __q<__tuple>>, - __ignore>, - _Senders...>; - - using __collect_errors = __mbind_front_q<__mset_insert, __mset<>>; + // tuple + using __values_tuple = __minvoke< + __mwith_default<__mtransform<__mbind_back_q<__value_storage_t, __child_env>, __q<__tuple>>, + __ignore>, + _Senders...>; - using __errors_list = - __minvoke<__mconcat<>, - __if<__mand<__nothrow_decay_copyable_results_t<_Senders, __child_env>...>, - __mlist<>, - __mlist>, - __error_types_of_t<_Senders, __child_env, __q<__mlist>>...>; - - using __errors_variant = __mapply<__q<__uniqued_variant>, __errors_list>; + using __completion_storage = storage_for_completion_signatures< + typename __completions<__child_env>::template __nonvalue_input_sigs_t<_Senders...>>; static constexpr bool __uses_stop_source = __when_all::__uses_stop_source<_Env, _Senders...>; }; @@ -582,7 +639,7 @@ namespace STDEXEC _State* __state_; }; - template (__rcvr_)); + __completion_.arrive(set_stopped); + static_cast<_CompletionStorage&&>(__completion_) + .complete(static_cast<_Receiver&&>(__rcvr_)); break; } // This is reachable because the stop callback sets __stopped whether @@ -632,11 +691,12 @@ namespace STDEXEC } break; case __error: - if constexpr (!__same_as<_ErrorsVariant, __variant<>>) + if constexpr (!__same_as>) { // One or more child operations completed with an error: - STDEXEC::__visit(__mk_completion_fn(set_error, __rcvr_), - static_cast<_ErrorsVariant&&>(__errors_)); + static_cast<_CompletionStorage&&>(__completion_) + .complete(static_cast<_Receiver&&>(__rcvr_)); } break; default:; @@ -650,7 +710,8 @@ namespace STDEXEC __stop_source_t __stop_source_{}; // Could be non-atomic here and atomic_ref everywhere except __completion_fn __std::atomic<__state_t> __state_{__started}; - _ErrorsVariant __errors_{__no_init}; + STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS + _CompletionStorage __completion_{}; STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS _ValuesTuple __values_{}; STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS @@ -708,15 +769,15 @@ namespace STDEXEC return [&](__ignore, __ignore, _Child&&...) noexcept requires(__max1_sender<_Child, __env_t, _Child...>> && ...) { - using _Traits = __traits, _Child...>; - using _ErrorsVariant = _Traits::__errors_variant; - using _ValuesTuple = _Traits::__values_tuple; - using _ChildEnv = _Traits::__child_env; - using _State = __state<_ErrorsVariant, - _ValuesTuple, - _Receiver, - (sends_stopped<_Child, _ChildEnv> || ...), - _Traits::__uses_stop_source>; + using _Traits = __traits, _Child...>; + using _CompletionStorage = _Traits::__completion_storage; + using _ValuesTuple = _Traits::__values_tuple; + using _ChildEnv = _Traits::__child_env; + using _State = __state<_CompletionStorage, + _ValuesTuple, + _Receiver, + (sends_stopped<_Child, _ChildEnv> || ...), + _Traits::__uses_stop_source>; return _State{static_cast<_Receiver&&>(__rcvr), sizeof...(_Child)}; }; } @@ -729,6 +790,18 @@ namespace STDEXEC template using __completions_t = __children_of<_Self, __when_all::__completions_for<_Env...>>; + template + struct __get_completions; + + template + struct __get_completions<_Completions, __mlist<_Children...>> + { + static consteval auto __f() + { + return _Completions::template __get<_Children...>(); + } + }; + static constexpr auto __get_attrs = [](__ignore, __ignore, _Child const &...) noexcept { @@ -741,8 +814,9 @@ namespace STDEXEC static_assert(__sender_for<_Self, when_all_t>); if constexpr (__minvocable_q<__completions_t, _Self, _Env...>) { - // TODO: update this to use constant evaluation: - return __completions_t<_Self, _Env...>{}; + using __completions = __when_all::__completions_for<_Env...>; + using __children = __children_of<_Self>; + return __get_completions<__completions, __children>::__f(); } else if constexpr (sizeof...(_Env) == 0) { @@ -805,21 +879,7 @@ namespace STDEXEC case __stopped: // We are the first child to complete with an error, so we must save the error. (Any // subsequent errors are ignored.) - if constexpr (__nothrow_decay_copyable<_Error>) - { - __state.__errors_.template emplace<__decay_t<_Error>>(static_cast<_Error&&>(__err)); - } - else - { - STDEXEC_TRY - { - __state.__errors_.template emplace<__decay_t<_Error>>(static_cast<_Error&&>(__err)); - } - STDEXEC_CATCH_ALL - { - __state.__errors_.template emplace(std::current_exception()); - } - } + __state.__completion_.arrive(set_error, static_cast<_Error&&>(__err)); break; case __error:; // We're already in the "error" state. Ignore the error. } @@ -850,20 +910,16 @@ namespace STDEXEC } else if constexpr (!__same_as<_ValuesTuple, __ignore>) { - auto& __opt_values = STDEXEC::__get<_Index::value>(__state.__values_); - using _Tuple = __decayed_tuple<_Args...>; - static_assert(__same_as, - "One of the senders in this when_all() is fibbing about what types it " - "sends"); - if constexpr ((__nothrow_decay_copyable<_Args> && ...)) + auto& __value = STDEXEC::__get<_Index::value>(__state.__values_); + if constexpr (noexcept(__value.arrive(set_value, static_cast<_Args&&>(__args)...))) { - __opt_values.emplace(_Tuple{static_cast<_Args&&>(__args)...}); + __value.arrive(set_value, static_cast<_Args&&>(__args)...); } else { STDEXEC_TRY { - __opt_values.emplace(_Tuple{static_cast<_Args&&>(__args)...}); + __value.arrive(set_value, static_cast<_Args&&>(__args)...); } STDEXEC_CATCH_ALL { diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index 9f5f6a4da..fe6070b84 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -37,6 +37,7 @@ #include "__detail/__intrusive_slist.hpp" #include "__detail/__just.hpp" #include "__detail/__let.hpp" +#include "__detail/__matching_completion_signature.hpp" #include "__detail/__meta.hpp" #include "__detail/__on.hpp" #include "__detail/__operation_states.hpp" @@ -55,6 +56,7 @@ #include "__detail/__starts_on.hpp" #include "__detail/__stopped_as_error.hpp" #include "__detail/__stopped_as_optional.hpp" +#include "__detail/__storage_for_completion_signatures.hpp" #include "__detail/__submit.hpp" #include "__detail/__sync_wait.hpp" #include "__detail/__task.hpp" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf974fbc1..0a2b6fae6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -87,7 +87,9 @@ set(stdexec_test_sources stdexec/queries/test_get_completion_behavior.cpp stdexec/queries/test_forwarding_queries.cpp stdexec/types/test_task.cpp - stdexec/types/test_counting_scopes.cpp) + stdexec/types/test_counting_scopes.cpp + stdexec/types/test_matching_completion_signature.cpp + stdexec/types/test_storage_for_completion_signatures.cpp) add_library(common_test_settings INTERFACE) set_target_properties( diff --git a/test/exec/sequence/test_ignore_all_values.cpp b/test/exec/sequence/test_ignore_all_values.cpp index 935774f30..ef2fbb217 100644 --- a/test/exec/sequence/test_ignore_all_values.cpp +++ b/test/exec/sequence/test_ignore_all_values.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "exec/just_from.hpp" #include "exec/sequence/empty_sequence.hpp" #include "exec/sequence/ignore_all_values.hpp" @@ -65,6 +66,31 @@ namespace CHECK_FALSE(STDEXEC::sync_wait(sndr)); } + TEST_CASE("ignore_all_values - preserve error references", + "[sequence_senders][ignore_all_values]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } error; + + auto source = exec::just_error_from([&](auto sink) noexcept { return sink(error); }); + auto sndr = exec::ignore_all_values(std::move(source)); + using sender_t = decltype(sndr); + using actual_t = STDEXEC::completion_signatures_of_t>; + using expected_t = STDEXEC::__mset; + STATIC_REQUIRE(STDEXEC::__mset_eq); + + auto recovered = std::move(sndr) + | STDEXEC::upon_error([&](immovable &actual) noexcept + { CHECK(&actual == &error); }); + CHECK(STDEXEC::sync_wait(std::move(recovered))); + } + #if !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("ignore_all_values - ignore just_error()", "[sequence_senders][ignore_all_values]") { diff --git a/test/exec/test_any_sender.cpp b/test/exec/test_any_sender.cpp index 74b82e058..8c988991c 100644 --- a/test/exec/test_any_sender.cpp +++ b/test/exec/test_any_sender.cpp @@ -282,7 +282,7 @@ namespace #if !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("any_sender uses overload rules for completion signatures", "[types][any_sender]") { - auto split_sender = exec::split(ex::just(42)); + auto split_sender = exec::split(ex::just(42) | ex::then([](int value) { return value; })); STATIC_REQUIRE( ex::sender_of); STATIC_REQUIRE(ex::sender_of); diff --git a/test/exec/test_ensure_started.cpp b/test/exec/test_ensure_started.cpp index bc5139741..7218fb9a6 100644 --- a/test/exec/test_ensure_started.cpp +++ b/test/exec/test_ensure_started.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -111,6 +112,35 @@ namespace ex::start(op); } + TEST_CASE("ensure_started preserves reference results", "[adaptors][ensure_started]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return ex::completion_signatures{}; + }); + auto sndr = exec::ensure_started(std::move(source)); + using sndr_t = decltype(sndr); + static_assert( + set_equivalent>, + ex::completion_signatures>); + + auto checked = std::move(sndr) + | ex::then([&](immovable &result) noexcept { CHECK(&result == &value); }); + auto op = ex::connect(std::move(checked), expect_void_receiver{}); + ex::start(op); + } + TEST_CASE("ensure_started multiple values early", "[adaptors][ensure_started]") { bool called{false}; diff --git a/test/exec/test_fork_join.cpp b/test/exec/test_fork_join.cpp index 5f1635242..26e089e9c 100644 --- a/test/exec/test_fork_join.cpp +++ b/test/exec/test_fork_join.cpp @@ -104,15 +104,40 @@ namespace return (is + ...); })); STATIC_REQUIRE(sender_in>); - STATIC_REQUIRE(set_equivalent< - completion_signatures_of_t>, - completion_signatures>); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); auto [i1, i2] = sync_wait(sndr).value(); CHECK(i1 == 42); CHECK(i2 == 42); } + TEST_CASE("fork_join preserves reference results", "[adaptors][fork_join]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return completion_signatures{}; + }); + auto check_reference = then([&](immovable &result) noexcept { CHECK(&result == &value); }); + + auto sndr = exec::fork_join(std::move(source), check_reference, check_reference); + STATIC_REQUIRE(sender_in>); + sync_wait(std::move(sndr)); + } + TEST_CASE("fork_join with empty value channel", "[adaptors][fork_join]") { auto sndr = ::STDEXEC::just() | ::STDEXEC::then([]() noexcept -> void {}) diff --git a/test/exec/test_split.cpp b/test/exec/test_split.cpp index 5e9353db9..24f959cef 100644 --- a/test/exec/test_split.cpp +++ b/test/exec/test_split.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ +#include #include #include #include @@ -118,6 +119,36 @@ namespace // The receiver will ensure that the right value is produced } + TEST_CASE("split preserves reference results", "[adaptors][split]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return ex::completion_signatures{}; + }); + auto split = exec::split(std::move(source)); + using split_t = decltype(split); + static_assert( + set_equivalent>, + ex::completion_signatures>); + + auto check_reference = ex::then([&](immovable &result) noexcept { CHECK(&result == &value); }); + auto op1 = ex::connect(split | check_reference, expect_void_receiver{}); + auto op2 = ex::connect(split | check_reference, expect_void_receiver{}); + ex::start(op1); + ex::start(op2); + } + TEST_CASE("split forwards errors", "[adaptors][split]") { SECTION("of exception_ptr type") @@ -137,11 +168,18 @@ namespace auto split = exec::split(ex::just_error(42)); using split_t = decltype(split); using error_t = ex::error_types_of_t, ex::__mmake_set>; - static_assert(ex::__mset_eq>); + static_assert(ex::__mset_eq>); auto op = ex::connect(split, expect_error_receiver{}); ex::start(op); } + + SECTION("from storing a potentially throwing result") + { + using split_t = decltype(exec::split(ex::just(potentially_throwing{}))); + using error_t = ex::error_types_of_t, ex::__mmake_set>; + static_assert(ex::__mset_eq>); + } } TEST_CASE("split forwards stop signal", "[adaptors][split]") @@ -536,7 +574,7 @@ namespace REQUIRE(v1 == 1); REQUIRE(v2 == 2); - REQUIRE(v3 == 1); + REQUIRE(v3 == 2); } TEST_CASE("split doesn't advertise completion scheduler", "[adaptors][split]") diff --git a/test/exec/test_when_any.cpp b/test/exec/test_when_any.cpp index 58df5951c..70577e8e8 100644 --- a/test/exec/test_when_any.cpp +++ b/test/exec/test_when_any.cpp @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -54,6 +55,16 @@ namespace ex::start(op); } + TEST_CASE("when_any preserves reference value completions", "[adaptors][when_any]") + { + auto source = exec::split(ex::just(42)); + auto snd = exec::when_any(std::move(source)); + static_assert(set_equivalent, + completion_signatures>); + auto op = ex::connect(std::move(snd), expect_value_receiver{42}); + ex::start(op); + } + TEST_CASE("when_any completes with only one sender", "[adaptors][when_any]") { ex::sender auto snd = exec::when_any(completes_if{false} | ex::then([] { return 1; }), @@ -215,9 +226,13 @@ namespace } }; - TEST_CASE("when_any - with duplicate completions", "[adaptors][when_any]") + TEST_CASE("when_any rejects ambiguous normalized completions", "[adaptors][when_any]") { - REQUIRE_THROWS(STDEXEC::sync_wait(exec::when_any(dup_sender{}))); + using sender_t = decltype(exec::when_any(dup_sender{})); + using completions_t = + decltype(sender_t::template get_completion_signatures>()); + + static_assert(ex::__merror); } #endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() } // namespace diff --git a/test/stdexec/algos/adaptors/test_continues_on.cpp b/test/stdexec/algos/adaptors/test_continues_on.cpp index c4a829af3..535242b96 100644 --- a/test/stdexec/algos/adaptors/test_continues_on.cpp +++ b/test/stdexec/algos/adaptors/test_continues_on.cpp @@ -31,6 +31,144 @@ using namespace std::chrono_literals; namespace { + constinit int global_int = 0; + constinit double global_double = 0.0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int *value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + int *value_; + }; + + struct references_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int *int_value_; + double *double_value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), int_value_, double_value_}; + } + + int *int_value_; + double *double_value_; + }; + + struct value_domain + {}; + + struct error_domain + {}; + + struct throwing_value_domain_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + struct attrs + { + constexpr auto + query(ex::get_completion_domain_t) const noexcept -> value_domain + { + return {}; + } + + constexpr auto + query(ex::get_completion_domain_t) const noexcept -> error_domain + { + return {}; + } + }; + + constexpr auto get_env() const noexcept -> attrs + { + return {}; + } + }; + + struct error_domain_scheduler + { + using scheduler_concept = ex::scheduler_tag; + + struct sender + { + using sender_concept = ex::sender_tag; + using completion_signatures = + ex::completion_signatures; + + struct attrs + { + template + constexpr auto query(ex::get_completion_domain_t<_Tag>) const noexcept -> error_domain + { + return {}; + } + }; + + constexpr auto get_env() const noexcept -> attrs + { + return {}; + } + }; + + constexpr auto schedule() const noexcept -> sender + { + return {}; + } + + auto operator==(error_domain_scheduler const &) const noexcept -> bool = default; + }; + struct potentially_throwing_connect_scheduler { using scheduler_concept = ex::scheduler_tag; @@ -101,6 +239,54 @@ namespace // The receiver checks if we receive the right value } + TEST_CASE("continues_on preserves a single reference completion", "[adaptors][continues_on]") + { + global_int = 42; + auto raw = ex::continues_on(reference_sender{&global_int}, inline_scheduler{}); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto &&i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("continues_on preserves multiple reference completion arguments", + "[adaptors][continues_on]") + { + global_int = 42; + global_double = 0.125; + auto raw = ex::continues_on(references_sender{&global_int, &global_double}, inline_scheduler{}); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto &&i, auto &&d) noexcept + { + CHECK(&i == &global_int); + CHECK(&d == &global_double); + CHECK(i == 42); + CHECK(d == 0.125); + }); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("continues_on does not report the source value domain for persistence errors", + "[adaptors][continues_on]") + { + using attrs = ex::__trnsfr::__attrs; + + auto domain = attrs{error_domain_scheduler{}, throwing_value_domain_sender::attrs{}} + .query(ex::get_completion_domain, ex::env<>{}); + STATIC_REQUIRE(std::same_as); + } + TEST_CASE("continues_on can be piped", "[adaptors][continues_on]") { // Just transfer a value to the impulse scheduler diff --git a/test/stdexec/algos/adaptors/test_finally.cpp b/test/stdexec/algos/adaptors/test_finally.cpp index 05b67bf45..6d9b8c1cd 100644 --- a/test/stdexec/algos/adaptors/test_finally.cpp +++ b/test/stdexec/algos/adaptors/test_finally.cpp @@ -19,10 +19,121 @@ #include +#include + using namespace STDEXEC; namespace { + constinit int global_int = 0; + constinit double global_double = 0.0; + + struct reference_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + int* value_; + }; + + struct references_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* int_value_; + double* double_value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), int_value_, double_value_}; + } + + int* int_value_; + double* double_value_; + }; + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct throws_on_move + { + throws_on_move() = default; + throws_on_move(throws_on_move const &) = delete; + throws_on_move(throws_on_move&&) + { + throw std::runtime_error("Throwing as requested"); + } + }; + + struct throwing_value_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + throws_on_move* value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), std::move(*value_)); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + throws_on_move* value_; + }; +#endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("finally is a sender", "[adaptors][finally]") { @@ -65,7 +176,67 @@ namespace CHECK(i == 42); } + TEST_CASE("finally preserves a single reference completion", "[adaptors][finally]") + { + bool called = false; + global_int = 42; + auto raw = exec::finally(reference_sender{&global_int}, + just() | then([&called]() noexcept { called = true; })); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + auto s = std::move(raw) + | then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + + sync_wait(s); + CHECK(called); + } + + TEST_CASE("finally preserves multiple reference completion arguments", "[adaptors][finally]") + { + bool called = false; + global_int = 42; + global_double = 0.125; + auto raw = exec::finally(references_sender{&global_int, &global_double}, + just() | then([&called]() noexcept { called = true; })); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + auto s = std::move(raw) + | then( + [](auto&& i, auto&& d) noexcept + { + CHECK(&i == &global_int); + CHECK(&d == &global_double); + CHECK(i == 42); + CHECK(d == 0.125); + }); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + + sync_wait(s); + CHECK(called); + } + #if !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("finally executes the final action when storing the initial completion throws", + "[adaptors][finally]") + { + bool called = false; + throws_on_move value; + + auto s = exec::finally(throwing_value_sender{&value}, + just() | then([&called]() noexcept { called = true; })); + + CHECK_THROWS_AS(sync_wait(s), std::runtime_error); + CHECK(called); + } + TEST_CASE("finally does not execute the final action and throws integer", "[adaptors][finally]") { bool called = false; diff --git a/test/stdexec/algos/adaptors/test_on.cpp b/test/stdexec/algos/adaptors/test_on.cpp index 6300b09cd..f0f0b18ed 100644 --- a/test/stdexec/algos/adaptors/test_on.cpp +++ b/test/stdexec/algos/adaptors/test_on.cpp @@ -20,11 +20,50 @@ #include #include #include +#include namespace ex = STDEXEC; namespace { + constinit int global_int = 0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + auto get_env() const noexcept + { + return ex::prop{ex::get_completion_scheduler, inline_scheduler{}}; + } + + int* value_; + }; + template inline auto _make_env_with_sched(Sched sched = {}) { @@ -119,6 +158,52 @@ namespace REQUIRE(called); } + TEST_CASE("STDEXEC::on preserves reference completions in whole-sender form", "[adaptors][on]") + { + global_int = 42; + auto raw = ex::on(inline_scheduler{}, reference_sender{&global_int}); + STATIC_REQUIRE( + set_equivalent< + ex::completion_signatures_of_t, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }) + | _with_scheduler(); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("STDEXEC::on preserves reference completions in closure form", "[adaptors][on]") + { + global_int = 42; + auto raw = reference_sender{&global_int} + | ex::on(inline_scheduler{}, + ex::then( + [](int& i) noexcept -> int& + { + CHECK(&i == &global_int); + CHECK(i == 42); + return i; + })); + STATIC_REQUIRE(set_equivalent, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }) + | _with_scheduler(); + + ex::sync_wait(std::move(snd)); + } + TEST_CASE("STDEXEC::on can be called with rvalue ref scheduler", "[adaptors][on]") { auto env = _make_env_with_sched(); diff --git a/test/stdexec/algos/adaptors/test_spawn_future.cpp b/test/stdexec/algos/adaptors/test_spawn_future.cpp index d44931853..2fe84ed4b 100644 --- a/test/stdexec/algos/adaptors/test_spawn_future.cpp +++ b/test/stdexec/algos/adaptors/test_spawn_future.cpp @@ -31,6 +31,39 @@ namespace ex = STDEXEC; namespace { + constinit int global_int = 0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && noexcept -> operation + { + return {static_cast(receiver), value_}; + } + + int* value_; + }; + TEST_CASE("future completion signature calculation works", "[adaptors][spawn_future]") { { @@ -56,9 +89,8 @@ namespace } { - using expected = ex::completion_signatures; + using expected = + ex::completion_signatures; using actual = ex::__spawn_future::__future_completions_t, ex::set_value_t(std::string const &)>; @@ -66,6 +98,25 @@ namespace } } + TEST_CASE("spawn_future preserves reference completions", "[adaptors][spawn_future]") + { + global_int = 42; + auto future = ex::spawn_future(reference_sender{&global_int}, null_token{}); + using expected = ex::completion_signatures; + using actual = ex::completion_signatures_of_t>; + STATIC_REQUIRE(actual{} == expected{}); + + auto snd = std::move(future) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + + ex::sync_wait(std::move(snd)); + } + TEST_CASE("spawn_future(just(...)) is equivalent to just(...)", "[adaptors][spawn_future]") { constexpr auto checkEquivalence = [](ex::sender auto const & sender) diff --git a/test/stdexec/algos/adaptors/test_when_all.cpp b/test/stdexec/algos/adaptors/test_when_all.cpp index 202d0a702..1f851f34e 100644 --- a/test/stdexec/algos/adaptors/test_when_all.cpp +++ b/test/stdexec/algos/adaptors/test_when_all.cpp @@ -68,6 +68,93 @@ namespace } }; + struct error_that_throws_when_persisted + { + error_that_throws_when_persisted() = default; + + error_that_throws_when_persisted(error_that_throws_when_persisted &&) + { + throw std::runtime_error("error persistence failed"); + } + }; + + struct throws_when_error_is_persisted_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + using operation_state_concept = ex::operation_state_tag; + + Receiver receiver_; + + void start() & noexcept + { + ex::set_error(std::move(receiver_), error_that_throws_when_persisted{}); + } + }; + + template + auto connect(Receiver receiver) const -> operation + { + return {std::move(receiver)}; + } + }; + + template + struct advertises_value_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + }; + + struct not_move_constructible + { + not_move_constructible() = default; + not_move_constructible(not_move_constructible &&) = delete; + not_move_constructible(not_move_constructible const &) = delete; + }; + + struct expect_rvalue_value_receiver + { + using receiver_concept = ex::receiver_t; + + void set_value(int &&i, double &&d) && noexcept + { + CHECK(i == 3); + CHECK(d == 0.1415); + } + + template + void set_error(Error &&) && noexcept + { + FAIL("unexpected error"); + } + + void set_stopped() && noexcept + { + FAIL("unexpected stopped"); + } + + auto get_env() const noexcept -> ex::env<> + { + return {}; + } + }; + TEST_CASE("when_all returns a sender", "[adaptors][when_all]") { auto snd = ex::when_all(ex::just(3), ex::just(0.1415)); @@ -120,6 +207,15 @@ namespace ex::start(op); } + TEST_CASE("when_all sends by-value completions as rvalues", "[adaptors][when_all]") + { + auto snd = ex::when_all(ex::just(3), ex::just(0.1415)); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto op = ex::connect(std::move(snd), expect_rvalue_value_receiver{}); + ex::start(op); + } + TEST_CASE("when_all returning two values can we waited on", "[adaptors][when_all]") { ex::sender auto snd = ex::when_all(ex::just(2), ex::just(3)); @@ -325,7 +421,7 @@ namespace CHECK(cancelled); } - TEST_CASE("when_all has the values_type based on the children, decayed and as rvalue references", + TEST_CASE("when_all has the values_type based on the children, preserving references", "[adaptors][when_all]") { check_val_types>>(ex::when_all(ex::just(13))); @@ -342,11 +438,35 @@ namespace check_val_types>>( ex::when_all(ex::just(3), ex::just(), ex::just(0.14))); - // if children send references, they get decayed - check_val_types>>( + // unary when_all coalesces to the child, so references are preserved + check_val_types>>(ex::when_all(exec::split(ex::just(3)))); + + // if multiple children send references, they are preserved + check_val_types>>( ex::when_all(exec::split(ex::just(3)), exec::split(ex::just(0.14)))); } + TEST_CASE("when_all validates every value completion storage", "[adaptors][when_all]") + { +#if STDEXEC_NO_STDCPP_CONSTEXPR_EXCEPTIONS() + using invalid_sender = + decltype(ex::__make_sexpr(ex::__{}, + advertises_value_sender{}, + ex::just())); + using invalid_completions = + decltype(invalid_sender::template get_completion_signatures>()); + STATIC_REQUIRE(ex::__merror); +#endif + + using reference_sender = + decltype(ex::when_all(advertises_value_sender{}, ex::just())); + using reference_completions = + decltype(ex::get_completion_signatures>()); + STATIC_REQUIRE( + set_equivalent>); + } + TEST_CASE("when_all has the error_types based on the children", "[adaptors][when_all]") { check_err_types>(ex::when_all(ex::just_error(13))); @@ -362,6 +482,44 @@ namespace ex::when_all(ex::just(13), ex::just_error(std::exception_ptr{}), ex::just_stopped())); } + TEST_CASE("when_all reports exception_ptr if persisting an error throws", "[adaptors][when_all]") + { + auto snd = ex::when_all(ex::just(), throws_when_error_is_persisted_sender{}); + STATIC_REQUIRE(exec::sender_for); + check_err_types>(snd); + + std::exception_ptr ex; + struct receiver + { + using receiver_concept = ex::receiver_t; + + void set_value() noexcept + { + FAIL("Unexpected value completion"); + } + + void set_stopped() noexcept + { + FAIL("Unexpected stopped completion"); + } + + void set_error(error_that_throws_when_persisted &&) noexcept + { + FAIL("Unexpected original error completion"); + } + + void set_error(std::exception_ptr err) noexcept + { + *ex_ = std::move(err); + } + + std::exception_ptr *ex_; + }; + auto op = ex::connect(std::move(snd), receiver{&ex}); + ex::start(op); + CHECK(ex); + } + TEST_CASE("when_all has sends_stopped == true if and only if at least one child sends stopped", "[adaptors][when_all]") { diff --git a/test/stdexec/types/test_matching_completion_signature.cpp b/test/stdexec/types/test_matching_completion_signature.cpp new file mode 100644 index 000000000..09ad99e8a --- /dev/null +++ b/test/stdexec/types/test_matching_completion_signature.cpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2026 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +namespace +{ + + struct base + {}; + + struct derived : base + {}; + + using signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_value_t(base), + ::STDEXEC::set_value_t(derived&), + ::STDEXEC::set_error_t(std::exception_ptr), + ::STDEXEC::set_stopped_t()>; + + static_assert(std::is_same_v< + ::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_value_t(int)>); + + static_assert(std::is_same_v<::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_error_t(std::exception_ptr)>); + + static_assert( + std::is_same_v<::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_stopped_t()>); + + static_assert( + std::is_same_v< + ::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_value_t(derived&)>); + + static_assert( + ::STDEXEC::has_matching_completion_signature_v); + static_assert( + ::STDEXEC::has_matching_completion_signature::value); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + static_assert(!::STDEXEC::has_matching_completion_signature::value); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + + TEST_CASE("matching_completion_signature compile-time checks", "[matching_completion_signature]") + { + CHECK(true); + } + +} // unnamed namespace diff --git a/test/stdexec/types/test_storage_for_completion_signatures.cpp b/test/stdexec/types/test_storage_for_completion_signatures.cpp new file mode 100644 index 000000000..c7307acbd --- /dev/null +++ b/test/stdexec/types/test_storage_for_completion_signatures.cpp @@ -0,0 +1,949 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + + template + concept stored_completion = requires { typename std::remove_cvref_t::tag_type; }; + + template + concept completion_with_tag = + stored_completion + && std::is_same_v::tag_type, Tag>; + + template + struct overloaded : Fns... + { + using Fns::operator()...; + }; + + template + overloaded(Fns...) -> overloaded; + + template + concept tuple_size_defined_for = requires { typename std::tuple_size::type; }; + + template + concept tuple_element_defined_for = requires { typename std::tuple_element<0, T>::type; }; + + struct monostate_may_throw + { + void operator()(std::monostate) const {} + + template + requires completion_with_tag + void operator()(Completion&&) const noexcept + {} + }; + + static_assert(std::is_same_v<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int const)>); + static_assert(std::is_same_v<::STDEXEC::set_value_t(int*), ::STDEXEC::set_value_t(int[])>); + static_assert( + std::is_same_v<::STDEXEC::set_value_t(int (*)(int)), ::STDEXEC::set_value_t(int(int))>); + + TEST_CASE("A single stored completion exposes its tag and arguments", + "[storage_for_completion_signatures]") + { + using completion = + ::STDEXEC::storage_for_completion_signature<::STDEXEC::set_value_t(int, int&)>; + + static_assert(!std::derived_from>); + static_assert(!tuple_size_defined_for); + static_assert(!tuple_element_defined_for); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert( + std::is_same_v); + static_assert(std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(std::is_same_v); + static_assert(noexcept(completion::tag())); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert( + std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + using internal_arguments = decltype(std::declval().__forward_arguments()); + static_assert( + std::is_same_v(std::declval())), int&&>); + static_assert( + std::is_same_v(std::declval())), int&>); + + int referenced = 42; + completion c{::STDEXEC::set_value, 13, referenced}; + auto args = c.forward_arguments(); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<1>(args) == &referenced); + + auto const & cc = c; + auto cc_args = cc.forward_arguments(); + std::get<1>(cc_args) = 99; + CHECK(referenced == 99); + + ::STDEXEC::__apply( + [](auto&& first, auto&& second) + { + static_assert(std::is_same_v); + static_assert(std::is_same_v); + }, + std::move(c).__forward_arguments()); + + int other = 7; + ::STDEXEC::storage_for_completion_signature<::STDEXEC::set_value_t(int&)> c2{ + ::STDEXEC::set_value, + other}; + bool invoked = false; + std::apply( + [&](auto&&... values) + { + CHECK(!invoked); + invoked = true; + CHECK(sizeof...(values) == 3); + }, + std::tuple_cat(std::move(c).forward_arguments(), c2.forward_arguments())); + CHECK(invoked); + } + + TEST_CASE("An internal argument tuple forwards more than eight values", + "[storage_for_completion_signatures]") + { + using completion = ::STDEXEC::storage_for_completion_signature< + ::STDEXEC::set_value_t(int, int, int, int, int, int, int, int, int)>; + + completion c{::STDEXEC::set_value, 0, 1, 2, 3, 4, 5, 6, 7, 8}; + ::STDEXEC::__apply( + [](_Ts&&... values) + { + static_assert(sizeof...(_Ts) == 9); + static_assert((std::is_same_v<_Ts, int> && ...)); + CHECK((values + ...) == 36); + }, + std::move(c).__forward_arguments()); + } + + template + concept can_arrive_set_value_with = requires(Storage& s, Value&& value) { + s.arrive(::STDEXEC::set_value, static_cast(value)); + }; + + template + concept can_arrive_set_error_with = requires(Storage& s, Error&& error) { + s.arrive(::STDEXEC::set_error, static_cast(error)); + }; + + struct copyable_value + { + copyable_value() = default; + copyable_value(copyable_value const &) noexcept = default; + copyable_value(copyable_value&&) noexcept = default; + }; + + using copyable_value_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(copyable_value)>>; + + static_assert(!can_arrive_set_value_with); + static_assert(can_arrive_set_value_with); + + TEST_CASE("Storing no completion signatures works", "[storage_for_completion_signatures]") + { + ::STDEXEC::storage_for_completion_signatures<::STDEXEC::completion_signatures<>> storage; + static_assert(std::is_empty_v); + static_assert( + std::is_same_v>); + static_assert(noexcept(storage.has_completion())); + CHECK(!storage.has_completion()); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion([&] { visited_empty = true; }, std::move(storage)); + CHECK(visited_empty); + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + explicit receiver(bool& moved) noexcept + : moved_(&moved) + {} + receiver(receiver&& other) noexcept + : moved_(std::exchange(other.moved_, nullptr)) + { + *moved_ = true; + } + void set_value() noexcept + { + FAIL("Unexpected value"); + } + bool* moved_; + }; + bool moved = false; + CHECK(!std::move(storage).complete(receiver{moved})); + CHECK(!moved); + } + + TEST_CASE("Storing simple completion signatures and then visiting them works", + "[storage_for_completion_signatures]") + { + using completion_signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::error_code)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(set_equivalent); + { + storage s; + CHECK(!s.has_completion()); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + std::move(s)); + CHECK(visited_empty); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_stopped))); + s.arrive(::STDEXEC::set_stopped); + CHECK(s.has_completion()); + static_assert(noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) noexcept {}}, + std::move(s)))); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + CHECK(std::tuple_size_v == 0); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_value, 5))); + s.arrive(::STDEXEC::set_value, 5); + CHECK(s.has_completion()); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 5); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_error, std::error_code{}))); + s.arrive(::STDEXEC::set_error, make_error_code(std::errc::no_such_file_or_directory)); + CHECK(s.has_completion()); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == make_error_code(std::errc::no_such_file_or_directory)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + + TEST_CASE("Stored completions can be visited as completion objects", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int, int&)>>; + + int referenced = 42; + storage s; + s.arrive(::STDEXEC::set_value, 13, referenced); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = + static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<1>(args) == &referenced); + } + else + { + FAIL("Unexpected completion"); + } + }}, + s); + CHECK(invoked); + + auto const & cs = s; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + if constexpr (completion_with_tag) + { + static_assert(std::is_const_v>); + auto args = completion.forward_arguments(); + std::get<1>(args) = 99; + } + else + { + FAIL("Unexpected completion"); + } + }}, + cs); + CHECK(referenced == 99); + } + + TEST_CASE("Multiple stored completions can be visited together", + "[storage_for_completion_signatures]") + { + using first_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + using second_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_error_t(std::exception_ptr)>>; + using third_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int&)>>; + + int referenced = 42; + first_storage first; + second_storage second; + third_storage third; + first.arrive(::STDEXEC::set_value, 13); + second.arrive(::STDEXEC::set_error, std::exception_ptr{}); + third.arrive(::STDEXEC::set_value, referenced); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& first_completion, auto&& second_completion, auto&& third_completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag + && completion_with_tag + && completion_with_tag) + { + auto args = std::tuple_cat( + static_cast(first_completion).forward_arguments(), + static_cast(second_completion).forward_arguments(), + static_cast(third_completion).forward_arguments()); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<2>(args) == &referenced); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(first), + std::move(second), + std::move(third)); + CHECK(invoked); + } + + TEST_CASE("Stored completion visitation computes noexcept", "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage s; + static_assert(noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) noexcept {}}, s))); + static_assert( + !noexcept(::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) noexcept {}}, s))); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) {}}, s))); + + using empty_storage = + ::STDEXEC::storage_for_completion_signatures<::STDEXEC::completion_signatures<>>; + empty_storage empty; + static_assert(noexcept(::STDEXEC::visit_stored_completion([]() noexcept {}, empty))); + } + + TEST_CASE("Stored completion visitation collapses empty storage to nullary invocation", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage first; + storage second; + second.arrive(::STDEXEC::set_value, 13); + + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + first); + CHECK(visited_empty); + + bool visited_empty_from_mixed = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty_from_mixed = true; }, + [](auto&&...) { FAIL("Unexpected completion"); }}, + second, + first); + CHECK(visited_empty_from_mixed); + } + + TEST_CASE("Stored completion visitation returns visitor results", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage empty; + storage value; + value.arrive(::STDEXEC::set_value, 42); + + auto const classify = overloaded{ + []() noexcept -> int { return 0; }, + [](auto&& completion) noexcept -> int + { + if constexpr (completion_with_tag) + { + return std::get<0>(static_cast(completion).forward_arguments()); + } + else + { + return -1; + } + }}; + + static_assert( + std::is_same_v); + static_assert(noexcept(::STDEXEC::visit_stored_completion(classify, empty))); + CHECK(::STDEXEC::visit_stored_completion(classify, empty) == 0); + CHECK(::STDEXEC::visit_stored_completion(classify, value) == 42); + + static_assert( + std::is_same_v(classify, value)), long>); + static_assert(noexcept(::STDEXEC::visit_stored_completion(classify, value))); + CHECK(::STDEXEC::visit_stored_completion(classify, value) == 42L); + } + + TEST_CASE("Storing simple completion signatures and then completing a receiver therewith works", + "[storage_for_completion_signatures]") + { + using storage_type = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::exception_ptr)>>; + { + storage_type storage; + storage.arrive(::STDEXEC::set_value); + CHECK(std::move(storage).complete(expect_void_receiver{})); + } + { + std::optional storage(std::in_place); + storage->arrive(::STDEXEC::set_error, std::make_exception_ptr(std::logic_error("TEST"))); + std::exception_ptr ex; + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value() noexcept + { + FAIL("Unexpected value invocation"); + } + void set_stopped() noexcept + { + FAIL("Unexpected stopped invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept + { + // This ensures that the exception_ptr is moved onto the stack + CHECK(storage_); + storage_.reset(); + CHECK(!ex_); + ex_ = std::move(ex); + } + std::optional& storage_; + std::exception_ptr& ex_; + }; + CHECK(std::move(*storage).complete(receiver{storage, ex})); + REQUIRE(ex); + bool threw = false; + try + { + std::rethrow_exception(std::move(ex)); + } + catch (std::logic_error const & ex) + { + threw = true; + CHECK(ex.what() == std::string_view("TEST")); + } + CHECK(threw); + } + } + + TEST_CASE("When moving a stored value onto the stack throws complete reports std::exception_ptr", + "[storage_for_completion_signatures]") + { + struct throws_on_second_move + { + explicit throws_on_second_move(int& moves) noexcept + : moves_(&moves) + {} + throws_on_second_move(throws_on_second_move&& other) + { + moves_ = other.moves_; + ++*moves_; + if (*moves_ == 2) + { + throw std::runtime_error("Throwing as requested"); + } + } + int* moves_; + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(throws_on_second_move)>>; + + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value(throws_on_second_move&&) noexcept + { + FAIL("Unexpected value invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept + { + CHECK(!ex_); + ex_ = std::move(ex); + } + std::exception_ptr& ex_; + }; + + int moves = 0; + storage s; + s.arrive(::STDEXEC::set_value, throws_on_second_move{moves}); + CHECK(moves == 1); + + std::exception_ptr ex; + CHECK(std::move(s).complete(receiver{ex})); + REQUIRE(ex); + bool threw = false; + try + { + std::rethrow_exception(std::move(ex)); + } + catch (std::runtime_error const & ex) + { + threw = true; + CHECK(ex.what() == std::string_view("Throwing as requested")); + } + CHECK(threw); + } + + TEST_CASE("When storing a completion signature would throw it is simply coalesced to " + "std::exception_ptr", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) + { + if (other.throws) + { + throw std::runtime_error("Throwing as requested"); + } + } + bool throws{false}; + }; + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + static_assert(storage::nothrow_arrive); + static_assert(set_equivalent< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move), + ::STDEXEC::set_error_t(std::exception_ptr)>, + storage::completion_signatures>); + { + storage s; + static_assert(noexcept(s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}))); + maybe_throws_on_move obj; + obj.throws = true; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + REQUIRE(std::get<0>(args)); + // TODO? + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (!completion_with_tag) + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + // Important that the below cases don't add the std::exception_ptr completion + // since propagating a reference can't throw + { + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move&)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(std::is_same_v); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, obj); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(&obj == &std::get<0>(args)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move&&)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(std::is_same_v); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(&obj == &std::get<0>(args)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + + TEST_CASE("Storage can propagate persistence exceptions instead of coalescing them", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) + { + if (other.throws) + { + throw std::runtime_error("Throwing as requested"); + } + other.moved = true; + } + bool throws{false}; + bool moved{false}; + }; + + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>; + using storage = ::STDEXEC::storage_for_completion_signatures< + signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert(!storage::nothrow_arrive); + static_assert(std::is_same_v); + static_assert(!noexcept( + std::declval().arrive(::STDEXEC::set_value, std::declval()))); + + storage s; + maybe_throws_on_move obj; + obj.throws = true; + CHECK_THROWS_AS(s.arrive(::STDEXEC::set_value, std::move(obj)), std::runtime_error); + CHECK(!obj.moved); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + std::move(s)); + CHECK(visited_empty); + } + + TEST_CASE("Propagating storage remains noexcept when persistence is noexcept", + "[storage_for_completion_signatures]") + { + using signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>; + using storage = ::STDEXEC::storage_for_completion_signatures< + signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert(storage::nothrow_arrive); + static_assert(std::is_same_v); + storage s; + static_assert(noexcept(s.arrive(::STDEXEC::set_value, 42))); + s.arrive(::STDEXEC::set_value, 42); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[]() noexcept { FAIL("Unexpected completion"); }, + [&](auto&& completion) noexcept + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = + static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 42); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + + TEST_CASE("Storage deduplicates exact duplicate completion signatures", + "[storage_for_completion_signatures]") + { + using duplicate_signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int)>; + using deduplicated_signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>; + + using internalizing_storage = + ::STDEXEC::storage_for_completion_signatures; + using propagating_storage = ::STDEXEC::storage_for_completion_signatures< + duplicate_signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert( + std::is_same_v); + static_assert( + std::is_same_v); + + propagating_storage storage; + storage.arrive(::STDEXEC::set_value, 42); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected empty storage"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + CHECK(std::get<0>( + static_cast(completion).forward_arguments()) + == 42); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(storage)); + CHECK(invoked); + } + + TEST_CASE("Internalized storage errors are not external arrivals", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&&) {} + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + + static_assert( + set_equivalent>); + static_assert(!can_arrive_set_error_with); + } + + TEST_CASE("Storage computes completion signatures through a consteval function", + "[storage_for_completion_signatures]") + { + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int)>>; + + static_assert(std::is_same_v); + } + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&&) {} + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + + static_assert(set_equivalent); + } + } + + TEST_CASE("Storage rejects ambiguous or non-persistable completion signatures", + "[storage_for_completion_signatures]") + { +#if STDEXEC_NO_STDCPP_CONSTEXPR_EXCEPTIONS() + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_value_t(int&&)>>; + + static_assert(::STDEXEC::__merror); + } + { + struct not_move_constructible + { + not_move_constructible() = default; + not_move_constructible(not_move_constructible&&) = delete; + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(not_move_constructible)>>; + + static_assert(::STDEXEC::__merror); + } +#endif + } + +} // unnamed namespace diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 5fb0b4e3d..00dfe21cc 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -26,6 +26,8 @@ # include # include +# include +# include # include @@ -330,9 +332,191 @@ namespace } constinit int global_int = 0; + constinit double global_double = 0.0; constexpr auto wrap_ref = ex::then([](auto &i) noexcept { return std::ref(i); }); + template + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + if constexpr (Inline) + { + return ex::__completion_behavior::__inline_completion; + } + else + { + return ex::__completion_behavior::__asynchronous_affine; + } + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int *value_; + }; + + template + struct references_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * int_value_; + double * double_value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), int_value_, double_value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + if constexpr (Inline) + { + return ex::__completion_behavior::__inline_completion; + } + else + { + return ex::__completion_behavior::__asynchronous_affine; + } + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int * int_value_; + double * double_value_; + }; + + struct non_affine_reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + return ex::__completion_behavior::__asynchronous; + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int *value_; + }; + + template + auto task_awaits_reference_sender() -> ex::task + { + decltype(auto) i = co_await reference_sender{&global_int}; + STATIC_REQUIRE(std::same_as); + CHECK(&i == &global_int); + co_return i; + } + + template + auto task_awaits_references_sender() -> ex::task + { + decltype(auto) values = co_await references_sender{&global_int, &global_double}; + STATIC_REQUIRE(std::same_as>); + CHECK(&std::get<0>(values) == &global_int); + CHECK(&std::get<1>(values) == &global_double); + } + + auto task_awaits_non_affine_reference_sender() -> ex::task + { + auto &&i = co_await non_affine_reference_sender{&global_int}; + CHECK(&i == &global_int); + CHECK(i == 42); + } + auto test_task_of_reference_type() -> ex::task { int &i = co_await []() -> ex::task @@ -365,6 +549,45 @@ namespace CHECK(i == 42); } + TEST_CASE("task co_await preserves a single reference from inline sender", + "[types][task]") + { + global_int = 42; + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + CHECK(i == 42); + } + + TEST_CASE("task co_await preserves a single reference from async sender", + "[types][task]") + { + global_int = 42; + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + CHECK(i == 42); + } + + TEST_CASE("task co_await preserves reference tuple from inline sender", + "[types][task]") + { + global_int = 42; + global_double = 0.125; + ex::sync_wait(task_awaits_references_sender()); + } + + TEST_CASE("task co_await preserves reference tuple from async sender", + "[types][task]") + { + global_int = 42; + global_double = 0.125; + ex::sync_wait(task_awaits_references_sender()); + } + + TEST_CASE("task co_await preserves a reference when affine inserts finally", + "[types][task]") + { + global_int = 42; + ex::sync_wait(task_awaits_non_affine_reference_sender()); + } + struct inline_affine_stopped_sender { using sender_concept = ex::sender_tag;