From f4509a9f9c5c13872da8e73b0e0e220fbe523565 Mon Sep 17 00:00:00 2001 From: Emmett Date: Wed, 7 Oct 2026 22:02:32 +0800 Subject: [PATCH 1/2] Constrain completion CPOs to non-const rvalue receivers Update receiver tests and fix affected calls in parallel_scheduler and receiver storage in merge_each. --- include/exec/sequence/merge_each.hpp | 6 +- .../stdexec/__detail/__parallel_scheduler.hpp | 2 +- include/stdexec/__detail/__receivers.hpp | 13 +++- test/stdexec/cpos/test_cpo_receiver.cpp | 68 +++++++++---------- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/include/exec/sequence/merge_each.hpp b/include/exec/sequence/merge_each.hpp index 814df5ff3..1d5f80805 100644 --- a/include/exec/sequence/merge_each.hpp +++ b/include/exec/sequence/merge_each.hpp @@ -1092,8 +1092,10 @@ namespace experimental::execution using sender_concept = STDEXEC::sender_tag; template - using __next_sequence_op_t = - __next_sequence_op<_NestedSequenceSender, _NextReceiver, _OperationBase, _NestedSeqOp>; + using __next_sequence_op_t = __next_sequence_op<_NestedSequenceSender, + __decay_t<_NextReceiver>, + _OperationBase, + _NestedSeqOp>; template static consteval auto get_completion_signatures() noexcept diff --git a/include/stdexec/__detail/__parallel_scheduler.hpp b/include/stdexec/__detail/__parallel_scheduler.hpp index 5a08d3b48..c3263bcce 100644 --- a/include/stdexec/__detail/__parallel_scheduler.hpp +++ b/include/stdexec/__detail/__parallel_scheduler.hpp @@ -530,7 +530,7 @@ namespace STDEXEC auto st = get_stop_token(STDEXEC::get_env(__state_.__rcvr_)); if (st.stop_requested()) { - STDEXEC::set_stopped(__state_.__rcvr_); + STDEXEC::set_stopped(static_cast<__rcvr_t&&>(__state_.__rcvr_)); return; } diff --git a/include/stdexec/__detail/__receivers.hpp b/include/stdexec/__detail/__receivers.hpp index f814888e6..5078b8fa0 100644 --- a/include/stdexec/__detail/__receivers.hpp +++ b/include/stdexec/__detail/__receivers.hpp @@ -33,6 +33,7 @@ import stdexec; # if !STDEXEC_USE_MODULES() # include +# include # endif # include "__prologue.hpp" @@ -41,6 +42,12 @@ namespace STDEXEC { namespace __detail { + // For template arguments deduced from a forwarding-reference parameter: + // lvalues deduce to T&; rvalues deduce to T, preserving const qualification. + // This check assumes deduction, not explicitly supplied template arguments. + template + concept __non_const_rvalue = (!std::is_lvalue_reference_v<_Ty>) && (!STDEXEC_IS_CONST(_Ty)); + template <__disposition _Disposition> struct __completion_tag { @@ -114,7 +121,7 @@ namespace STDEXEC //! `.set_value(_As...)` member. //! @tparam _As The value-datum argument types. template - requires __set_value_member<_Receiver, _As...> + requires __detail::__non_const_rvalue<_Receiver> && __set_value_member<_Receiver, _As...> STDEXEC_ATTRIBUTE(host, device, always_inline) constexpr void operator()(_Receiver &&__rcvr, _As &&...__as) const noexcept { @@ -182,7 +189,7 @@ namespace STDEXEC //! @tparam _Receiver A type with a matching `.set_error(_Error)` member. //! @tparam _Error The error datum type. template - requires __set_error_member<_Receiver, _Error> + requires __detail::__non_const_rvalue<_Receiver> && __set_error_member<_Receiver, _Error> STDEXEC_ATTRIBUTE(host, device, always_inline) constexpr void operator()(_Receiver &&__rcvr, _Error &&__err) const noexcept { @@ -247,7 +254,7 @@ namespace STDEXEC //! @tparam _Receiver A type with a matching nullary //! `.set_stopped()` member. template - requires __set_stopped_member<_Receiver> + requires __detail::__non_const_rvalue<_Receiver> && __set_stopped_member<_Receiver> STDEXEC_ATTRIBUTE(host, device, always_inline) constexpr void operator()(_Receiver &&__rcvr) const noexcept { diff --git a/test/stdexec/cpos/test_cpo_receiver.cpp b/test/stdexec/cpos/test_cpo_receiver.cpp index 96267e389..674127ccd 100644 --- a/test/stdexec/cpos/test_cpo_receiver.cpp +++ b/test/stdexec/cpos/test_cpo_receiver.cpp @@ -35,7 +35,7 @@ namespace struct recv_value { - int* target_; + int *target_; void set_value(int val) noexcept { @@ -55,7 +55,7 @@ namespace struct recv_rvalref { - int* target_; + int *target_; void set_value(int val) noexcept { @@ -75,7 +75,7 @@ namespace struct recv_ref { - int* target_; + int *target_; void set_value(int val) noexcept { @@ -95,7 +95,7 @@ namespace struct recv_cref { - int* target_; + int *target_; void set_value(int val) noexcept { @@ -113,6 +113,14 @@ namespace } }; + // Const members ensure that rejection comes from the CPO constraints. + struct recv_const + { + void set_value(int) const noexcept {} + void set_error(int) const noexcept {} + void set_stopped() const noexcept {} + }; + TEST_CASE("can call set_value on a void receiver", "[cpo][cpo_receiver]") { ex::set_value(expect_void_receiver{}); @@ -170,16 +178,6 @@ namespace REQUIRE(val == 10); } - TEST_CASE("can call set_value on a receiver with ref type", "[cpo][cpo_receiver]") - { - static_assert(std::invocable, - "cannot call set_value on recv_ref"); - int val = 0; - recv_ref recv{&val}; - ex::set_value(recv, 10); - REQUIRE(val == 10); - } - TEST_CASE("can call set_value on a receiver with const ref type", "[cpo][cpo_receiver]") { static_assert(std::invocable, @@ -207,16 +205,6 @@ namespace REQUIRE(val == -10); } - TEST_CASE("can call set_error on a receiver with ref type", "[cpo][cpo_receiver]") - { - static_assert(std::invocable, - "cannot call set_error on recv_ref"); - int val = 0; - recv_ref recv{&val}; - ex::set_error(recv, 10); - REQUIRE(val == -10); - } - TEST_CASE("can call set_error on a receiver with const ref type", "[cpo][cpo_receiver]") { static_assert(std::invocable, @@ -244,16 +232,6 @@ namespace REQUIRE(val == INT_MAX); } - TEST_CASE("can call set_stopped on a receiver with ref type", "[cpo][cpo_receiver]") - { - static_assert(std::invocable, - "cannot call set_stopped on recv_ref"); - int val = 0; - recv_ref recv{&val}; - ex::set_stopped(recv); - REQUIRE(val == INT_MAX); - } - TEST_CASE("can call set_stopped on a receiver with const ref type", "[cpo][cpo_receiver]") { static_assert(std::invocable, @@ -263,6 +241,28 @@ namespace REQUIRE(val == INT_MAX); } + TEST_CASE("completion CPOs reject lvalue receivers", "[cpo][cpo_receiver]") + { + static_assert(!std::invocable); + static_assert(!std::invocable); + static_assert(!std::invocable); + + static_assert(!std::invocable); + static_assert(!std::invocable); + static_assert(!std::invocable); + } + + TEST_CASE("completion CPOs reject const rvalue receivers", "[cpo][cpo_receiver]") + { + static_assert(std::invocable); + static_assert(std::invocable); + static_assert(std::invocable); + + static_assert(!std::invocable); + static_assert(!std::invocable); + static_assert(!std::invocable); + } + TEST_CASE("tag types can be deduced from set_value, set_error and set_stopped", "[cpo][cpo_receiver]") { From be8962a9ee0ab875c63afefa8e52323f7ad13892 Mon Sep 17 00:00:00 2001 From: Emmett Date: Thu, 8 Oct 2026 14:52:53 +0800 Subject: [PATCH 2/2] fix review comment --- include/stdexec/__detail/__receivers.hpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/include/stdexec/__detail/__receivers.hpp b/include/stdexec/__detail/__receivers.hpp index cb8aa0a14..2cc3197bd 100644 --- a/include/stdexec/__detail/__receivers.hpp +++ b/include/stdexec/__detail/__receivers.hpp @@ -33,7 +33,6 @@ import stdexec; # if !STDEXEC_USE_MODULES() # include -# include # endif # include "__prologue.hpp" @@ -42,11 +41,9 @@ namespace STDEXEC { namespace __detail { - // For template arguments deduced from a forwarding-reference parameter: - // lvalues deduce to T&; rvalues deduce to T, preserving const qualification. - // This check assumes deduction, not explicitly supplied template arguments. + // For deduced forwarding-reference arguments, rejects lvalues and const rvalues. template - concept __non_const_rvalue = (!std::is_lvalue_reference_v<_Ty>) && (!STDEXEC_IS_CONST(_Ty)); + concept __non_const_rvalue = !STDEXEC_IS_SAME(_Ty, _Ty const); template <__disposition _Disposition> struct __completion_tag