From 88883afe8cc805eac3222de53c2268a946fbaacf Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Sun, 25 Jan 2026 21:00:44 -0400 Subject: [PATCH 1/2] Clean repeat_ states --- include/exec/repeat_n.hpp | 27 +++++--- include/exec/repeat_until.hpp | 64 ++++++++++++------- include/stdexec/__detail/__bulk.hpp | 4 +- include/stdexec/__detail/__let.hpp | 32 ++++++---- .../__detail/__sender_adaptor_closure.hpp | 5 +- include/stdexec/__detail/__tuple.hpp | 10 +-- test/exec/test_fork.cpp | 9 +++ test/exec/test_repeat_until.cpp | 43 +++++++++++++ test/stdexec/algos/adaptors/test_bulk.cpp | 20 ++++++ 9 files changed, 161 insertions(+), 53 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 6092531a0..546998c5b 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -46,7 +46,10 @@ namespace exec { _Receiver __rcvr_; std::size_t __count_; - trampoline_scheduler __sched_; + trampoline_scheduler __sched_{}; + + protected: + ~__repeat_n_state_base() noexcept = default; }; template @@ -101,7 +104,7 @@ namespace exec { __child_count_pair(_Child, std::size_t) -> __child_count_pair<_Child>; template - struct __repeat_n_state : __repeat_n_state_base<_Receiver> { + struct __repeat_n_state final : __repeat_n_state_base<_Receiver> { using __child_count_pair_t = __decay_t<__data_of<_Sender>>; using __child_t = decltype(__child_count_pair_t::__child_); using __receiver_t = STDEXEC::__t<__receiver<__id<_Sender>, __id<_Receiver>>>; @@ -109,9 +112,12 @@ namespace exec { __result_of, __child_t &>; using __child_op_t = STDEXEC::connect_result_t<__child_on_sched_sender_t, __receiver_t>; - constexpr explicit __repeat_n_state(_Sender &&__sndr, _Receiver &&__rcvr) - : __repeat_n_state_base<_Receiver>{ - static_cast<_Receiver &&>(__rcvr), + constexpr explicit __repeat_n_state(_Sender &&__sndr, _Receiver &&__rcvr) noexcept( + std::is_nothrow_constructible_v<__child_t, STDEXEC::__tuple_element_t<1, _Sender &&>> + && noexcept(__connect())) + : __repeat_n_state_base< + _Receiver + >{static_cast<_Receiver &&>(__rcvr), STDEXEC::__get<1>(static_cast<_Sender &&>(__sndr)).__count_} , __child_(STDEXEC::__get<1>(static_cast<_Sender &&>(__sndr)).__child_) { if (this->__count_ != 0) { @@ -119,7 +125,8 @@ namespace exec { } } - constexpr auto __connect() -> __child_op_t & { + constexpr __child_op_t &__connect() + noexcept(STDEXEC::__nothrow_connectable<__child_on_sched_sender_t, __receiver_t>) { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), @@ -134,11 +141,11 @@ namespace exec { } } - constexpr void __cleanup() noexcept final { + constexpr void __cleanup() noexcept override { __child_op_.reset(); } - constexpr void __repeat() noexcept final { + constexpr void __repeat() noexcept override { STDEXEC_ASSERT(this->__count_ > 0); STDEXEC_TRY { if (--this->__count_ == 0) { @@ -149,7 +156,9 @@ namespace exec { } } STDEXEC_CATCH_ALL { - STDEXEC::set_error(std::move(this->__rcvr_), std::current_exception()); + if constexpr (!STDEXEC::__nothrow_connectable<__child_on_sched_sender_t, __receiver_t>) { + STDEXEC::set_error(std::move(this->__rcvr_), std::current_exception()); + } } } diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 4fdc31423..8a7972389 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -33,19 +33,25 @@ namespace exec { template struct __repeat_state_base { - constexpr explicit __repeat_state_base(_Receiver &&__rcvr) - : __rcvr_{static_cast<_Receiver &&>(__rcvr)} { + constexpr explicit __repeat_state_base(_Receiver &&__rcvr) noexcept + : __rcvr_{std::move(__rcvr)} { + static_assert( + std::is_nothrow_default_constructible_v, + "trampoline_scheduler c'tor is always expected to be noexcept"); } virtual constexpr void __cleanup() noexcept = 0; virtual constexpr void __repeat() noexcept = 0; _Receiver __rcvr_; - trampoline_scheduler __sched_; + trampoline_scheduler __sched_{}; + + protected: + ~__repeat_state_base() noexcept = default; }; - template - concept __bool_constant = __decay_t<_Bool>::value == _Expected; + template + concept __bool_constant = __decay_t<_Boolean>::value == _Expected; template struct __receiver { @@ -55,22 +61,22 @@ namespace exec { using __id = __receiver; using receiver_concept = STDEXEC::receiver_t; - template - constexpr void set_value(_Bools &&...__bools) noexcept { - if constexpr ((__bool_constant<_Bools, true> && ...)) { + template + constexpr void set_value(_Booleans &&...__bools) noexcept { + if constexpr ((__bool_constant<_Booleans, true> && ...)) { // Always done: __state_->__cleanup(); STDEXEC::set_value(std::move(__state_->__rcvr_)); - } else if constexpr ((__bool_constant<_Bools, false> && ...)) { + } else if constexpr ((__bool_constant<_Booleans, false> && ...)) { // Never done: __state_->__repeat(); } else { // Mixed results: constexpr bool __is_nothrow = noexcept( - (static_cast(static_cast<_Bools &&>(__bools)) && ...)); + (static_cast(static_cast<_Booleans &&>(__bools)) && ...)); STDEXEC_TRY { // If the child sender completed with true, we're done - const bool __done = (static_cast(static_cast<_Bools &&>(__bools)) && ...); + const bool __done = (static_cast(static_cast<_Booleans &&>(__bools)) && ...); if (__done) { __state_->__cleanup(); STDEXEC::set_value(std::move(__state_->__rcvr_)); @@ -81,8 +87,7 @@ namespace exec { STDEXEC_CATCH_ALL { if constexpr (!__is_nothrow) { __state_->__cleanup(); - STDEXEC::set_error( - std::move(__state_->__rcvr_), std::current_exception()); + STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception()); } } } @@ -98,8 +103,7 @@ namespace exec { STDEXEC_CATCH_ALL { if constexpr (!__nothrow_decay_copyable<_Error>) { __state_->__cleanup(); - STDEXEC::set_error( - std::move(__state_->__rcvr_), std::current_exception()); + STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception()); } } } @@ -122,20 +126,23 @@ namespace exec { STDEXEC_PRAGMA_IGNORE_GNU("-Wtsan") template - struct __repeat_state : __repeat_state_base<_Receiver> { + struct __repeat_state final : __repeat_state_base<_Receiver> { using __child_t = __decay_t<__data_of<_Sender>>; using __receiver_t = STDEXEC::__t<__receiver<__id<_Receiver>>>; using __child_on_sched_sender_t = __result_of, __child_t &>; using __child_op_t = STDEXEC::connect_result_t<__child_on_sched_sender_t, __receiver_t>; - constexpr explicit __repeat_state(_Sender &&__sndr, _Receiver &&__rcvr) + constexpr explicit __repeat_state(_Sender &&__sndr, _Receiver &&__rcvr) noexcept( + std::is_nothrow_constructible_v<__child_t, STDEXEC::__tuple_element_t<1, _Sender &&>> + && noexcept(__connect())) : __repeat_state_base<_Receiver>(static_cast<_Receiver &&>(__rcvr)) , __child_(STDEXEC::__get<1>(static_cast<_Sender &&>(__sndr))) { __connect(); } - constexpr auto __connect() -> __child_op_t & { + constexpr __child_op_t &__connect() + noexcept(STDEXEC::__nothrow_connectable<__child_on_sched_sender_t, __receiver_t>) { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), @@ -146,16 +153,18 @@ namespace exec { STDEXEC::start(*__child_op_); } - constexpr void __cleanup() noexcept final { + constexpr void __cleanup() noexcept override { __child_op_.reset(); } - constexpr void __repeat() noexcept final { + constexpr void __repeat() noexcept override { STDEXEC_TRY { STDEXEC::start(__connect()); } STDEXEC_CATCH_ALL { - STDEXEC::set_error(static_cast<_Receiver &&>(this->__rcvr_), std::current_exception()); + if constexpr (!STDEXEC::__nothrow_connectable<__child_on_sched_sender_t, __receiver_t>) { + STDEXEC::set_error(static_cast<_Receiver &&>(this->__rcvr_), std::current_exception()); + } } } @@ -202,8 +211,6 @@ namespace exec { __mbind_front_q<__values_t, _Sender>::template __f >; - struct __repeat_tag { }; - struct __repeat_until_tag { }; struct __repeat_until_impl : __sexpr_defaults { @@ -304,4 +311,15 @@ namespace STDEXEC { return STDEXEC::get_completion_signatures<__sndr_t, _Env...>(); } }; + + template <> + struct __sexpr_impl : __sexpr_defaults { + template + static consteval auto get_completion_signatures() { + static_assert(sender_expr_for<_Sender, exec::repeat_t>); + using __sndr_t = + __detail::__transform_sender_result_t>; + return STDEXEC::get_completion_signatures<__sndr_t, _Env...>(); + } + }; } // namespace STDEXEC diff --git a/include/stdexec/__detail/__bulk.hpp b/include/stdexec/__detail/__bulk.hpp index 64cf95333..4dc2f58c2 100644 --- a/include/stdexec/__detail/__bulk.hpp +++ b/include/stdexec/__detail/__bulk.hpp @@ -188,7 +188,7 @@ namespace STDEXEC { > requires is_execution_policy_v> STDEXEC_ATTRIBUTE(host, device) - auto operator()(_Sender&& __sndr, _Policy&& __pol, _Shape __shape, _Fun __fun) const + constexpr auto operator()(_Sender&& __sndr, _Policy&& __pol, _Shape __shape, _Fun __fun) const -> __well_formed_sender auto { return __make_sexpr<_AlgoTag>( __data{__pol, __shape, static_cast<_Fun&&>(__fun)}, static_cast<_Sender&&>(__sndr)); @@ -197,7 +197,7 @@ namespace STDEXEC { template requires is_execution_policy_v> STDEXEC_ATTRIBUTE(always_inline) - auto operator()(_Policy&& __pol, _Shape __shape, _Fun __fun) const { + constexpr auto operator()(_Policy&& __pol, _Shape __shape, _Fun __fun) const { return __closure( *this, static_cast<_Policy&&>(__pol), diff --git a/include/stdexec/__detail/__let.hpp b/include/stdexec/__detail/__let.hpp index a45560c52..05c5b6e91 100644 --- a/include/stdexec/__detail/__let.hpp +++ b/include/stdexec/__detail/__let.hpp @@ -113,7 +113,9 @@ namespace STDEXEC { } [[nodiscard]] - constexpr auto get_env() const noexcept { + constexpr decltype(__env::__join( + __declval(), + __declval>())) get_env() const noexcept { return __env::__join(__env_, STDEXEC::get_env(__rcvr_)); } @@ -648,7 +650,20 @@ namespace STDEXEC { }; template - struct __let_impl : __sexpr_defaults { + class __let_impl : public __sexpr_defaults { + template + using __state_t = __gather_completions_of_t< + _SetTag, + __child_of_t<_Sender>, + __fwd_env_t>, + __q<__decayed_tuple>, + __mbind_front_q< + __state, + _SetTag, + __child_of_t<_Sender>, + __decay_t<__fn_of_t<_Sender>>, + _Receiver>>; + public: static constexpr auto get_attrs = []( const __data<_Child, _Fun>& __data) noexcept -> decltype(auto) { @@ -679,23 +694,14 @@ namespace STDEXEC { static constexpr auto get_state = [](_Sender&& __sndr, _Receiver&& __rcvr) + -> __state_t<_Sender, _Receiver> requires sender_in<__child_of_t<_Sender>, __fwd_env_t>> // TODO(ericniebler): make this conditionally noexcept { static_assert(sender_expr_for<_Sender, __let_tag<_SetTag>>); - using __child_t = __child_of_t<_Sender>; - using __fn_t = __decay_t<__fn_of_t<_Sender>>; - using __mk_state = __mbind_front_q<__state, _SetTag, __child_t, __fn_t, _Receiver>; - using __state_t = __gather_completions_of_t< - _SetTag, - __child_t, - __fwd_env_t>, - __q<__decayed_tuple>, - __mk_state - >; auto& [__tag, __data] = __sndr; auto& [__child, __fn] = __data; - return __state_t( + return __state_t<_Sender, _Receiver>( STDEXEC::__forward_like<_Sender>(__child), STDEXEC::__forward_like<_Sender>(__fn), static_cast<_Receiver&&>(__rcvr)); diff --git a/include/stdexec/__detail/__sender_adaptor_closure.hpp b/include/stdexec/__detail/__sender_adaptor_closure.hpp index 102d1a56f..868cbcb5c 100644 --- a/include/stdexec/__detail/__sender_adaptor_closure.hpp +++ b/include/stdexec/__detail/__sender_adaptor_closure.hpp @@ -93,7 +93,8 @@ namespace STDEXEC { template requires __callable<_Fn, _Sender, _As...> STDEXEC_ATTRIBUTE(host, device, always_inline) - auto operator()(_Sender&& __sndr) && noexcept(__nothrow_callable<_Fn, _Sender, _As...>) { + constexpr auto + operator()(_Sender&& __sndr) && noexcept(__nothrow_callable<_Fn, _Sender, _As...>) { return STDEXEC::__apply( static_cast<_Fn&&>(__fn_), static_cast<__tuple<_As...>&&>(__args_), @@ -103,7 +104,7 @@ namespace STDEXEC { template requires __callable STDEXEC_ATTRIBUTE(host, device, always_inline) - auto operator()(_Sender&& __sndr) const & noexcept( + constexpr auto operator()(_Sender&& __sndr) const & noexcept( __nothrow_callable) { return STDEXEC::__apply(__fn_, __args_, static_cast<_Sender&&>(__sndr)); } diff --git a/include/stdexec/__detail/__tuple.hpp b/include/stdexec/__detail/__tuple.hpp index 52ce15d21..f6818d3fd 100644 --- a/include/stdexec/__detail/__tuple.hpp +++ b/include/stdexec/__detail/__tuple.hpp @@ -219,9 +219,10 @@ namespace STDEXEC { template using __tuple_t = __mcall<_CvRef, __tuple<_Ts...>>; - template ...> _Fn> - void operator()(_Fn&& __fn, __tuple_t<_Ts...>&& __tupl, _Us&&... __us) const - noexcept(__nothrow_callable<_Fn, _Us..., __mcall<_CvRef, _Ts>...>); + template ...> _Fn> + auto operator()(_Fn&& __fn, __tuple_t<_Ts...>&& __tupl, _Us&&... __us) const + noexcept(__nothrow_callable<_Fn, _Us..., __mcall1<_CvRef, _Ts>...>) + -> __call_result_t<_Fn, _Us..., __mcall1<_CvRef, _Ts>...>; }; template @@ -232,7 +233,8 @@ namespace STDEXEC { requires __callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...> STDEXEC_ATTRIBUTE(always_inline, host, device) constexpr auto operator()(_Fn&& __fn, _Tuple&& __tupl, _Us&&... __us) const - noexcept(__nothrow_callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...>) -> decltype(auto) { + noexcept(__nothrow_callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...>) + -> __call_result_t<__impl_t<_Tuple>, _Fn, _Tuple, _Us...> { constexpr size_t __size = STDEXEC_REMOVE_REFERENCE(_Tuple)::__size; if constexpr (__size == 0) { diff --git a/test/exec/test_fork.cpp b/test/exec/test_fork.cpp index 77653becc..f767bc4ad 100644 --- a/test/exec/test_fork.cpp +++ b/test/exec/test_fork.cpp @@ -72,4 +72,13 @@ namespace { CHECK(i1 == 42); CHECK(i2 == 42); } + + TEST_CASE("fork_join with empty value channel", "[adaptors][fork_join]") { + auto sndr = ::STDEXEC::just() | ::STDEXEC::then([]() noexcept -> void { }) + | exec::fork_join( + ::STDEXEC::then([]() noexcept -> void { }), + ::STDEXEC::then([]() noexcept -> void { })); + + ::STDEXEC::sync_wait(std::move(sndr)); + } } // namespace diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index ff864a790..dbf817783 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -17,6 +17,7 @@ #include "exec/repeat_until.hpp" #include "exec/static_thread_pool.hpp" +#include "exec/trampoline_scheduler.hpp" #include "stdexec/execution.hpp" #include @@ -30,6 +31,7 @@ #include #include #include +#include namespace ex = STDEXEC; @@ -269,6 +271,47 @@ namespace { } while (!done); } + TEST_CASE("repeat composes with completion signatures") { + { + ex::sender auto only_stopped = ex::just_stopped() | exec::repeat(); + static_assert( + std::same_as, ex::__detail::__not_a_variant>, + "Expect no value completions"); + static_assert( + std::same_as, std::variant>, + "Missing added set_error_t(std::exception_ptr)"); + static_assert( + ex::sender_of, + "Missing set_stopped_t() from upstream"); + + // operator| and sync_wait require valid completion signatures + ex::sync_wait(only_stopped | ex::upon_stopped([]() noexcept { return -1; })); + } + + { + ex::sender auto only_error = ex::just_error(-1) | exec::repeat(); + static_assert( + std::same_as, ex::__detail::__not_a_variant>, + "Expect no value completions"); + static_assert( + std::same_as< + ex::error_types_of_t, + std::variant + >, + "Missing added set_error_t(std::exception_ptr)"); + + // set_stopped_t is always added as a consequence of the internal trampoline_scheduler + using SC = ex::completion_signatures_of_t>; + static_assert( + !ex::sender_of + || ex::sender_of, + "Missing added set_stopped_t()"); + + // operator| and sync_wait require valid completion signatures + ex::sync_wait(only_error | ex::upon_error([](const auto) { return -1; })); + } + } + TEST_CASE( "repeat_until works correctly when the child operation sends type which throws when " "decay-copied, and when converted to bool, and which is only rvalue convertible to bool", diff --git a/test/stdexec/algos/adaptors/test_bulk.cpp b/test/stdexec/algos/adaptors/test_bulk.cpp index 254a85143..278d1e954 100644 --- a/test/stdexec/algos/adaptors/test_bulk.cpp +++ b/test/stdexec/algos/adaptors/test_bulk.cpp @@ -61,6 +61,26 @@ namespace { } }; + constexpr auto test_constexpr() noexcept { + struct receiver { + using receiver_concept = ex::receiver_t; + constexpr void set_value(const int i) && noexcept { + this->i = i; + } + void set_error(std::exception_ptr) && noexcept { + } + int& i; + }; + int i = 0; + auto op = ex::connect( + ex::just(666) + | ex::bulk(ex::par, 42, [](std::size_t item, auto& val) noexcept { val += item; }), + receiver{i}); + ex::start(op); + return i; + } + static_assert(test_constexpr() == 666 + 42 * 41 / 2); + TEST_CASE("bulk returns a sender", "[adaptors][bulk]") { auto snd = ex::bulk(ex::just(19), ex::par, 8, [](int, int) { }); static_assert(ex::sender); From 5fe222bbe4e8d86c80e847d0a60ec6c074e81760 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Tue, 27 Jan 2026 22:04:41 -0400 Subject: [PATCH 2/2] repeat_* conditionally add set_error_t(std::exception_ptr) --- include/exec/repeat_n.hpp | 24 ++++++-- include/exec/repeat_until.hpp | 28 ++++++++- test/exec/test_repeat_n.cpp | 43 ++++++++++++++ test/exec/test_repeat_until.cpp | 102 +++++++++++++++++++++++--------- 4 files changed, 161 insertions(+), 36 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 546998c5b..9019eb1ae 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -189,16 +189,28 @@ namespace exec { template using __error_t = completion_signatures)>; - template + template + using __errors_nothrow_copyable = STDEXEC::__error_types_t< + STDEXEC::__completion_signatures_of_t<_Sender, _Env...>, // sigs + STDEXEC::__q // variant + >; + + template + using __with_eptr_completion = STDEXEC::__eptr_completion_unless_t, + __mbool>>... + >>; + + template using __completions_t = STDEXEC::transform_completion_signatures< - __completion_signatures_of_t::__child_) &, _Env...>, + __completion_signatures_of_t<_Sender &, _Env...>, STDEXEC::transform_completion_signatures< - __completion_signatures_of_t, _Env...>, - __eptr_completion, + __completion_signatures_of_t, _Env...>, + __with_eptr_completion<_Sender, _Env...>, __cmplsigs::__default_set_value, __error_t >, - __mbind_front_q<__values_t, decltype(__decay_t<_Pair>::__child_)>::template __f, + __mbind_front_q<__values_t, _Sender>::template __f, __error_t >; @@ -208,7 +220,7 @@ namespace exec { template static consteval auto get_completion_signatures() { // TODO: port this to use constant evaluation - return __completions_t<__data_of<_Sender>, _Env...>{}; + return __completions_t>::__child_), _Env...>{}; } static constexpr auto get_state = []( diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 8a7972389..3ae775df7 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -197,6 +197,30 @@ namespace exec { __mexception<_INVALID_ARGUMENT_TO_REPEAT_EFFECT_UNTIL_<>, _WITH_PRETTY_SENDER_<_Sender>> >; + template + using __values_overload_nothrow_bool_convertible = + STDEXEC::__mand...>; + + template + using __values_nothrow_bool_convertible = STDEXEC::__value_types_t< + STDEXEC::__completion_signatures_of_t<_Sender, _Env...>, // sigs + STDEXEC::__qq<__values_overload_nothrow_bool_convertible>, // tuple + STDEXEC::__q // variant + >; + + template + using __errors_nothrow_copyable = STDEXEC::__error_types_t< + STDEXEC::__completion_signatures_of_t<_Sender, _Env...>, // sigs + STDEXEC::__q // variant + >; + + template + using __with_eptr_completion = STDEXEC::__eptr_completion_unless_t, + __errors_nothrow_copyable<_Sender, _Env...>, + __mbool>>... + >>; + template using __delete_set_value_t = completion_signatures<>; @@ -204,8 +228,8 @@ namespace exec { using __completions_t = STDEXEC::transform_completion_signatures< __completion_signatures_of_t<__decay_t<_Sender> &, _Env...>, STDEXEC::transform_completion_signatures< - __completion_signatures_of_t, _Env...>, - __eptr_completion, + __completion_signatures_of_t, _Env...>, + __with_eptr_completion<_Sender, _Env...>, __delete_set_value_t >, __mbind_front_q<__values_t, _Sender>::template __f diff --git a/test/exec/test_repeat_n.cpp b/test/exec/test_repeat_n.cpp index ddb84fc8f..1a83fc893 100644 --- a/test/exec/test_repeat_n.cpp +++ b/test/exec/test_repeat_n.cpp @@ -141,4 +141,47 @@ namespace { REQUIRE(called); REQUIRE(!failed.load()); } + + TEST_CASE("repeat_n conditionally adds set_error_t(exception)", "[adaptors][repeat_n]") { + // 0. ensure exception isn't always added + { + ex::sender auto snd = ex::just() | exec::repeat_n(1); + static_assert( + std::same_as, ex::__detail::__not_a_variant>, + "Expected no errors "); + } + + // There are two main cases that will contribute set_error_t(std::exception_ptr) + // 1. error's copy constructor could throw + // 2. connect() could throw + { + // 1. + struct Error_with_throw_copy { + Error_with_throw_copy() noexcept = default; + Error_with_throw_copy(const Error_with_throw_copy&) noexcept(false) = default; + }; + ex::sender auto snd = ex::just_error(Error_with_throw_copy{}) | exec::repeat_n(1); + static_assert( + std::same_as< + ex::error_types_of_t, + std::variant + >, + "Missing added set_error_t(std::exception_ptr)"); + } + { + // 2. + using Sender_connect_throws = just_with_env>; + static_assert( + !ex::__error_types_t< + ex::completion_signatures_of_t, + ex::__mcontains + >::value, + "Sender can't already emit exception to test if repeat_until() adds it"); + ex::sender auto snd = Sender_connect_throws{} | exec::repeat_n(1); + static_assert( + std::same_as, std::variant>, + "Missing added set_error_t(std::exception_ptr)"); + } + } + } // namespace diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index dbf817783..f8fdb4838 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -27,11 +27,11 @@ #include +#include #include #include #include #include -#include namespace ex = STDEXEC; @@ -72,17 +72,13 @@ namespace { (void) snd; } - TEST_CASE( - "repeat_until with environment returns a sender", - "[adaptors][repeat_until]") { + TEST_CASE("repeat_until with environment returns a sender", "[adaptors][repeat_until]") { auto snd = exec::repeat_until(ex::just() | ex::then([] { return true; })); static_assert(ex::sender_in>); (void) snd; } - TEST_CASE( - "repeat_until produces void value to downstream receiver", - "[adaptors][repeat_until]") { + TEST_CASE("repeat_until produces void value to downstream receiver", "[adaptors][repeat_until]") { ex::sender auto source = ex::just(1) | ex::then([](int) { return true; }); ex::sender auto snd = exec::repeat_until(std::move(source)); // The receiver checks if we receive the void value @@ -134,9 +130,7 @@ namespace { ex::sync_wait(exec::repeat_until(std::move(input_snd))); } - TEST_CASE( - "repeat_until forwards set_error calls of other types", - "[adaptors][repeat_until]") { + TEST_CASE("repeat_until forwards set_error calls of other types", "[adaptors][repeat_until]") { auto snd = ex::just_error(std::string("error")) | exec::repeat_until(); auto op = ex::connect(std::move(snd), expect_error_receiver{std::string("error")}); ex::start(op); @@ -153,9 +147,9 @@ namespace { "[adaptors][repeat_until]") { int n = 1; ex::sender auto snd = exec::repeat_until(ex::just() | ex::then([&n] { - ++n; - return n == 1'000'000; - })); + ++n; + return n == 1'000'000; + })); ex::sync_wait(std::move(snd)); CHECK(n == 1'000'000); } @@ -172,9 +166,7 @@ namespace { REQUIRE(called); } - TEST_CASE( - "repeat_until works with bulk on a static_thread_pool", - "[adaptors][repeat_until]") { + TEST_CASE("repeat_until works with bulk on a static_thread_pool", "[adaptors][repeat_until]") { exec::static_thread_pool pool{2}; std::atomic failed{false}; const auto tid = std::this_thread::get_id(); @@ -262,8 +254,7 @@ namespace { do { // NOLINT(bugprone-infinite-loop) const auto tmp = throw_after; throw_after = std::numeric_limits::max(); - auto op = - ex::connect(exec::repeat(ex::just_error(error_type(throw_after))), receiver(done)); + auto op = ex::connect(exec::repeat(ex::just_error(error_type(throw_after))), receiver(done)); throw_after = tmp; ex::start(op); throw_after = tmp; @@ -271,15 +262,15 @@ namespace { } while (!done); } - TEST_CASE("repeat composes with completion signatures") { + TEST_CASE("repeat composes with completion signatures", "[adaptors][repeat]") { { ex::sender auto only_stopped = ex::just_stopped() | exec::repeat(); static_assert( std::same_as, ex::__detail::__not_a_variant>, "Expect no value completions"); static_assert( - std::same_as, std::variant>, - "Missing added set_error_t(std::exception_ptr)"); + std::same_as, ex::__detail::__not_a_variant>, + "Expect no value completions"); static_assert( ex::sender_of, "Missing set_stopped_t() from upstream"); @@ -294,11 +285,8 @@ namespace { std::same_as, ex::__detail::__not_a_variant>, "Expect no value completions"); static_assert( - std::same_as< - ex::error_types_of_t, - std::variant - >, - "Missing added set_error_t(std::exception_ptr)"); + std::same_as, std::variant>, + "Unexpected added set_error_t(std::exception_ptr)"); // set_stopped_t is always added as a consequence of the internal trampoline_scheduler using SC = ex::completion_signatures_of_t>; @@ -355,12 +343,70 @@ namespace { do { // NOLINT(bugprone-infinite-loop) const auto tmp = throw_after; throw_after = std::numeric_limits::max(); - auto op = - ex::connect(exec::repeat_until(ex::just(value_type(throw_after))), receiver(done)); + auto op = ex::connect(exec::repeat_until(ex::just(value_type(throw_after))), receiver(done)); throw_after = tmp; ex::start(op); throw_after = tmp; ++throw_after; } while (!done); } + + TEST_CASE("repeat_until conditionally adds set_error_t(exception)", "[adaptors][repeat_until]") { + // 0. ensure exception isn't always added + { + ex::sender auto snd = ex::just(false) | exec::repeat_until(); + static_assert( + std::same_as, ex::__detail::__not_a_variant>, + "Expected no errors "); + } + + // There are three main cases that will contribute set_error_t(std::exception_ptr) + // 1. value's conversion to bool could throw + // 2. error's copy constructor could throw + // 3. connect() could throw + { + // 1. + struct To_bool_can_throw { + [[nodiscard]] operator bool() const noexcept(false) { + return true; + } + }; + ex::sender auto snd = ex::just(To_bool_can_throw{}) | exec::repeat_until(); + static_assert( + std::same_as, std::variant>, + "Missing added set_error_t(std::exception_ptr)"); + } + { + // 2. + struct Error_with_throw_copy { + Error_with_throw_copy() noexcept = default; + Error_with_throw_copy(const Error_with_throw_copy&) noexcept(false) = default; + }; + ex::sender auto snd = ex::just_error(Error_with_throw_copy{}) | exec::repeat_until(); + static_assert( + std::same_as< + ex::error_types_of_t, + std::variant + >, + "Missing added set_error_t(std::exception_ptr)"); + } + { + // 3. + using Sender_connect_throws = just_with_env, bool>; + static_assert( + !ex::__error_types_t< + ex::completion_signatures_of_t, + ex::__mcontains + >::value, + "Sender can't already emit exception to test if repeat_until() adds it"); + + ex::sender auto snd = Sender_connect_throws{{},true} | exec::repeat_until(); + static_assert( + std::same_as< + ex::error_types_of_t, + std::variant + >, + "Missing added set_error_t(std::exception_ptr)"); + } + } } // namespace