diff --git a/include/exec/repeat_n.hpp b/include/exec/repeat_n.hpp index 5741cf08c..c444a7e0a 100644 --- a/include/exec/repeat_n.hpp +++ b/include/exec/repeat_n.hpp @@ -39,6 +39,9 @@ namespace exec { constexpr explicit __opstate_base(_Receiver &&__rcvr, std::size_t __count) noexcept : __rcvr_{static_cast<_Receiver &&>(__rcvr)} , __count_{__count} { + static_assert( + std::is_nothrow_default_constructible_v, + "trampoline_scheduler c'tor is always expected to be noexcept"); } virtual constexpr void __cleanup() noexcept = 0; @@ -46,7 +49,10 @@ namespace exec { _Receiver __rcvr_; std::size_t __count_; - trampoline_scheduler __sched_; + trampoline_scheduler __sched_{}; + + protected: + ~__opstate_base() noexcept = default; }; template @@ -90,16 +96,14 @@ namespace exec { }; template - struct __opstate : __opstate_base<_Receiver> { + struct __opstate final : __opstate_base<_Receiver> { using __receiver_t = STDEXEC::__t<__receiver<__id<_Receiver>>>; using __bouncy_sndr_t = __result_of, _Child &>; using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; - static constexpr bool __nothrow_connect = - STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>; constexpr explicit __opstate(std::size_t __count, _Child __child, _Receiver __rcvr) - noexcept(__nothrow_connect) + noexcept(std::is_nothrow_move_constructible_v<_Child> && noexcept(__connect())) : __opstate_base<_Receiver>{static_cast<_Receiver &&>(__rcvr), __count} , __child_(std::move(__child)) { if (this->__count_ != 0) { @@ -115,18 +119,19 @@ namespace exec { } } - constexpr auto __connect() noexcept(__nothrow_connect) -> __child_op_t & { + constexpr __child_op_t & + __connect() noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), __receiver_t{this}); } - 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) { @@ -137,7 +142,9 @@ namespace exec { } } STDEXEC_CATCH_ALL { - STDEXEC::set_error(std::move(this->__rcvr_), std::current_exception()); + if constexpr (!noexcept(__connect())) { + STDEXEC::set_error(std::move(this->__rcvr_), std::current_exception()); + } } } @@ -164,12 +171,24 @@ namespace exec { template 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 + >; + + 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...>, STDEXEC::transform_completion_signatures< - __completion_signatures_of_t, _Env...>, - __eptr_completion, + __completion_signatures_of_t, _Env...>, + __with_eptr_completion<_Child, _Env...>, __cmplsigs::__default_set_value, __error_t >, diff --git a/include/exec/repeat_until.hpp b/include/exec/repeat_until.hpp index 108b07b2f..77728ed47 100644 --- a/include/exec/repeat_until.hpp +++ b/include/exec/repeat_until.hpp @@ -36,15 +36,21 @@ namespace exec { template struct __opstate_base { - constexpr explicit __opstate_base(_Receiver &&__rcvr) - : __rcvr_{static_cast<_Receiver &&>(__rcvr)} { + constexpr explicit __opstate_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: + ~__opstate_base() noexcept = default; }; template @@ -123,15 +129,13 @@ namespace exec { STDEXEC_PRAGMA_IGNORE_GNU("-Wtsan") template - struct __opstate : __opstate_base<_Receiver> { + struct __opstate final : __opstate_base<_Receiver> { using __receiver_t = STDEXEC::__t<__receiver<__id<_Receiver>>>; using __bouncy_sndr_t = __result_of, _Child &>; using __child_op_t = STDEXEC::connect_result_t<__bouncy_sndr_t, __receiver_t>; - static constexpr bool __nothrow_connect = - STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>; - constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept(__nothrow_connect) + constexpr explicit __opstate(_Child __child, _Receiver __rcvr) noexcept(noexcept(__connect())) : __opstate_base<_Receiver>(static_cast<_Receiver &&>(__rcvr)) , __child_(static_cast<_Child &&>(__child)) { __connect(); @@ -141,23 +145,25 @@ namespace exec { STDEXEC::start(*__child_op_); } - constexpr auto __connect() noexcept(__nothrow_connect) -> __child_op_t & { + constexpr __child_op_t& __connect() noexcept(STDEXEC::__nothrow_connectable<__bouncy_sndr_t, __receiver_t>) { return __child_op_.__emplace_from( STDEXEC::connect, exec::sequence(STDEXEC::schedule(this->__sched_), __child_), __receiver_t{this}); } - 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 (!noexcept(__connect())) { + STDEXEC::set_error(static_cast<_Receiver &&>(this->__rcvr_), std::current_exception()); + } } } @@ -189,6 +195,30 @@ namespace exec { > >; + 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<>; @@ -196,13 +226,15 @@ namespace exec { using __completions_t = STDEXEC::transform_completion_signatures< __completion_signatures_of_t<__decay_t<_Child> &, _Env...>, STDEXEC::transform_completion_signatures< - __completion_signatures_of_t, _Env...>, - __eptr_completion, + __completion_signatures_of_t, _Env...>, + __with_eptr_completion<_Child, _Env...>, __delete_set_value_t >, __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() { 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 a79d79879..a3f5a390f 100644 --- a/test/exec/test_repeat_until.cpp +++ b/test/exec/test_repeat_until.cpp @@ -17,7 +17,7 @@ #include "exec/repeat_until.hpp" #include "exec/static_thread_pool.hpp" -#include "stdexec/__detail/__sync_wait.hpp" +#include "exec/trampoline_scheduler.hpp" #include "stdexec/execution.hpp" #include @@ -27,6 +27,7 @@ #include +#include #include #include #include @@ -262,45 +263,42 @@ 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_error_t(std::exception_ptr)"); - - // operator| and sync_wait require valid completion signatures - ex::sync_wait( - only_error // - | ex::upon_stopped([]() { return -1; }) - | ex::upon_error([](const auto) { return -1; })); + 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, ex::__detail::__not_a_variant>, + "Expect no value completions"); + 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, 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>; + 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( @@ -353,4 +351,63 @@ namespace { ++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