From 29e3b2cddedc7822bc0f40060a08bab42a129bda Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 16 Aug 2026 02:38:22 +0200 Subject: [PATCH 1/3] Fix when_any error completion decay --- include/exec/when_any.hpp | 22 ++--- test/exec/test_when_any.cpp | 167 ++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/include/exec/when_any.hpp b/include/exec/when_any.hpp index 94d8e6918..8766383d0 100644 --- a/include/exec/when_any.hpp +++ b/include/exec/when_any.hpp @@ -34,32 +34,26 @@ namespace experimental::execution template using __env_t = __join_env_t, _Env>; - template - using __nothrow_decay_copyable_and_move_constructible_t = __mbool<( - (__nothrow_decay_copyable<_Ts> && __nothrow_move_constructible<__decay_t<_Ts>>) && ...)>; - template using __as_rvalues = set_value_t (*)(__decay_t...); template - using __as_error = set_error_t (*)(E...); + using __as_error = set_error_t (*)(__decay_t...); - // Here we convert all set_value(Args...) to set_value(__decay_t...). Note, we keep all - // error types as they are and unconditionally add set_stopped(). The indirection through the - // __completions_fn is to avoid a pack expansion bug in nvc++. + // Here we convert all set_value(Args...) and set_error(Args...) to use decayed arguments and + // unconditionally add set_stopped(). The indirection through the __completions_fn is to avoid + // a pack expansion bug in nvc++. template struct __completions_fn { template - using __all_value_args_nothrow_decay_copyable = - __minvoke_q<__mand_t, - __value_types_t<__completion_signatures_of_t<_CvSenders, __env_t<_Env>...>, - __qq<__nothrow_decay_copyable_and_move_constructible_t>, - __qq<__mand_t>>...>; + using __all_nothrow_decay_copyable_results = + __mand<__nothrow_decay_copyable_results_t< + __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>>...>; template using __f = __mtry_q<__concat_completion_signatures_t>::__f< - __eptr_completion_unless_t<__all_value_args_nothrow_decay_copyable<_CvSenders...>>, + __eptr_completion_unless_t<__all_nothrow_decay_copyable_results<_CvSenders...>>, completion_signatures, __transform_reduce_completion_signatures_t< __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>, diff --git a/test/exec/test_when_any.cpp b/test/exec/test_when_any.cpp index 58df5951c..697d21b8b 100644 --- a/test/exec/test_when_any.cpp +++ b/test/exec/test_when_any.cpp @@ -31,6 +31,142 @@ using namespace STDEXEC; namespace { + struct error_ref_sender + { + struct error + {}; + + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() noexcept + -> ex::completion_signatures + { + return {}; + } + + template + struct operation + { + Receiver rcvr_; + error error_; + + void start() & noexcept + { + ex::set_error(static_cast(rcvr_), error_); + } + }; + + template + auto connect(Receiver rcvr) && noexcept -> operation + { + return {static_cast(rcvr), {}}; + } + }; + + struct error_ref_receiver + { + using receiver_concept = ex::receiver_tag; + + bool* lvalue_error_; + + void set_value() noexcept {} + void set_stopped() noexcept {} + + void set_error(error_ref_sender::error&) noexcept + { + *lvalue_error_ = true; + } + + void set_error(error_ref_sender::error&&) noexcept + { + *lvalue_error_ = false; + } + + auto get_env() const noexcept -> ex::env<> + { + return {}; + } + }; + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct error_copy_error + {}; + + struct throwing_error + { + explicit throwing_error(bool* throw_on_copy) noexcept + : throw_on_copy_{throw_on_copy} + {} + + throwing_error(throwing_error const& other) + : throw_on_copy_{other.throw_on_copy_} + { + if (*throw_on_copy_) + { + throw error_copy_error{}; + } + } + + throwing_error(throwing_error&&) noexcept = default; + + bool* throw_on_copy_; + }; + + struct throwing_error_sender + { + using sender_concept = ex::sender_tag; + + bool* throw_on_copy_; + + template + static consteval auto get_completion_signatures() noexcept + -> ex::completion_signatures + { + return {}; + } + + template + struct operation + { + Receiver rcvr_; + throwing_error error_; + + void start() & noexcept + { + ex::set_error(static_cast(rcvr_), error_); + } + }; + + template + auto connect(Receiver rcvr) && noexcept -> operation + { + return {static_cast(rcvr), throwing_error{throw_on_copy_}}; + } + }; + + struct throwing_error_receiver + { + using receiver_concept = ex::receiver_tag; + + bool* got_exception_; + + void set_error(throwing_error) noexcept {} + + void set_error(std::exception_ptr) noexcept + { + *got_exception_ = true; + } + + void set_stopped() noexcept {} + + auto get_env() const noexcept -> ex::env<> + { + return {}; + } + }; +#endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("when_ny returns a sender", "[adaptors][when_any]") { auto snd = exec::when_any(ex::just(3), ex::just(0.1415)); @@ -188,6 +324,37 @@ namespace // wait_for_value(std::move(snd), movable(42)); } + TEST_CASE("when_any decays error completion arguments", "[adaptors][when_any]") + { + auto snd = exec::when_any(error_ref_sender{}); + static_assert(set_equivalent, + completion_signatures>); + + bool lvalue_error = false; + auto op = ex::connect(std::move(snd), error_ref_receiver{&lvalue_error}); + ex::start(op); + CHECK_FALSE(lvalue_error); + } + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("when_any reports errors from throwing error decay", "[adaptors][when_any]") + { + bool throw_on_copy = false; + auto snd = exec::when_any(throwing_error_sender{&throw_on_copy}); + static_assert(set_equivalent, + completion_signatures>); + + bool got_exception = false; + auto op = ex::connect(std::move(snd), throwing_error_receiver{&got_exception}); + throw_on_copy = true; + ex::start(op); + CHECK(got_exception); + } +#endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() + #if !STDEXEC_NO_STDCPP_EXCEPTIONS() template struct dup_op From 2f3839222c231c24c082c80c6c40b43b09e1a59c Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 16 Aug 2026 12:35:36 +0200 Subject: [PATCH 2/3] Fix when_any completion move safety --- include/exec/when_any.hpp | 22 ++++--- test/exec/test_when_any.cpp | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/include/exec/when_any.hpp b/include/exec/when_any.hpp index 8766383d0..4f2b936ab 100644 --- a/include/exec/when_any.hpp +++ b/include/exec/when_any.hpp @@ -46,21 +46,27 @@ namespace experimental::execution template struct __completions_fn { + template + using __raw_completions_t = __completion_signatures_of_t<_CvSender, __env_t<_Env>...>; + + template + using __decayed_completions_t = __transform_reduce_completion_signatures_t< + __raw_completions_t<_CvSender>, + __as_rvalues, + __as_error, + set_stopped_t (*)(), + __completion_signature_ptrs_t>; + template using __all_nothrow_decay_copyable_results = - __mand<__nothrow_decay_copyable_results_t< - __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>>...>; + __mand<__nothrow_decay_copyable_results_t<__raw_completions_t<_CvSenders>>..., + __nothrow_decay_copyable_results_t<__decayed_completions_t<_CvSenders>>...>; template using __f = __mtry_q<__concat_completion_signatures_t>::__f< __eptr_completion_unless_t<__all_nothrow_decay_copyable_results<_CvSenders...>>, completion_signatures, - __transform_reduce_completion_signatures_t< - __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>, - __as_rvalues, - __as_error, - set_stopped_t (*)(), - __completion_signature_ptrs_t>...>; + __decayed_completions_t<_CvSenders>...>; }; template diff --git a/test/exec/test_when_any.cpp b/test/exec/test_when_any.cpp index 697d21b8b..d5cb280bc 100644 --- a/test/exec/test_when_any.cpp +++ b/test/exec/test_when_any.cpp @@ -90,6 +90,77 @@ namespace }; #if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct copy_noexcept_move_throws + { + copy_noexcept_move_throws() = default; + + copy_noexcept_move_throws(copy_noexcept_move_throws const&) noexcept = default; + + copy_noexcept_move_throws(copy_noexcept_move_throws&&) noexcept(false) {} + }; + + struct throwing_move_error_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() noexcept + -> ex::completion_signatures + { + return {}; + } + + template + struct operation + { + Receiver rcvr_; + copy_noexcept_move_throws error_; + + void start() & noexcept + { + ex::set_error(static_cast(rcvr_), error_); + } + }; + + template + auto connect(Receiver rcvr) && noexcept -> operation + { + return {static_cast(rcvr), {}}; + } + }; + + template + struct throwing_move_receiver + { + using receiver_concept = ex::receiver_tag; + + bool* got_value_; + bool* got_error_; + bool* got_exception_; + + void set_value(Type&&) noexcept + { + *got_value_ = true; + } + + void set_error(Type&&) noexcept + { + *got_error_ = true; + } + + void set_error(std::exception_ptr) noexcept + { + *got_exception_ = true; + } + + void set_stopped() noexcept {} + + auto get_env() const noexcept -> ex::env<> + { + return {}; + } + }; + struct error_copy_error {}; @@ -353,6 +424,47 @@ namespace ex::start(op); CHECK(got_exception); } + + TEST_CASE("when_any reports errors for potentially throwing value moves", "[adaptors][when_any]") + { + copy_noexcept_move_throws value; + auto snd = exec::when_any(just_ref{value}); + static_assert(set_equivalent, + completion_signatures>); + + bool got_value = false; + bool got_error = false; + bool got_exception = false; + auto op = ex::connect(std::move(snd), + throwing_move_receiver{ + &got_value, &got_error, &got_exception}); + ex::start(op); + CHECK(got_value); + CHECK_FALSE(got_error); + CHECK_FALSE(got_exception); + } + + TEST_CASE("when_any reports errors for potentially throwing error moves", "[adaptors][when_any]") + { + auto snd = exec::when_any(throwing_move_error_sender{}); + static_assert(set_equivalent, + completion_signatures>); + + bool got_value = false; + bool got_error = false; + bool got_exception = false; + auto op = ex::connect(std::move(snd), + throwing_move_receiver{ + &got_value, &got_error, &got_exception}); + ex::start(op); + CHECK_FALSE(got_value); + CHECK(got_error); + CHECK_FALSE(got_exception); + } #endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() #if !STDEXEC_NO_STDCPP_EXCEPTIONS() From 64e65cb889158a48f699cd4f236b68148a77c9f7 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 16 Aug 2026 20:55:26 +0200 Subject: [PATCH 3/3] Format when_any error decay changes --- include/exec/when_any.hpp | 12 +++++------ test/exec/test_when_any.cpp | 40 +++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/include/exec/when_any.hpp b/include/exec/when_any.hpp index 4f2b936ab..1e1a58bae 100644 --- a/include/exec/when_any.hpp +++ b/include/exec/when_any.hpp @@ -50,12 +50,12 @@ namespace experimental::execution using __raw_completions_t = __completion_signatures_of_t<_CvSender, __env_t<_Env>...>; template - using __decayed_completions_t = __transform_reduce_completion_signatures_t< - __raw_completions_t<_CvSender>, - __as_rvalues, - __as_error, - set_stopped_t (*)(), - __completion_signature_ptrs_t>; + using __decayed_completions_t = + __transform_reduce_completion_signatures_t<__raw_completions_t<_CvSender>, + __as_rvalues, + __as_error, + set_stopped_t (*)(), + __completion_signature_ptrs_t>; template using __all_nothrow_decay_copyable_results = diff --git a/test/exec/test_when_any.cpp b/test/exec/test_when_any.cpp index d5cb280bc..7a5518f1b 100644 --- a/test/exec/test_when_any.cpp +++ b/test/exec/test_when_any.cpp @@ -39,8 +39,8 @@ namespace using sender_concept = ex::sender_tag; template - static consteval auto get_completion_signatures() noexcept - -> ex::completion_signatures + static consteval auto + get_completion_signatures() noexcept -> ex::completion_signatures { return {}; } @@ -94,7 +94,7 @@ namespace { copy_noexcept_move_throws() = default; - copy_noexcept_move_throws(copy_noexcept_move_throws const&) noexcept = default; + copy_noexcept_move_throws(copy_noexcept_move_throws const &) noexcept = default; copy_noexcept_move_throws(copy_noexcept_move_throws&&) noexcept(false) {} }; @@ -113,7 +113,7 @@ namespace template struct operation { - Receiver rcvr_; + Receiver rcvr_; copy_noexcept_move_throws error_; void start() & noexcept @@ -170,7 +170,7 @@ namespace : throw_on_copy_{throw_on_copy} {} - throwing_error(throwing_error const& other) + throwing_error(throwing_error const & other) : throw_on_copy_{other.throw_on_copy_} { if (*throw_on_copy_) @@ -398,12 +398,12 @@ namespace TEST_CASE("when_any decays error completion arguments", "[adaptors][when_any]") { auto snd = exec::when_any(error_ref_sender{}); - static_assert(set_equivalent, - completion_signatures>); + static_assert( + set_equivalent, + completion_signatures>); bool lvalue_error = false; - auto op = ex::connect(std::move(snd), error_ref_receiver{&lvalue_error}); + auto op = ex::connect(std::move(snd), error_ref_receiver{&lvalue_error}); ex::start(op); CHECK_FALSE(lvalue_error); } @@ -412,15 +412,15 @@ namespace TEST_CASE("when_any reports errors from throwing error decay", "[adaptors][when_any]") { bool throw_on_copy = false; - auto snd = exec::when_any(throwing_error_sender{&throw_on_copy}); + auto snd = exec::when_any(throwing_error_sender{&throw_on_copy}); static_assert(set_equivalent, completion_signatures>); bool got_exception = false; - auto op = ex::connect(std::move(snd), throwing_error_receiver{&got_exception}); - throw_on_copy = true; + auto op = ex::connect(std::move(snd), throwing_error_receiver{&got_exception}); + throw_on_copy = true; ex::start(op); CHECK(got_exception); } @@ -428,7 +428,7 @@ namespace TEST_CASE("when_any reports errors for potentially throwing value moves", "[adaptors][when_any]") { copy_noexcept_move_throws value; - auto snd = exec::when_any(just_ref{value}); + auto snd = exec::when_any(just_ref{value}); static_assert(set_equivalent, completion_signatures{ - &got_value, &got_error, &got_exception}); + auto op = ex::connect(std::move(snd), + throwing_move_receiver{&got_value, + &got_error, + &got_exception}); ex::start(op); CHECK(got_value); CHECK_FALSE(got_error); @@ -457,9 +458,10 @@ namespace bool got_value = false; bool got_error = false; bool got_exception = false; - auto op = ex::connect(std::move(snd), - throwing_move_receiver{ - &got_value, &got_error, &got_exception}); + auto op = ex::connect(std::move(snd), + throwing_move_receiver{&got_value, + &got_error, + &got_exception}); ex::start(op); CHECK_FALSE(got_value); CHECK(got_error);