From 88883afe8cc805eac3222de53c2268a946fbaacf Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Sun, 25 Jan 2026 21:00:44 -0400 Subject: [PATCH 01/11] 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 02/11] 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 From 843d5099978c5d3adc5495e1798828cb6fdc0987 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Tue, 27 Jan 2026 22:50:02 -0400 Subject: [PATCH 03/11] improve noexcept c'tor check --- include/exec/repeat_n.hpp | 6 +++--- include/exec/repeat_until.hpp | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index c444a7e0a..7158ec945 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -37,7 +37,7 @@ namespace exec { template struct __opstate_base : __immovable { constexpr explicit __opstate_base(_Receiver &&__rcvr, std::size_t __count) noexcept - : __rcvr_{static_cast<_Receiver &&>(__rcvr)} + : __rcvr_{std::move(__rcvr)} , __count_{__count} { static_assert( std::is_nothrow_default_constructible_v, @@ -103,8 +103,8 @@ namespace exec { using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; constexpr explicit __opstate(std::size_t __count, _Child __child, _Receiver __rcvr) - noexcept(std::is_nothrow_move_constructible_v<_Child> && noexcept(__connect())) - : __opstate_base<_Receiver>{static_cast<_Receiver &&>(__rcvr), __count} + noexcept(__nothrow_move_constructible<_Child> && noexcept(__connect())) + : __opstate_base<_Receiver>{std::move(__rcvr), __count} , __child_(std::move(__child)) { if (this->__count_ != 0) { __connect(); diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 77728ed47..e62f31aa3 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -135,9 +135,11 @@ namespace exec { __result_of, _Child &>; using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; - constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept(noexcept(__connect())) - : __opstate_base<_Receiver>(static_cast<_Receiver &&>(__rcvr)) - , __child_(static_cast<_Child &&>(__child)) { + constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept( + __nothrow_move_constructible<_Child> && __nothrow_move_constructible<_Receiver> + && noexcept(__connect())) + : __opstate_base<_Receiver>(std::move(__rcvr)) + , __child_(std::move(__child)) { __connect(); } From bb09349fb79e43be116588d0b066f761ae1d361c Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Tue, 27 Jan 2026 22:59:54 -0400 Subject: [PATCH 04/11] rm unused repeat_until_tag from merge --- include/exec/repeat_until.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index e62f31aa3..2743460ae 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -235,8 +235,6 @@ namespace exec { __mbind_front_q<__values_t, _Child>::template __f >; - struct __repeat_until_tag { }; - struct __repeat_until_impl : __sexpr_defaults { template static consteval auto get_completion_signatures() { From 2d9d09a4b5b62dfea1a228f90f33ad79a509f384 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio <10579359+justend29@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:45:37 -0400 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Eric Niebler --- include/exec/repeat_n.hpp | 12 +++++----- include/exec/repeat_until.hpp | 43 ++++++++++++++++------------------- test/exec/test_repeat_n.cpp | 5 +++- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 7158ec945..802f3746e 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -37,10 +37,10 @@ namespace exec { template struct __opstate_base : __immovable { constexpr explicit __opstate_base(_Receiver &&__rcvr, std::size_t __count) noexcept - : __rcvr_{std::move(__rcvr)} + : __rcvr_{static_cast<_Receiver &&>(__rcvr)} , __count_{__count} { static_assert( - std::is_nothrow_default_constructible_v, + __nothrow_constructible_from, "trampoline_scheduler c'tor is always expected to be noexcept"); } @@ -104,7 +104,7 @@ namespace exec { constexpr explicit __opstate(std::size_t __count, _Child __child, _Receiver __rcvr) noexcept(__nothrow_move_constructible<_Child> && noexcept(__connect())) - : __opstate_base<_Receiver>{std::move(__rcvr), __count} + : __opstate_base<_Receiver>{static_cast<_Receiver &&>(__rcvr), __count} , __child_(std::move(__child)) { if (this->__count_ != 0) { __connect(); @@ -127,11 +127,11 @@ namespace exec { __receiver_t{this}); } - constexpr void __cleanup() noexcept override { + constexpr void __cleanup() noexcept final { __child_op_.reset(); } - constexpr void __repeat() noexcept override { + constexpr void __repeat() noexcept final { STDEXEC_ASSERT(this->__count_ > 0); STDEXEC_TRY { if (--this->__count_ == 0) { @@ -188,7 +188,7 @@ namespace exec { __completion_signatures_of_t<_Child &, _Env...>, STDEXEC::transform_completion_signatures< __completion_signatures_of_t, _Env...>, - __with_eptr_completion<_Child, _Env...>, + __with_eptr_completion_t<_Child, _Env...>, __cmplsigs::__default_set_value, __error_t >, diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 2743460ae..704eb1da3 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -37,9 +37,9 @@ namespace exec { template struct __opstate_base { constexpr explicit __opstate_base(_Receiver &&__rcvr) noexcept - : __rcvr_{std::move(__rcvr)} { + : __rcvr_{static_cast<_Receiver &&>(__rcvr)} { static_assert( - std::is_nothrow_default_constructible_v, + __nothrow_constructible_from, "trampoline_scheduler c'tor is always expected to be noexcept"); } @@ -136,8 +136,7 @@ namespace exec { using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept( - __nothrow_move_constructible<_Child> && __nothrow_move_constructible<_Receiver> - && noexcept(__connect())) + __nothrow_move_constructible<_Child> && noexcept(__connect())) : __opstate_base<_Receiver>(std::move(__rcvr)) , __child_(std::move(__child)) { __connect(); @@ -154,11 +153,11 @@ namespace exec { __receiver_t{this}); } - constexpr void __cleanup() noexcept override { + constexpr void __cleanup() noexcept final { __child_op_.reset(); } - constexpr void __repeat() noexcept override { + constexpr void __repeat() noexcept final { STDEXEC_TRY { STDEXEC::start(__connect()); } @@ -198,28 +197,24 @@ namespace exec { >; template - using __values_overload_nothrow_bool_convertible = - STDEXEC::__mand...>; + using __values_overload_nothrow_bool_convertible_t = + __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 + using __values_nothrow_bool_convertible_t = __value_types_t< + __completion_signatures_of_t<_Sender, _Env...>, // sigs + __qq<__values_overload_nothrow_bool_convertible_t>, // tuple + __qq<__mand> // variant >; template - using __with_eptr_completion = STDEXEC::__eptr_completion_unless_t, - __errors_nothrow_copyable<_Sender, _Env...>, - __mbool>>... - >>; + using __with_eptr_completion_t = __eptr_completion_unless< + __values_nothrow_bool_convertible_t<_Sender, _Env...>::value + && __cmplsigs::__partition_completion_signatures_t< + __completion_signatures_of_t<_Sender, _Env...> + >::__nothrow_decay_copyable::__errors::value + && (__nothrow_connectable<_Sender, __receiver_archetype<_Env>> && ...) + >; template using __delete_set_value_t = completion_signatures<>; @@ -229,7 +224,7 @@ namespace exec { __completion_signatures_of_t<__decay_t<_Child> &, _Env...>, STDEXEC::transform_completion_signatures< __completion_signatures_of_t, _Env...>, - __with_eptr_completion<_Child, _Env...>, + __with_eptr_completion_t<_Child, _Env...>, __delete_set_value_t >, __mbind_front_q<__values_t, _Child>::template __f diff --git a/test/exec/test_repeat_n.cpp b/test/exec/test_repeat_n.cpp index 1a83fc893..06e7706e8 100644 --- a/test/exec/test_repeat_n.cpp +++ b/test/exec/test_repeat_n.cpp @@ -143,7 +143,7 @@ namespace { } TEST_CASE("repeat_n conditionally adds set_error_t(exception)", "[adaptors][repeat_n]") { - // 0. ensure exception isn't always added + SECTION("ensure exception isn't always added") { ex::sender auto snd = ex::just() | exec::repeat_n(1); static_assert( @@ -154,6 +154,7 @@ namespace { // 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 + SECTION("error completion is added when an error's copy ctor can throw") { // 1. struct Error_with_throw_copy { @@ -168,6 +169,8 @@ namespace { >, "Missing added set_error_t(std::exception_ptr)"); } + + SECTION("error completion is added when connect can throw") { // 2. using Sender_connect_throws = just_with_env>; From bdd0f3ec084bdd9ba75e2dab0af70431ac429425 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 08:49:32 -0400 Subject: [PATCH 06/11] use Catch2 sections for test blocks --- test/exec/test_repeat_until.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index a3f5a390f..2a5268d49 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -353,8 +353,7 @@ namespace { } TEST_CASE("repeat_until conditionally adds set_error_t(exception)", "[adaptors][repeat_until]") { - // 0. ensure exception isn't always added - { + SECTION("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>, @@ -365,7 +364,7 @@ namespace { // 1. value's conversion to bool could throw // 2. error's copy constructor could throw // 3. connect() could throw - { + SECTION("error completion is added when an error's copy ctor can throw"){ // 1. struct To_bool_can_throw { [[nodiscard]] operator bool() const noexcept(false) { @@ -377,6 +376,8 @@ namespace { std::same_as, std::variant>, "Missing added set_error_t(std::exception_ptr)"); } + + SECTION("error completion is added when error->bool can throw") { // 2. struct Error_with_throw_copy { @@ -391,6 +392,8 @@ namespace { >, "Missing added set_error_t(std::exception_ptr)"); } + + SECTION("error completion is added when connect can throw") { // 3. using Sender_connect_throws = just_with_env, bool>; From 371940b14e9812a7606eb3d8502fb4dc43f88732 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 08:50:15 -0400 Subject: [PATCH 07/11] autoformat test changes --- test/exec/test_repeat_n.cpp | 9 +++------ test/exec/test_repeat_until.cpp | 20 ++++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/test/exec/test_repeat_n.cpp b/test/exec/test_repeat_n.cpp index 06e7706e8..6f9bc60d8 100644 --- a/test/exec/test_repeat_n.cpp +++ b/test/exec/test_repeat_n.cpp @@ -143,8 +143,7 @@ namespace { } TEST_CASE("repeat_n conditionally adds set_error_t(exception)", "[adaptors][repeat_n]") { - SECTION("ensure exception isn't always added") - { + SECTION("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>, @@ -154,8 +153,7 @@ namespace { // 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 - SECTION("error completion is added when an error's copy ctor can throw") - { + SECTION("error completion is added when an error's copy ctor can throw") { // 1. struct Error_with_throw_copy { Error_with_throw_copy() noexcept = default; @@ -170,8 +168,7 @@ namespace { "Missing added set_error_t(std::exception_ptr)"); } - SECTION("error completion is added when connect can throw") - { + SECTION("error completion is added when connect can throw") { // 2. using Sender_connect_throws = just_with_env>; static_assert( diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index 2a5268d49..f6c58ad31 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -353,7 +353,7 @@ namespace { } TEST_CASE("repeat_until conditionally adds set_error_t(exception)", "[adaptors][repeat_until]") { - SECTION("ensure exception isn't always added"){ + SECTION("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>, @@ -364,10 +364,11 @@ namespace { // 1. value's conversion to bool could throw // 2. error's copy constructor could throw // 3. connect() could throw - SECTION("error completion is added when an error's copy ctor can throw"){ + SECTION("error completion is added when an error's copy ctor can throw") { // 1. struct To_bool_can_throw { - [[nodiscard]] operator bool() const noexcept(false) { + [[nodiscard]] + operator bool() const noexcept(false) { return true; } }; @@ -377,8 +378,7 @@ namespace { "Missing added set_error_t(std::exception_ptr)"); } - SECTION("error completion is added when error->bool can throw") - { + SECTION("error completion is added when error->bool can throw") { // 2. struct Error_with_throw_copy { Error_with_throw_copy() noexcept = default; @@ -393,8 +393,7 @@ namespace { "Missing added set_error_t(std::exception_ptr)"); } - SECTION("error completion is added when connect can throw") - { + SECTION("error completion is added when connect can throw") { // 3. using Sender_connect_throws = just_with_env, bool>; static_assert( @@ -404,12 +403,9 @@ namespace { >::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(); + ex::sender auto snd = Sender_connect_throws{{}, true} | exec::repeat_until(); static_assert( - std::same_as< - ex::error_types_of_t, - std::variant - >, + std::same_as, std::variant>, "Missing added set_error_t(std::exception_ptr)"); } } From 78e05448c13e2e046820bb3bda2c253ff4373294 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 08:56:28 -0400 Subject: [PATCH 08/11] employ trailing return types on __connect() --- include/exec/repeat_n.hpp | 21 ++++++++++++--------- include/exec/repeat_until.hpp | 9 +++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 802f3746e..363af44df 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -51,7 +51,7 @@ namespace exec { std::size_t __count_; trampoline_scheduler __sched_{}; - protected: + protected: ~__opstate_base() noexcept = default; }; @@ -119,8 +119,11 @@ namespace exec { } } - constexpr __child_op_t & - __connect() noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) { + constexpr auto __connect() + noexcept( + STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t> && + STDEXEC::__nothrow_ + ) -> __child_op_t & { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), @@ -206,12 +209,12 @@ namespace exec { static constexpr auto connect = // [](_Sender &&__sndr, _Receiver &&__rcvr) noexcept( noexcept(__opstate(0, STDEXEC::__get<2>(__declval<_Sender>()), __declval<_Receiver>()))) { - const std::size_t __count = STDEXEC::__get<1>(__sndr); - return __opstate( - __count, - STDEXEC::__get<2>(static_cast<_Sender &&>(__sndr)), - static_cast<_Receiver &&>(__rcvr)); - }; + const std::size_t __count = STDEXEC::__get<1>(__sndr); + return __opstate( + __count, + STDEXEC::__get<2>(static_cast<_Sender &&>(__sndr)), + static_cast<_Receiver &&>(__rcvr)); + }; }; struct repeat_n_t { diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 704eb1da3..2362d98dd 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -49,7 +49,7 @@ namespace exec { _Receiver __rcvr_; trampoline_scheduler __sched_{}; - protected: + protected: ~__opstate_base() noexcept = default; }; @@ -135,8 +135,8 @@ namespace exec { __result_of, _Child &>; using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; - constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept( - __nothrow_move_constructible<_Child> && noexcept(__connect())) + constexpr explicit __opstate(_Child __child, _Receiver __rcvr) + noexcept(__nothrow_move_constructible<_Child> && noexcept(__connect())) : __opstate_base<_Receiver>(std::move(__rcvr)) , __child_(std::move(__child)) { __connect(); @@ -146,7 +146,8 @@ namespace exec { STDEXEC::start(*__child_op_); } - constexpr __child_op_t& __connect() noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) { + constexpr auto __connect() + noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) -> __child_op_t & { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), From b7a2d9d09b247a414c94a96bdec9e3229ba596f0 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 09:51:37 -0400 Subject: [PATCH 09/11] correct noexcept() on __connect(); rm partition suggestion to compile --- include/exec/repeat_n.hpp | 19 +++++++++---------- include/exec/repeat_until.hpp | 24 +++++++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 363af44df..290b0e0df 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -119,11 +119,10 @@ namespace exec { } } - constexpr auto __connect() - noexcept( - STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t> && - STDEXEC::__nothrow_ - ) -> __child_op_t & { + constexpr auto __connect() noexcept( + __nothrow_invocable + && __nothrow_invocable, _Child &> + && __nothrow_connectable<__bouncy_sndr_t, __receiver_t>) -> __child_op_t & { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), @@ -175,15 +174,15 @@ namespace exec { using __error_t = completion_signatures)>; template - using __errors_nothrow_copyable = STDEXEC::__error_types_t< - STDEXEC::__completion_signatures_of_t<_Sender, _Env...>, // sigs - STDEXEC::__q // variant + using __errors_nothrow_copyable = __error_types_t< + __completion_signatures_of_t<_Sender, _Env...>, // sigs + __q<__nothrow_decay_copyable_t> // variant >; template - using __with_eptr_completion = STDEXEC::__eptr_completion_unless_t, - __mbool>>... + __mbool<__nothrow_connectable<_Sender, __receiver_archetype<_Env>>>... >>; template diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 2362d98dd..fd0adb1c3 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -146,8 +146,10 @@ namespace exec { STDEXEC::start(*__child_op_); } - constexpr auto __connect() - noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) -> __child_op_t & { + constexpr auto __connect() noexcept( + __nothrow_invocable + && __nothrow_invocable, _Child &> + && __nothrow_connectable<__bouncy_sndr_t, __receiver_t>) -> __child_op_t & { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), @@ -208,15 +210,19 @@ namespace exec { __qq<__mand> // variant >; - template - using __with_eptr_completion_t = __eptr_completion_unless< - __values_nothrow_bool_convertible_t<_Sender, _Env...>::value - && __cmplsigs::__partition_completion_signatures_t< - __completion_signatures_of_t<_Sender, _Env...> - >::__nothrow_decay_copyable::__errors::value - && (__nothrow_connectable<_Sender, __receiver_archetype<_Env>> && ...) + template + using __errors_nothrow_copyable = __error_types_t< + __completion_signatures_of_t<_Sender, _Env...>, // sigs + __q<__nothrow_decay_copyable_t> // variant >; + template + using __with_eptr_completion_t = __eptr_completion_unless_t<__mand< + __values_nothrow_bool_convertible_t<_Sender, _Env...>, + __errors_nothrow_copyable<_Sender, _Env...>, + __mbool<__nothrow_connectable<_Sender, __receiver_archetype<_Env>>>... + >>; + template using __delete_set_value_t = completion_signatures<>; From 56bb23b1e42362cd9de1a857d56da6d0e8952cad Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 10:28:48 -0400 Subject: [PATCH 10/11] correct partition completions suggestion --- include/exec/repeat_n.hpp | 16 ++++++---------- include/exec/repeat_until.hpp | 18 +++++++----------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 6df4e5d56..644693aeb 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -174,18 +174,14 @@ namespace exec { template using __error_t = completion_signatures)>; - template - using __errors_nothrow_copyable = __error_types_t< - __completion_signatures_of_t<_Sender, _Env...>, // sigs - __q<__nothrow_decay_copyable_t> // variant + template + using __with_eptr_completion_t = __eptr_completion_unless< + __cmplsigs::__partitions_of_t< + __completion_signatures_of_t<_Sender, _Env...> + >::__nothrow_decay_copyable::__errors::value + && (__nothrow_connectable<_Sender, __receiver_archetype<_Env>> && ...) >; - template - using __with_eptr_completion_t = __eptr_completion_unless_t<__mand< - __errors_nothrow_copyable<_Sender, _Env...>, - __mbool<__nothrow_connectable<_Sender, __receiver_archetype<_Env>>>... - >>; - template using __completions_t = STDEXEC::transform_completion_signatures< __completion_signatures_of_t<_Child &, _Env...>, diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 26ef9bda7..c8f5b0624 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -209,19 +209,15 @@ namespace exec { __qq<__mand> // variant >; - template - using __errors_nothrow_copyable = __error_types_t< - __completion_signatures_of_t<_Sender, _Env...>, // sigs - __q<__nothrow_decay_copyable_t> // variant + template + using __with_eptr_completion_t = __eptr_completion_unless< + __values_nothrow_bool_convertible_t<_Sender, _Env...>::value + && __cmplsigs::__partitions_of_t< + __completion_signatures_of_t<_Sender, _Env...> + >::__nothrow_decay_copyable::__errors::value + && (__nothrow_connectable<_Sender, __receiver_archetype<_Env>> && ...) >; - template - using __with_eptr_completion_t = __eptr_completion_unless_t<__mand< - __values_nothrow_bool_convertible_t<_Sender, _Env...>, - __errors_nothrow_copyable<_Sender, _Env...>, - __mbool<__nothrow_connectable<_Sender, __receiver_archetype<_Env>>>... - >>; - template using __delete_set_value_t = completion_signatures<>; From ac8352ca55e44d2d989639955e843856b067d376 Mon Sep 17 00:00:00 2001 From: Justen Di Ruscio Date: Thu, 29 Jan 2026 10:39:46 -0400 Subject: [PATCH 11/11] add Catch2 sections to existing composition test --- test/exec/test_repeat_until.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/exec/test_repeat_until.cpp b/test/exec/test_repeat_until.cpp index f6c58ad31..ce8851d97 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -264,7 +264,7 @@ namespace { } TEST_CASE("repeat composes with completion signatures", "[adaptors][repeat]") { - { + SECTION("repeat composes with stopped upstream") { ex::sender auto only_stopped = ex::just_stopped() | exec::repeat(); static_assert( std::same_as, ex::__detail::__not_a_variant>, @@ -280,7 +280,7 @@ namespace { ex::sync_wait(only_stopped | ex::upon_stopped([]() noexcept { return -1; })); } - { + SECTION("repeat composes with errors upstream") { ex::sender auto only_error = ex::just_error(-1) | exec::repeat(); static_assert( std::same_as, ex::__detail::__not_a_variant>,