From 24bf99f33beabce18cae7d9d8e991dfa7727ec72 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 13:09:50 -0400 Subject: [PATCH 01/15] Include __storage.hpp From execution.hpp Several headers in exec depend on __storage.hpp. --- include/stdexec/execution.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index 9f5f6a4da..3d357664a 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -55,6 +55,7 @@ #include "__detail/__starts_on.hpp" #include "__detail/__stopped_as_error.hpp" #include "__detail/__stopped_as_optional.hpp" +#include "__detail/__storage.hpp" #include "__detail/__submit.hpp" #include "__detail/__sync_wait.hpp" #include "__detail/__task.hpp" From 7cdfa49e3c74d8ad12f551dd1c606739dc73d2f1 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Mon, 29 Jun 2026 14:00:30 -0400 Subject: [PATCH 02/15] matching_completion_signature_t Allows for template arguments deduced from a completion, for example in: template void set_value(Args&&...); To be used to determine which completion signature such a completion corresponds to. See P4288. --- .../__matching_completion_signature.hpp | 104 ++++++++++++++++++ include/stdexec/execution.hpp | 1 + test/CMakeLists.txt | 3 +- .../test_matching_completion_signature.cpp | 81 ++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 include/stdexec/__detail/__matching_completion_signature.hpp create mode 100644 test/stdexec/types/test_matching_completion_signature.cpp diff --git a/include/stdexec/__detail/__matching_completion_signature.hpp b/include/stdexec/__detail/__matching_completion_signature.hpp new file mode 100644 index 000000000..83d3678be --- /dev/null +++ b/include/stdexec/__detail/__matching_completion_signature.hpp @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2026 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "__completion_signatures.hpp" + +#include +#include + +namespace STDEXEC +{ + + namespace __matching_completion_signature + { + + template + struct __canonicalize; + + template + struct __canonicalize<_Tag(_Args...)> + { + using type = _Tag(_Args&&...); + }; + + template + using __canonicalize_t = typename __canonicalize<_Signature>::type; + + template + inline constexpr bool __is_canonical_match = + std::is_same_v<__canonicalize_t<_Needle>, __canonicalize_t<_Signature>>; + + template + struct __canonical_match_count; + + template + struct __canonical_match_count<_Needle, completion_signatures<_Signatures...>> + : std::integral_constant + ... + 0)> + {}; + + template + struct __find_canonical_match; + + template + struct __find_canonical_match<_Needle> + {}; + + template + struct __find_canonical_match<_Needle, _Signature, _Rest...> + : std::conditional_t<__is_canonical_match<_Needle, _Signature>, + std::type_identity<_Signature>, + __find_canonical_match<_Needle, _Rest...>> + {}; + + template ::value> + struct __matching_completion_signature + {}; + + template + struct __matching_completion_signature, _Signature, 1> + : __find_canonical_match<_Signature, _Signatures...> + {}; + + } // namespace __matching_completion_signature + + template + using matching_completion_signature_t = + typename __matching_completion_signature::__matching_completion_signature<_Signatures, + _Tag(_Args...)>::type; + + template + inline constexpr bool has_matching_completion_signature_v = requires { + typename matching_completion_signature_t<_Signatures, _Tag, _Args...>; + }; + + template + struct matching_completion_signature + { + using type = matching_completion_signature_t<_Signatures, _Tag, _Args...>; + }; + + template + struct has_matching_completion_signature + : std::bool_constant> + {}; + +} // namespace STDEXEC diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index 3d357664a..357899ec8 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -37,6 +37,7 @@ #include "__detail/__intrusive_slist.hpp" #include "__detail/__just.hpp" #include "__detail/__let.hpp" +#include "__detail/__matching_completion_signature.hpp" #include "__detail/__meta.hpp" #include "__detail/__on.hpp" #include "__detail/__operation_states.hpp" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bf974fbc1..4c5f31cf0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -87,7 +87,8 @@ set(stdexec_test_sources stdexec/queries/test_get_completion_behavior.cpp stdexec/queries/test_forwarding_queries.cpp stdexec/types/test_task.cpp - stdexec/types/test_counting_scopes.cpp) + stdexec/types/test_counting_scopes.cpp + stdexec/types/test_matching_completion_signature.cpp) add_library(common_test_settings INTERFACE) set_target_properties( diff --git a/test/stdexec/types/test_matching_completion_signature.cpp b/test/stdexec/types/test_matching_completion_signature.cpp new file mode 100644 index 000000000..09ad99e8a --- /dev/null +++ b/test/stdexec/types/test_matching_completion_signature.cpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2026 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +namespace +{ + + struct base + {}; + + struct derived : base + {}; + + using signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_value_t(base), + ::STDEXEC::set_value_t(derived&), + ::STDEXEC::set_error_t(std::exception_ptr), + ::STDEXEC::set_stopped_t()>; + + static_assert(std::is_same_v< + ::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_value_t(int)>); + + static_assert(std::is_same_v<::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_error_t(std::exception_ptr)>); + + static_assert( + std::is_same_v<::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_stopped_t()>); + + static_assert( + std::is_same_v< + ::STDEXEC::matching_completion_signature_t, + ::STDEXEC::set_value_t(derived&)>); + + static_assert( + ::STDEXEC::has_matching_completion_signature_v); + static_assert( + ::STDEXEC::has_matching_completion_signature::value); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + static_assert(!::STDEXEC::has_matching_completion_signature::value); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + + static_assert( + !::STDEXEC::has_matching_completion_signature_v); + + TEST_CASE("matching_completion_signature compile-time checks", "[matching_completion_signature]") + { + CHECK(true); + } + +} // unnamed namespace From b1d04e0c046b26ba6b4f7969987f1213c6eff553 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 21:45:39 -0400 Subject: [PATCH 03/15] __tuple: Reference Aware --- include/stdexec/__detail/__tuple.hpp | 32 +++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/include/stdexec/__detail/__tuple.hpp b/include/stdexec/__detail/__tuple.hpp index 82255c293..cb96d7964 100644 --- a/include/stdexec/__detail/__tuple.hpp +++ b/include/stdexec/__detail/__tuple.hpp @@ -80,7 +80,8 @@ namespace STDEXEC { return static_cast<_Fn&&>(__fn)( static_cast<_Us&&>(__us)..., - static_cast<_Self&&>(__self).STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value...); + static_cast<__copy_cvref_t<_Self&&, _Ts>>( + static_cast<_Self&&>(__self).STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value)...); } }; @@ -232,7 +233,10 @@ namespace STDEXEC template STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE __tuple(_Ts...) -> __tuple<_Ts...>; -# define STDEXEC_TUPLE_GET(_Idx) , static_cast<_Tuple&&>(__tupl).__val##_Idx +# define STDEXEC_TUPLE_GET(_Idx) \ + , static_cast< \ + __copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val##_Idx)>>( \ + static_cast<_Tuple&&>(__tupl).__val##_Idx) // // __apply(fn, tuple, extra...) @@ -376,35 +380,43 @@ namespace STDEXEC } else if constexpr (_Index == 0) { - return static_cast<_Tuple&&>(__tupl).__val0; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val0)>>( + static_cast<_Tuple&&>(__tupl).__val0); } else if constexpr (_Index == 1) { - return static_cast<_Tuple&&>(__tupl).__val1; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val1)>>( + static_cast<_Tuple&&>(__tupl).__val1); } else if constexpr (_Index == 2) { - return static_cast<_Tuple&&>(__tupl).__val2; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val2)>>( + static_cast<_Tuple&&>(__tupl).__val2); } else if constexpr (_Index == 3) { - return static_cast<_Tuple&&>(__tupl).__val3; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val3)>>( + static_cast<_Tuple&&>(__tupl).__val3); } else if constexpr (_Index == 4) { - return static_cast<_Tuple&&>(__tupl).__val4; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val4)>>( + static_cast<_Tuple&&>(__tupl).__val4); } else if constexpr (_Index == 5) { - return static_cast<_Tuple&&>(__tupl).__val5; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val5)>>( + static_cast<_Tuple&&>(__tupl).__val5); } else if constexpr (_Index == 6) { - return static_cast<_Tuple&&>(__tupl).__val6; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val6)>>( + static_cast<_Tuple&&>(__tupl).__val6); } else if constexpr (_Index == 7) { - return static_cast<_Tuple&&>(__tupl).__val7; + return static_cast<__copy_cvref_t<_Tuple&&, decltype(static_cast<_Tuple&&>(__tupl).__val7)>>( + static_cast<_Tuple&&>(__tupl).__val7); } else { From 8a101c443224d64c49ccddc20de56ada259ebc34 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Mon, 29 Jun 2026 14:00:39 -0400 Subject: [PATCH 04/15] storage_for_completion_signatures Provides a turnkey, reference-aware way to store completion signatures for later examination or transmission. See P4288. Note that this implementation diverges from the implementation described in P4288. In order to remain device compatible this implementation stores a __tuple rather than a std::tuple. Therefore storage_for_completion_signature::arguments cannot be provided (since it returns a reference to the directly stored std::tuple, which doesn't exist in this implementation). Moreover to support algorithms which aim to be usable on host and device the implementation in this commit provides both storage_for_completion_signature::forward_arguments and ::__forward_arguments where the latter returns a __tuple and can therefore be marked as being device compatible. --- .../__storage_for_completion_signatures.hpp | 734 ++++++++++++++ include/stdexec/execution.hpp | 1 + test/CMakeLists.txt | 3 +- ...test_storage_for_completion_signatures.cpp | 949 ++++++++++++++++++ 4 files changed, 1686 insertions(+), 1 deletion(-) create mode 100644 include/stdexec/__detail/__storage_for_completion_signatures.hpp create mode 100644 test/stdexec/types/test_storage_for_completion_signatures.cpp diff --git a/include/stdexec/__detail/__storage_for_completion_signatures.hpp b/include/stdexec/__detail/__storage_for_completion_signatures.hpp new file mode 100644 index 000000000..2e215d90d --- /dev/null +++ b/include/stdexec/__detail/__storage_for_completion_signatures.hpp @@ -0,0 +1,734 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../functional.hpp" +#include "__config.hpp" +#include "__matching_completion_signature.hpp" +#include "__receivers.hpp" +#include "__transform_completion_signatures.hpp" +#include "__tuple.hpp" +#include "__variant.hpp" + +#include +#include +#include +#include + +namespace STDEXEC +{ + + template + struct storage_for_completion_signature; + + template + struct storage_for_completion_signature<_Tag(_Args...)> + { + using tag_type = _Tag; + using signature_type = _Tag(_Args...); + using __normalized_signature_t = _Tag(_Args&&...); + + private: + using __tuple_t = __tuple<_Args...>; + + struct __forward_as_tuple_t + { + template + constexpr std::tuple<_Ts&&...> operator()(_Ts&&... __args) const noexcept + { + return {static_cast<_Ts&&>(__args)...}; + } + }; + + struct __forward_as_internal_tuple_t + { + template + STDEXEC_ATTRIBUTE(host, device) + constexpr __tuple<_Ts&&...> operator()(_Ts&&... __args) const noexcept + { + return {static_cast<_Ts&&>(__args)...}; + } + }; + + public: + template + requires(sizeof...(_OtherArgs) == sizeof...(_Args)) + && (std::is_constructible_v<_Args, _OtherArgs> && ...) + STDEXEC_ATTRIBUTE(host, device) + constexpr explicit storage_for_completion_signature(_Tag, _OtherArgs&&... __args) + noexcept((std::is_nothrow_constructible_v<_Args, _OtherArgs> && ...)) + : __args_{static_cast<_OtherArgs&&>(__args)...} + {} + + STDEXEC_ATTRIBUTE(host, device) + static constexpr auto tag() noexcept -> _Tag + { + return {}; + } + + constexpr auto forward_arguments() & noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, __args_); + } + + constexpr auto forward_arguments() const & noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, __args_); + } + + constexpr auto forward_arguments() && noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, static_cast<__tuple_t&&>(__args_)); + } + + constexpr auto forward_arguments() const && noexcept + { + return STDEXEC::__apply(__forward_as_tuple_t{}, static_cast<__tuple_t const &&>(__args_)); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() & noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, __args_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() const & noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, __args_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() && noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, static_cast<__tuple_t&&>(__args_)); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr auto __forward_arguments() const && noexcept + { + return STDEXEC::__apply(__forward_as_internal_tuple_t{}, + static_cast<__tuple_t const &&>(__args_)); + } + + private: + __tuple_t __args_; + }; + + namespace __completion_storage + { + + template + using __storage_for_arrival_t = storage_for_completion_signature< + matching_completion_signature_t<_Signatures, _Tag, _Args...>>; + + template + struct __variant_for_signatures; + + template + struct __variant_for_signatures> + { + using __t = __uniqued_variant...>; + }; + + template + struct __nothrow_storable; + + template + struct __nothrow_storable<_Tag(_Args...)> + { + static constexpr bool value = (std::is_nothrow_constructible_v<_Args, _Args> && ...); + }; + + template + struct __arrival_storable; + + template + struct __arrival_storable, _Args...> + { + static constexpr bool value = + sizeof...(_StoredArgs) == sizeof...(_Args) + && ((std::is_reference_v<_StoredArgs> || !std::is_lvalue_reference_v<_Args>) && ...); + }; + + struct __ambiguous_normalized_completion_signatures; + struct __non_persistable_completion_signature; + + template + struct __persistable_completion_signature; + + template + struct __persistable_completion_signature<_Tag(_Args...)> + { + static constexpr bool value = + ((std::is_reference_v<_Args> || std::is_move_constructible_v<_Args>) && ...); + }; + + template + struct __persistable_completion_signatures; + + template + struct __persistable_completion_signatures> + { + static constexpr bool value = (__persistable_completion_signature<_Signatures>::value && ...); + }; + + template + struct __normalized_completion_signatures; + + template + struct __normalized_completion_signatures> + { + using __t = transform_completion_signatures::__normalized_signature_t...>>; + }; + + template + using __normalized_completion_signatures_t = + typename __normalized_completion_signatures<_Signatures>::__t; + + template + inline constexpr bool __has_unique_normalized_completion_signatures = + __mapply<__msize, _Signatures>::value + == __mapply<__msize, __normalized_completion_signatures_t<_Signatures>>::value; + + template + consteval auto __get_completion_signatures() + { + if constexpr (!__has_unique_normalized_completion_signatures<_InputSignatures>) + { + return __throw_compile_time_error<__ambiguous_normalized_completion_signatures>(); + } + else if constexpr (!__persistable_completion_signatures<_InputSignatures>::value) + { + return __throw_compile_time_error<__non_persistable_completion_signature>(); + } + else + { + return _OutputSignatures{}; + } + } + + template + using __variant_for_storage_t = decltype(std::declval<_Storage>().__get_variant()); + + template + struct __variant_alternatives; + + template + struct __variant_alternatives<__variant<_Alternatives...>> + { + template + using __with_cvref_t = __mlist...>; + }; + + template + using __variant_alternatives_t = typename __variant_alternatives< + std::remove_cvref_t<_Variant>>::template __with_cvref_t<_Variant>; + + template + struct __nothrow_visit_stored_completion; + + template + struct __nothrow_visit_stored_completion<_Visitor, __mlist<_Alternatives...>> + { + static constexpr bool value = __nothrow_invocable<_Visitor, _Alternatives...>; + }; + + template + struct __nothrow_visit_stored_completion<_Visitor, + __mlist<_Alternatives...>, + __mlist<_CurrentAlternatives...>, + _Variants...> + { + static constexpr bool value = + (__nothrow_visit_stored_completion<_Visitor, + __mlist<_Alternatives..., _CurrentAlternatives>, + _Variants...>::value + && ...); + }; + + template + constexpr bool __nothrow_visit_stored_completion_for_variants_v = + __nothrow_invocable<_Visitor> + && __nothrow_visit_stored_completion<_Visitor, + __mlist<>, + __variant_alternatives_t<_Variants>...>::value; + + template + constexpr bool __nothrow_visit_stored_completion_v = + __nothrow_visit_stored_completion_for_variants_v<_Visitor, + __variant_for_storage_t<_Storages>...>; + + template + struct __nothrow_visit_stored_completion_r; + + template + struct __nothrow_visit_stored_completion_r<_Result, _Visitor, __mlist<_Alternatives...>> + { + static constexpr bool value = + std::is_nothrow_invocable_r_v<_Result, _Visitor, _Alternatives...>; + }; + + template + struct __nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<_Alternatives...>, + __mlist<_CurrentAlternatives...>, + _Variants...> + { + static constexpr bool value = + (__nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<_Alternatives..., _CurrentAlternatives>, + _Variants...>::value + && ...); + }; + + template + constexpr bool __nothrow_visit_stored_completion_r_for_variants_v = + std::is_nothrow_invocable_r_v<_Result, _Visitor> + && __nothrow_visit_stored_completion_r<_Result, + _Visitor, + __mlist<>, + __variant_alternatives_t<_Variants>...>::value; + + template + constexpr bool __nothrow_visit_stored_completion_r_v = + __nothrow_visit_stored_completion_r_for_variants_v<_Result, + _Visitor, + __variant_for_storage_t<_Storages>...>; + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) __visit_variant_completion(_Visitor&& __visitor, _Variant&& __variant) + { + return STDEXEC::__visit(static_cast<_Visitor&&>(__visitor), + static_cast<_Variant&&>(__variant)); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) + __visit_present_stored_completion(_Visitor&& __visitor, _Storage&& __storage) + { + return __completion_storage::__visit_variant_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage).__get_variant()); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) __visit_present_stored_completion(_Visitor&& __visitor, + _Storage&& __storage, + _Storages&&... __storages) + { + return __completion_storage::__visit_variant_completion( + [&](auto&& __completion) -> decltype(auto) + { + return __completion_storage::__visit_present_stored_completion( + [&](auto&&... __completions) -> decltype(auto) + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor), + static_cast(__completion), + static_cast(__completions)...); + }, + static_cast<_Storages&&>(__storages)...); + }, + static_cast<_Storage&&>(__storage).__get_variant()); + } + + } // namespace __completion_storage + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr decltype(auto) + visit_stored_completion(_Visitor&& __visitor, _Storage&& __storage, _Storages&&... __storages) + noexcept( + __completion_storage::__nothrow_visit_stored_completion_v<_Visitor, _Storage, _Storages...>) + { + if (!(__storage.has_completion() && ... && __storages.has_completion())) + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + } + return __completion_storage::__visit_present_stored_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>(__storages)...); + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr _Result + visit_stored_completion(_Visitor&& __visitor, _Storage&& __storage, _Storages&&... __storages) + noexcept(__completion_storage::__nothrow_visit_stored_completion_r_v<_Result, + _Visitor, + _Storage, + _Storages...>) + { + if (!(__storage.has_completion() && ... && __storages.has_completion())) + { + if constexpr (std::is_void_v<_Result>) + { + STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + return; + } + else + { + return STDEXEC::__invoke(static_cast<_Visitor&&>(__visitor)); + } + } + if constexpr (std::is_void_v<_Result>) + { + __completion_storage::__visit_present_stored_completion(static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>( + __storages)...); + return; + } + else + { + return __completion_storage::__visit_present_stored_completion( + static_cast<_Visitor&&>(__visitor), + static_cast<_Storage&&>(__storage), + static_cast<_Storages&&>(__storages)...); + } + } + + enum class storage_for_completion_signatures_error_policy + { + internalize, + propagate + }; + + template + class storage_for_completion_signatures; + + template <> + class storage_for_completion_signatures< + completion_signatures<>, + storage_for_completion_signatures_error_policy::internalize> + { + public: + using completion_signatures = STDEXEC::completion_signatures<>; + static constexpr bool nothrow_arrive = true; + + static consteval auto get_completion_signatures() noexcept + { + return completion_signatures{}; + } + + STDEXEC_ATTRIBUTE(host, device) + static constexpr __variant<> __get_variant() noexcept + { + return __variant<>(__no_init); + } + + STDEXEC_ATTRIBUTE(host, device) + static constexpr bool has_completion() noexcept + { + return false; + } + + template + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&&) && noexcept + { + return false; + } + }; + + template <> + class storage_for_completion_signatures, + storage_for_completion_signatures_error_policy::propagate> + : public storage_for_completion_signatures< + completion_signatures<>, + storage_for_completion_signatures_error_policy::internalize> + {}; + + template + class storage_for_completion_signatures< + completion_signatures<_Signatures...>, + storage_for_completion_signatures_error_policy::internalize> + { + using __input_completion_signatures_t = + transform_completion_signatures>; + static constexpr auto __nothrow = (__completion_storage::__nothrow_storable<_Signatures>::value + && ...); + using __maybe_throwing_signature_t = + std::conditional_t<__nothrow, + STDEXEC::completion_signatures<>, + STDEXEC::completion_signatures>; + public: + using completion_signatures = transform_completion_signatures<__input_completion_signatures_t, + __maybe_throwing_signature_t>; + static constexpr bool nothrow_arrive = true; + + static consteval auto get_completion_signatures() + { + return __completion_storage::__get_completion_signatures<__input_completion_signatures_t, + completion_signatures>(); + } + + private: + template + using __storage_for_arrival_t = + __completion_storage::__storage_for_arrival_t<__input_completion_signatures_t, _Tag, _Args...>; + using __storage_t = + typename __completion_storage::__variant_for_signatures::__t; + __storage_t __storage_; + public: + STDEXEC_ATTRIBUTE(host, device) + constexpr storage_for_completion_signatures() noexcept + : __storage_(__no_init) + {} + + template + requires std::is_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...> + && __completion_storage::__arrival_storable<__storage_for_arrival_t<_Tag, _Args...>, + _Args...>::value + STDEXEC_ATTRIBUTE(host, device) + constexpr void arrive(_Tag __tag, _Args&&... __args) noexcept + { + STDEXEC_ASSERT(__storage_.__is_valueless()); + constexpr auto __nothrow = + std::is_nothrow_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...>; + auto const __impl = [&]() noexcept(__nothrow) + { + __storage_.template emplace<__storage_for_arrival_t<_Tag, _Args...>>((_Tag&&) __tag, + (_Args&&) __args...); + }; + if constexpr (__nothrow) + { + __impl(); + } + else + { + STDEXEC_TRY + { + __impl(); + } + STDEXEC_CATCH_ALL + { + __storage_ + .template emplace>( + set_error, + std::current_exception()); + } + } + } + STDEXEC_ATTRIBUTE(host, device) + constexpr bool has_completion() const noexcept + { + return !__storage_.__is_valueless(); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t& __get_variant() & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const & __get_variant() const & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t&& __get_variant() && noexcept + { + return static_cast<__storage_t&&>(__storage_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const && __get_variant() const && noexcept + { + return static_cast<__storage_t const &&>(__storage_); + } + + template _Receiver> + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&& __rcvr) && noexcept + { + if (!has_completion()) + { + return false; + } + STDEXEC::__visit( + [&](auto&& __active_alternative) noexcept + { + using __completion_t = std::remove_cvref_t; + constexpr auto __nothrow = + std::is_nothrow_constructible_v<__completion_t, __completion_t&&>; + auto const __complete = [&]() noexcept(__nothrow) + { + auto __completion = __completion_t(std::move(__active_alternative)); + STDEXEC::__apply( + [&](auto&&... __args) noexcept + { + typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...); + }, + // Odds are this is inside an operation state, which means that + // sending the completion signal may end our lifetime, which means + // we shouldn't send references into ourselves, therefore we move + // all the non-references onto the stack + std::move(__completion).__forward_arguments()); + }; + if constexpr (__nothrow) + { + __complete(); + } + else + { + STDEXEC_TRY + { + __complete(); + } + STDEXEC_CATCH_ALL + { + set_error((_Receiver&&) __rcvr, std::current_exception()); + } + } + }, + (__storage_t&&) __storage_); + return true; + } + }; + + template + class storage_for_completion_signatures, + storage_for_completion_signatures_error_policy::propagate> + { + using __input_completion_signatures_t = + transform_completion_signatures>; + public: + using completion_signatures = __input_completion_signatures_t; + static constexpr bool nothrow_arrive = + (__completion_storage::__nothrow_storable<_Signatures>::value && ...); + + static consteval auto get_completion_signatures() + { + return __completion_storage::__get_completion_signatures<__input_completion_signatures_t, + completion_signatures>(); + } + + private: + template + using __storage_for_arrival_t = + __completion_storage::__storage_for_arrival_t<__input_completion_signatures_t, _Tag, _Args...>; + using __storage_t = + typename __completion_storage::__variant_for_signatures::__t; + __storage_t __storage_; + + public: + STDEXEC_ATTRIBUTE(host, device) + constexpr storage_for_completion_signatures() noexcept + : __storage_(__no_init) + {} + + template + requires std::is_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...> + && __completion_storage::__arrival_storable<__storage_for_arrival_t<_Tag, _Args...>, + _Args...>::value + STDEXEC_ATTRIBUTE(host, device) + constexpr void arrive(_Tag __tag, _Args&&... __args) noexcept( + std::is_nothrow_constructible_v<__storage_for_arrival_t<_Tag, _Args...>, _Tag, _Args...>) + { + STDEXEC_ASSERT(__storage_.__is_valueless()); + STDEXEC_TRY + { + __storage_.template emplace<__storage_for_arrival_t<_Tag, _Args...>>((_Tag&&) __tag, + (_Args&&) __args...); + } + STDEXEC_CATCH_ALL + { + STDEXEC_THROW(); + } + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr bool has_completion() const noexcept + { + return !__storage_.__is_valueless(); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t& __get_variant() & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const & __get_variant() const & noexcept + { + return __storage_; + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t&& __get_variant() && noexcept + { + return static_cast<__storage_t&&>(__storage_); + } + + STDEXEC_ATTRIBUTE(host, device) + constexpr __storage_t const && __get_variant() const && noexcept + { + return static_cast<__storage_t const &&>(__storage_); + } + + template _Receiver> + STDEXEC_ATTRIBUTE(host, device) + constexpr bool complete(_Receiver&& __rcvr) && noexcept(nothrow_arrive) + { + if (!has_completion()) + { + return false; + } + STDEXEC::__visit( + [&](auto&& __active_alternative) noexcept(nothrow_arrive) + { + using __completion_t = std::remove_cvref_t; + auto __completion = __completion_t(std::move(__active_alternative)); + STDEXEC::__apply( + [&](auto&&... __args) noexcept( + noexcept(typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...))) + { + typename __completion_t::tag_type{}((_Receiver&&) __rcvr, + (decltype(__args)&&) __args...); + }, + std::move(__completion).__forward_arguments()); + }, + (__storage_t&&) __storage_); + return true; + } + }; + +} // namespace STDEXEC diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index 357899ec8..d686ebc2e 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -57,6 +57,7 @@ #include "__detail/__stopped_as_error.hpp" #include "__detail/__stopped_as_optional.hpp" #include "__detail/__storage.hpp" +#include "__detail/__storage_for_completion_signatures.hpp" #include "__detail/__submit.hpp" #include "__detail/__sync_wait.hpp" #include "__detail/__task.hpp" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4c5f31cf0..0a2b6fae6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -88,7 +88,8 @@ set(stdexec_test_sources stdexec/queries/test_forwarding_queries.cpp stdexec/types/test_task.cpp stdexec/types/test_counting_scopes.cpp - stdexec/types/test_matching_completion_signature.cpp) + stdexec/types/test_matching_completion_signature.cpp + stdexec/types/test_storage_for_completion_signatures.cpp) add_library(common_test_settings INTERFACE) set_target_properties( diff --git a/test/stdexec/types/test_storage_for_completion_signatures.cpp b/test/stdexec/types/test_storage_for_completion_signatures.cpp new file mode 100644 index 000000000..c7307acbd --- /dev/null +++ b/test/stdexec/types/test_storage_for_completion_signatures.cpp @@ -0,0 +1,949 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + + template + concept stored_completion = requires { typename std::remove_cvref_t::tag_type; }; + + template + concept completion_with_tag = + stored_completion + && std::is_same_v::tag_type, Tag>; + + template + struct overloaded : Fns... + { + using Fns::operator()...; + }; + + template + overloaded(Fns...) -> overloaded; + + template + concept tuple_size_defined_for = requires { typename std::tuple_size::type; }; + + template + concept tuple_element_defined_for = requires { typename std::tuple_element<0, T>::type; }; + + struct monostate_may_throw + { + void operator()(std::monostate) const {} + + template + requires completion_with_tag + void operator()(Completion&&) const noexcept + {} + }; + + static_assert(std::is_same_v<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int const)>); + static_assert(std::is_same_v<::STDEXEC::set_value_t(int*), ::STDEXEC::set_value_t(int[])>); + static_assert( + std::is_same_v<::STDEXEC::set_value_t(int (*)(int)), ::STDEXEC::set_value_t(int(int))>); + + TEST_CASE("A single stored completion exposes its tag and arguments", + "[storage_for_completion_signatures]") + { + using completion = + ::STDEXEC::storage_for_completion_signature<::STDEXEC::set_value_t(int, int&)>; + + static_assert(!std::derived_from>); + static_assert(!tuple_size_defined_for); + static_assert(!tuple_element_defined_for); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert( + std::is_same_v); + static_assert(std::is_constructible_v); + static_assert(!std::is_constructible_v); + static_assert(std::is_same_v); + static_assert(noexcept(completion::tag())); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().forward_arguments()), + std::tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert(std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + static_assert( + std::is_same_v().__forward_arguments()), + ::STDEXEC::__tuple>); + using internal_arguments = decltype(std::declval().__forward_arguments()); + static_assert( + std::is_same_v(std::declval())), int&&>); + static_assert( + std::is_same_v(std::declval())), int&>); + + int referenced = 42; + completion c{::STDEXEC::set_value, 13, referenced}; + auto args = c.forward_arguments(); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<1>(args) == &referenced); + + auto const & cc = c; + auto cc_args = cc.forward_arguments(); + std::get<1>(cc_args) = 99; + CHECK(referenced == 99); + + ::STDEXEC::__apply( + [](auto&& first, auto&& second) + { + static_assert(std::is_same_v); + static_assert(std::is_same_v); + }, + std::move(c).__forward_arguments()); + + int other = 7; + ::STDEXEC::storage_for_completion_signature<::STDEXEC::set_value_t(int&)> c2{ + ::STDEXEC::set_value, + other}; + bool invoked = false; + std::apply( + [&](auto&&... values) + { + CHECK(!invoked); + invoked = true; + CHECK(sizeof...(values) == 3); + }, + std::tuple_cat(std::move(c).forward_arguments(), c2.forward_arguments())); + CHECK(invoked); + } + + TEST_CASE("An internal argument tuple forwards more than eight values", + "[storage_for_completion_signatures]") + { + using completion = ::STDEXEC::storage_for_completion_signature< + ::STDEXEC::set_value_t(int, int, int, int, int, int, int, int, int)>; + + completion c{::STDEXEC::set_value, 0, 1, 2, 3, 4, 5, 6, 7, 8}; + ::STDEXEC::__apply( + [](_Ts&&... values) + { + static_assert(sizeof...(_Ts) == 9); + static_assert((std::is_same_v<_Ts, int> && ...)); + CHECK((values + ...) == 36); + }, + std::move(c).__forward_arguments()); + } + + template + concept can_arrive_set_value_with = requires(Storage& s, Value&& value) { + s.arrive(::STDEXEC::set_value, static_cast(value)); + }; + + template + concept can_arrive_set_error_with = requires(Storage& s, Error&& error) { + s.arrive(::STDEXEC::set_error, static_cast(error)); + }; + + struct copyable_value + { + copyable_value() = default; + copyable_value(copyable_value const &) noexcept = default; + copyable_value(copyable_value&&) noexcept = default; + }; + + using copyable_value_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(copyable_value)>>; + + static_assert(!can_arrive_set_value_with); + static_assert(can_arrive_set_value_with); + + TEST_CASE("Storing no completion signatures works", "[storage_for_completion_signatures]") + { + ::STDEXEC::storage_for_completion_signatures<::STDEXEC::completion_signatures<>> storage; + static_assert(std::is_empty_v); + static_assert( + std::is_same_v>); + static_assert(noexcept(storage.has_completion())); + CHECK(!storage.has_completion()); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion([&] { visited_empty = true; }, std::move(storage)); + CHECK(visited_empty); + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + explicit receiver(bool& moved) noexcept + : moved_(&moved) + {} + receiver(receiver&& other) noexcept + : moved_(std::exchange(other.moved_, nullptr)) + { + *moved_ = true; + } + void set_value() noexcept + { + FAIL("Unexpected value"); + } + bool* moved_; + }; + bool moved = false; + CHECK(!std::move(storage).complete(receiver{moved})); + CHECK(!moved); + } + + TEST_CASE("Storing simple completion signatures and then visiting them works", + "[storage_for_completion_signatures]") + { + using completion_signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::error_code)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(set_equivalent); + { + storage s; + CHECK(!s.has_completion()); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + std::move(s)); + CHECK(visited_empty); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_stopped))); + s.arrive(::STDEXEC::set_stopped); + CHECK(s.has_completion()); + static_assert(noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) noexcept {}}, + std::move(s)))); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + CHECK(std::tuple_size_v == 0); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_value, 5))); + s.arrive(::STDEXEC::set_value, 5); + CHECK(s.has_completion()); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 5); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + CHECK(!s.has_completion()); + static_assert(noexcept(s.arrive(::STDEXEC::set_error, std::error_code{}))); + s.arrive(::STDEXEC::set_error, make_error_code(std::errc::no_such_file_or_directory)); + CHECK(s.has_completion()); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) {}}, std::move(s)))); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == make_error_code(std::errc::no_such_file_or_directory)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + + TEST_CASE("Stored completions can be visited as completion objects", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int, int&)>>; + + int referenced = 42; + storage s; + s.arrive(::STDEXEC::set_value, 13, referenced); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = + static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<1>(args) == &referenced); + } + else + { + FAIL("Unexpected completion"); + } + }}, + s); + CHECK(invoked); + + auto const & cs = s; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + if constexpr (completion_with_tag) + { + static_assert(std::is_const_v>); + auto args = completion.forward_arguments(); + std::get<1>(args) = 99; + } + else + { + FAIL("Unexpected completion"); + } + }}, + cs); + CHECK(referenced == 99); + } + + TEST_CASE("Multiple stored completions can be visited together", + "[storage_for_completion_signatures]") + { + using first_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + using second_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_error_t(std::exception_ptr)>>; + using third_storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int&)>>; + + int referenced = 42; + first_storage first; + second_storage second; + third_storage third; + first.arrive(::STDEXEC::set_value, 13); + second.arrive(::STDEXEC::set_error, std::exception_ptr{}); + third.arrive(::STDEXEC::set_value, referenced); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& first_completion, auto&& second_completion, auto&& third_completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag + && completion_with_tag + && completion_with_tag) + { + auto args = std::tuple_cat( + static_cast(first_completion).forward_arguments(), + static_cast(second_completion).forward_arguments(), + static_cast(third_completion).forward_arguments()); + CHECK(std::get<0>(args) == 13); + CHECK(&std::get<2>(args) == &referenced); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(first), + std::move(second), + std::move(third)); + CHECK(invoked); + } + + TEST_CASE("Stored completion visitation computes noexcept", "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage s; + static_assert(noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) noexcept {}}, s))); + static_assert( + !noexcept(::STDEXEC::visit_stored_completion(overloaded{[] {}, [](auto&&) noexcept {}}, s))); + static_assert(!noexcept( + ::STDEXEC::visit_stored_completion(overloaded{[]() noexcept {}, [](auto&&) {}}, s))); + + using empty_storage = + ::STDEXEC::storage_for_completion_signatures<::STDEXEC::completion_signatures<>>; + empty_storage empty; + static_assert(noexcept(::STDEXEC::visit_stored_completion([]() noexcept {}, empty))); + } + + TEST_CASE("Stored completion visitation collapses empty storage to nullary invocation", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage first; + storage second; + second.arrive(::STDEXEC::set_value, 13); + + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + first); + CHECK(visited_empty); + + bool visited_empty_from_mixed = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty_from_mixed = true; }, + [](auto&&...) { FAIL("Unexpected completion"); }}, + second, + first); + CHECK(visited_empty_from_mixed); + } + + TEST_CASE("Stored completion visitation returns visitor results", + "[storage_for_completion_signatures]") + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>>; + + storage empty; + storage value; + value.arrive(::STDEXEC::set_value, 42); + + auto const classify = overloaded{ + []() noexcept -> int { return 0; }, + [](auto&& completion) noexcept -> int + { + if constexpr (completion_with_tag) + { + return std::get<0>(static_cast(completion).forward_arguments()); + } + else + { + return -1; + } + }}; + + static_assert( + std::is_same_v); + static_assert(noexcept(::STDEXEC::visit_stored_completion(classify, empty))); + CHECK(::STDEXEC::visit_stored_completion(classify, empty) == 0); + CHECK(::STDEXEC::visit_stored_completion(classify, value) == 42); + + static_assert( + std::is_same_v(classify, value)), long>); + static_assert(noexcept(::STDEXEC::visit_stored_completion(classify, value))); + CHECK(::STDEXEC::visit_stored_completion(classify, value) == 42L); + } + + TEST_CASE("Storing simple completion signatures and then completing a receiver therewith works", + "[storage_for_completion_signatures]") + { + using storage_type = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::exception_ptr)>>; + { + storage_type storage; + storage.arrive(::STDEXEC::set_value); + CHECK(std::move(storage).complete(expect_void_receiver{})); + } + { + std::optional storage(std::in_place); + storage->arrive(::STDEXEC::set_error, std::make_exception_ptr(std::logic_error("TEST"))); + std::exception_ptr ex; + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value() noexcept + { + FAIL("Unexpected value invocation"); + } + void set_stopped() noexcept + { + FAIL("Unexpected stopped invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept + { + // This ensures that the exception_ptr is moved onto the stack + CHECK(storage_); + storage_.reset(); + CHECK(!ex_); + ex_ = std::move(ex); + } + std::optional& storage_; + std::exception_ptr& ex_; + }; + CHECK(std::move(*storage).complete(receiver{storage, ex})); + REQUIRE(ex); + bool threw = false; + try + { + std::rethrow_exception(std::move(ex)); + } + catch (std::logic_error const & ex) + { + threw = true; + CHECK(ex.what() == std::string_view("TEST")); + } + CHECK(threw); + } + } + + TEST_CASE("When moving a stored value onto the stack throws complete reports std::exception_ptr", + "[storage_for_completion_signatures]") + { + struct throws_on_second_move + { + explicit throws_on_second_move(int& moves) noexcept + : moves_(&moves) + {} + throws_on_second_move(throws_on_second_move&& other) + { + moves_ = other.moves_; + ++*moves_; + if (*moves_ == 2) + { + throw std::runtime_error("Throwing as requested"); + } + } + int* moves_; + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(throws_on_second_move)>>; + + struct receiver + { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value(throws_on_second_move&&) noexcept + { + FAIL("Unexpected value invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept + { + CHECK(!ex_); + ex_ = std::move(ex); + } + std::exception_ptr& ex_; + }; + + int moves = 0; + storage s; + s.arrive(::STDEXEC::set_value, throws_on_second_move{moves}); + CHECK(moves == 1); + + std::exception_ptr ex; + CHECK(std::move(s).complete(receiver{ex})); + REQUIRE(ex); + bool threw = false; + try + { + std::rethrow_exception(std::move(ex)); + } + catch (std::runtime_error const & ex) + { + threw = true; + CHECK(ex.what() == std::string_view("Throwing as requested")); + } + CHECK(threw); + } + + TEST_CASE("When storing a completion signature would throw it is simply coalesced to " + "std::exception_ptr", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) + { + if (other.throws) + { + throw std::runtime_error("Throwing as requested"); + } + } + bool throws{false}; + }; + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + static_assert(storage::nothrow_arrive); + static_assert(set_equivalent< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move), + ::STDEXEC::set_error_t(std::exception_ptr)>, + storage::completion_signatures>); + { + storage s; + static_assert(noexcept(s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}))); + maybe_throws_on_move obj; + obj.throws = true; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + REQUIRE(std::get<0>(args)); + // TODO? + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + storage s; + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (!completion_with_tag) + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + // Important that the below cases don't add the std::exception_ptr completion + // since propagating a reference can't throw + { + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move&)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(std::is_same_v); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, obj); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(&obj == &std::get<0>(args)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + { + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move&&)>; + using storage = ::STDEXEC::storage_for_completion_signatures; + static_assert(std::is_same_v); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{ + [] { FAIL("Unexpected completion"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = static_cast(completion).forward_arguments(); + CHECK(&obj == &std::get<0>(args)); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + } + + TEST_CASE("Storage can propagate persistence exceptions instead of coalescing them", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) + { + if (other.throws) + { + throw std::runtime_error("Throwing as requested"); + } + other.moved = true; + } + bool throws{false}; + bool moved{false}; + }; + + using signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>; + using storage = ::STDEXEC::storage_for_completion_signatures< + signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert(!storage::nothrow_arrive); + static_assert(std::is_same_v); + static_assert(!noexcept( + std::declval().arrive(::STDEXEC::set_value, std::declval()))); + + storage s; + maybe_throws_on_move obj; + obj.throws = true; + CHECK_THROWS_AS(s.arrive(::STDEXEC::set_value, std::move(obj)), std::runtime_error); + CHECK(!obj.moved); + bool visited_empty = false; + ::STDEXEC::visit_stored_completion(overloaded{[&] { visited_empty = true; }, + [](auto&&) { FAIL("Unexpected completion"); }}, + std::move(s)); + CHECK(visited_empty); + } + + TEST_CASE("Propagating storage remains noexcept when persistence is noexcept", + "[storage_for_completion_signatures]") + { + using signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>; + using storage = ::STDEXEC::storage_for_completion_signatures< + signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert(storage::nothrow_arrive); + static_assert(std::is_same_v); + storage s; + static_assert(noexcept(s.arrive(::STDEXEC::set_value, 42))); + s.arrive(::STDEXEC::set_value, 42); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[]() noexcept { FAIL("Unexpected completion"); }, + [&](auto&& completion) noexcept + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + auto args = + static_cast(completion).forward_arguments(); + CHECK(std::get<0>(args) == 42); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(s)); + CHECK(invoked); + } + + TEST_CASE("Storage deduplicates exact duplicate completion signatures", + "[storage_for_completion_signatures]") + { + using duplicate_signatures = + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int)>; + using deduplicated_signatures = ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int)>; + + using internalizing_storage = + ::STDEXEC::storage_for_completion_signatures; + using propagating_storage = ::STDEXEC::storage_for_completion_signatures< + duplicate_signatures, + ::STDEXEC::storage_for_completion_signatures_error_policy::propagate>; + + static_assert( + std::is_same_v); + static_assert( + std::is_same_v); + + propagating_storage storage; + storage.arrive(::STDEXEC::set_value, 42); + + bool invoked = false; + ::STDEXEC::visit_stored_completion( + overloaded{[] { FAIL("Unexpected empty storage"); }, + [&](auto&& completion) + { + CHECK(!invoked); + invoked = true; + if constexpr (completion_with_tag) + { + CHECK(std::get<0>( + static_cast(completion).forward_arguments()) + == 42); + } + else + { + FAIL("Unexpected completion"); + } + }}, + std::move(storage)); + CHECK(invoked); + } + + TEST_CASE("Internalized storage errors are not external arrivals", + "[storage_for_completion_signatures]") + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&&) {} + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + + static_assert( + set_equivalent>); + static_assert(!can_arrive_set_error_with); + } + + TEST_CASE("Storage computes completion signatures through a consteval function", + "[storage_for_completion_signatures]") + { + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), ::STDEXEC::set_value_t(int)>>; + + static_assert(std::is_same_v); + } + { + struct maybe_throws_on_move + { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&&) {} + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(maybe_throws_on_move)>>; + + static_assert(set_equivalent); + } + } + + TEST_CASE("Storage rejects ambiguous or non-persistable completion signatures", + "[storage_for_completion_signatures]") + { +#if STDEXEC_NO_STDCPP_CONSTEXPR_EXCEPTIONS() + { + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(int), + ::STDEXEC::set_value_t(int&&)>>; + + static_assert(::STDEXEC::__merror); + } + { + struct not_move_constructible + { + not_move_constructible() = default; + not_move_constructible(not_move_constructible&&) = delete; + }; + + using storage = ::STDEXEC::storage_for_completion_signatures< + ::STDEXEC::completion_signatures<::STDEXEC::set_value_t(not_move_constructible)>>; + + static_assert(::STDEXEC::__merror); + } +#endif + } + +} // unnamed namespace From 6b3d3bcbf39e7c46a5029823089bea468ad3c5d7 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Fri, 26 Jun 2026 06:17:38 -0400 Subject: [PATCH 05/15] when_all: Reference Aware See P4288. --- include/stdexec/__detail/__when_all.hpp | 214 +++++++++++------- test/exec/test_fork_join.cpp | 7 +- test/stdexec/algos/adaptors/test_when_all.cpp | 164 +++++++++++++- 3 files changed, 300 insertions(+), 85 deletions(-) diff --git a/include/stdexec/__detail/__when_all.hpp b/include/stdexec/__detail/__when_all.hpp index 22358a3b7..daec0cdfb 100644 --- a/include/stdexec/__detail/__when_all.hpp +++ b/include/stdexec/__detail/__when_all.hpp @@ -37,6 +37,7 @@ import stdexec; # include "__optional.hpp" # include "__schedulers.hpp" # include "__senders.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__transform_completion_signatures.hpp" # include "__tuple.hpp" # include "__type_traits.hpp" @@ -48,6 +49,7 @@ import stdexec; # include "__atomic.hpp" # if !STDEXEC_USE_MODULES() # include +# include # endif # include "__prologue.hpp" @@ -108,14 +110,15 @@ namespace STDEXEC //! @code{.cpp} //! set_value_t(V1..., V2..., ..., Vn...) // concatenation of every input //! set_error_t(Eij)... // union across all inputs - //! set_error_t(std::exception_ptr) // added if any decay-copy may throw + //! set_error_t(std::exception_ptr) // added if storing a completion may throw //! set_stopped_t() // added if any input has it //! @endcode //! - //! The value datums of each input are decay-copied into the resulting - //! sender's state while it waits for the slowest input to finish; the - //! final tuple is built from those decay-copies. If any decay-copy - //! throws, the operation transitions to the error path. + //! Non-reference value datums are move-constructed into the resulting + //! sender's state while it waits for the slowest input to finish; reference + //! datums remain references. The final tuple is built from those stored + //! values. If storing a value throws, the operation transitions to the error + //! path. //! //! **Concurrency.** //! @@ -439,16 +442,29 @@ namespace STDEXEC __fn_t<_WITH_ENVIRONMENT_, _Env>...>; template - using __set_error_t = completion_signatures)>; + using __set_error_t = completion_signatures; template - using __nothrow_decay_copyable_results_t = - STDEXEC::__nothrow_decay_copyable_results_t<__completion_signatures_of_t<_Sender, _Env...>>; + using __value_signatures_t = decltype(__completion_signatures_of_t<_Sender, _Env...>{}.__select( + set_value)); + + template + using __value_storage_for_signatures_t = + storage_for_completion_signatures<_Signatures, + storage_for_completion_signatures_error_policy::propagate>; + + template + using __value_storage_t = + __value_storage_for_signatures_t<__value_signatures_t<_Sender, _Env...>>; + + template + using __nothrow_storable_values_t = + __mbool<__value_storage_t<_Sender, _Env...>::nothrow_arrive>; template inline constexpr bool __can_fail = !__never_sends || sends_stopped<_Sender, _Env> - || !__nothrow_decay_copyable_results_t<_Sender, _Env>::value; + || !__nothrow_storable_values_t<_Sender, _Env>::value; template inline constexpr bool __uses_stop_source = (__can_fail<_Senders, _Env> || ...); @@ -461,8 +477,8 @@ namespace STDEXEC { // TODO(ericniebler): check that all senders have a common completion domain template - using __all_nothrow_decay_copyable_results_t = - __mand<__nothrow_decay_copyable_results_t<_Senders, _Env...>...>; + using __all_nothrow_storable_values_t = + __mand<__nothrow_storable_values_t<_Senders, _Env...>...>; template using __value_tuple_t = __minvoke<__if_c<(0 == sizeof...(_Rest)), @@ -473,7 +489,7 @@ namespace STDEXEC template using __single_values_of_t = __value_types_t<__completion_signatures_of_t<_Sender, _Env...>, - __mtransform<__q<__decay_t>, __q<__mlist>>, + __qq<__mlist>, __mbind_front_q<__value_tuple_t, _Sender>>; template @@ -482,15 +498,43 @@ namespace STDEXEC __minvoke<__mconcat<__qf>, __single_values_of_t<_Senders>...>>; template - using __f = __minvoke_q< - __concat_completion_signatures_t, - __minvoke_q<__eptr_completion_unless_t, __all_nothrow_decay_copyable_results_t<_Senders...>>, - __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, _Senders...>, + using __nonvalue_input_sigs_t = __concat_completion_signatures_t< + __minvoke_q<__eptr_completion_unless_t, __all_nothrow_storable_values_t<_Senders...>>, __transform_reduce_completion_signatures_t<__completion_signatures_of_t<_Senders, _Env...>, __mconst>::__f, __set_error_t, completion_signatures, __concat_completion_signatures_t>...>; + + template + using __nonvalue_sigs_t = typename storage_for_completion_signatures< + __nonvalue_input_sigs_t<_Senders...>>::completion_signatures; + + template + using __f = __minvoke_q< + __concat_completion_signatures_t, + __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, _Senders...>, + __nonvalue_sigs_t<_Senders...>>; + + template + static consteval auto __get() + { + auto __value_completions = __concat_completion_signatures( + __value_storage_t<_Senders, _Env...>::get_completion_signatures()...); + STDEXEC_IF_OK(__value_completions) + { + using __nonvalue_storage_t = + storage_for_completion_signatures<__nonvalue_input_sigs_t<_Senders...>>; + auto __nonvalue_completions = __nonvalue_storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__nonvalue_completions) + { + return __concat_completion_signatures( + __minvoke<__mwith_default<__qq<__set_values_sig_t>, completion_signatures<>>, + _Senders...>{}, + __nonvalue_completions); + } + } + } }; template @@ -501,6 +545,12 @@ namespace STDEXEC { template using __f = __completions<>::template __f<_Senders...>; + + template + static consteval auto __get() + { + return __completions<>::template __get<_Senders...>(); + } }; template @@ -508,46 +558,53 @@ namespace STDEXEC { template using __f = __completions<__env_t<_Env, _Senders...>>::template __f<_Senders...>; + + template + static consteval auto __get() + { + return __completions<__env_t<_Env, _Senders...>>::template __get<_Senders...>(); + } }; template constexpr void __set_values(_Receiver& __rcvr, _ValuesTuple& __values) noexcept { + static_assert(__tuple_size_v<_ValuesTuple> != 0); STDEXEC::__apply( - [&](OptTuples&&... __opt_vals) noexcept -> void + [&](auto&... __value_storages) noexcept -> void { - STDEXEC::__cat_apply(__mk_completion_fn(set_value, __rcvr), - *static_cast(__opt_vals)...); + STDEXEC::visit_stored_completion( + STDEXEC::__overload{[]() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto& __completion, auto&... __completions) noexcept -> void + { + STDEXEC::__cat_apply( + __mk_completion_fn(set_value, __rcvr), + std::move(__completion).__forward_arguments(), + std::move(__completions).__forward_arguments()...); + }}, + __value_storages...); }, - static_cast<_ValuesTuple&&>(__values)); + __values); } - template - using __values_opt_tuple_t = value_types_of_t<_Sender, _ChildEnv, __decayed_tuple, __optional>; - template requires(__max1_sender<_Senders, __env_t<_Env, _Senders...>> && ...) struct __traits { using __child_env = __env_t<_Env, _Senders...>; - // tuple>, optional>, ...> - using __values_tuple = - __minvoke<__mwith_default< - __mtransform<__mbind_front_q<__values_opt_tuple_t, __child_env>, __q<__tuple>>, - __ignore>, - _Senders...>; - - using __collect_errors = __mbind_front_q<__mset_insert, __mset<>>; + // tuple + using __values_tuple = __minvoke< + __mwith_default<__mtransform<__mbind_back_q<__value_storage_t, __child_env>, __q<__tuple>>, + __ignore>, + _Senders...>; - using __errors_list = - __minvoke<__mconcat<>, - __if<__mand<__nothrow_decay_copyable_results_t<_Senders, __child_env>...>, - __mlist<>, - __mlist>, - __error_types_of_t<_Senders, __child_env, __q<__mlist>>...>; - - using __errors_variant = __mapply<__q<__uniqued_variant>, __errors_list>; + using __completion_storage = storage_for_completion_signatures< + typename __completions<__child_env>::template __nonvalue_input_sigs_t<_Senders...>>; static constexpr bool __uses_stop_source = __when_all::__uses_stop_source<_Env, _Senders...>; }; @@ -582,7 +639,7 @@ namespace STDEXEC _State* __state_; }; - template (__rcvr_)); + __completion_.arrive(set_stopped); + static_cast<_CompletionStorage&&>(__completion_) + .complete(static_cast<_Receiver&&>(__rcvr_)); break; } // This is reachable because the stop callback sets __stopped whether @@ -632,11 +691,12 @@ namespace STDEXEC } break; case __error: - if constexpr (!__same_as<_ErrorsVariant, __variant<>>) + if constexpr (!__same_as>) { // One or more child operations completed with an error: - STDEXEC::__visit(__mk_completion_fn(set_error, __rcvr_), - static_cast<_ErrorsVariant&&>(__errors_)); + static_cast<_CompletionStorage&&>(__completion_) + .complete(static_cast<_Receiver&&>(__rcvr_)); } break; default:; @@ -650,7 +710,8 @@ namespace STDEXEC __stop_source_t __stop_source_{}; // Could be non-atomic here and atomic_ref everywhere except __completion_fn __std::atomic<__state_t> __state_{__started}; - _ErrorsVariant __errors_{__no_init}; + STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS + _CompletionStorage __completion_{}; STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS _ValuesTuple __values_{}; STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS @@ -708,15 +769,15 @@ namespace STDEXEC return [&](__ignore, __ignore, _Child&&...) noexcept requires(__max1_sender<_Child, __env_t, _Child...>> && ...) { - using _Traits = __traits, _Child...>; - using _ErrorsVariant = _Traits::__errors_variant; - using _ValuesTuple = _Traits::__values_tuple; - using _ChildEnv = _Traits::__child_env; - using _State = __state<_ErrorsVariant, - _ValuesTuple, - _Receiver, - (sends_stopped<_Child, _ChildEnv> || ...), - _Traits::__uses_stop_source>; + using _Traits = __traits, _Child...>; + using _CompletionStorage = _Traits::__completion_storage; + using _ValuesTuple = _Traits::__values_tuple; + using _ChildEnv = _Traits::__child_env; + using _State = __state<_CompletionStorage, + _ValuesTuple, + _Receiver, + (sends_stopped<_Child, _ChildEnv> || ...), + _Traits::__uses_stop_source>; return _State{static_cast<_Receiver&&>(__rcvr), sizeof...(_Child)}; }; } @@ -729,6 +790,18 @@ namespace STDEXEC template using __completions_t = __children_of<_Self, __when_all::__completions_for<_Env...>>; + template + struct __get_completions; + + template + struct __get_completions<_Completions, __mlist<_Children...>> + { + static consteval auto __f() + { + return _Completions::template __get<_Children...>(); + } + }; + static constexpr auto __get_attrs = [](__ignore, __ignore, _Child const &...) noexcept { @@ -741,8 +814,9 @@ namespace STDEXEC static_assert(__sender_for<_Self, when_all_t>); if constexpr (__minvocable_q<__completions_t, _Self, _Env...>) { - // TODO: update this to use constant evaluation: - return __completions_t<_Self, _Env...>{}; + using __completions = __when_all::__completions_for<_Env...>; + using __children = __children_of<_Self>; + return __get_completions<__completions, __children>::__f(); } else if constexpr (sizeof...(_Env) == 0) { @@ -805,21 +879,7 @@ namespace STDEXEC case __stopped: // We are the first child to complete with an error, so we must save the error. (Any // subsequent errors are ignored.) - if constexpr (__nothrow_decay_copyable<_Error>) - { - __state.__errors_.template emplace<__decay_t<_Error>>(static_cast<_Error&&>(__err)); - } - else - { - STDEXEC_TRY - { - __state.__errors_.template emplace<__decay_t<_Error>>(static_cast<_Error&&>(__err)); - } - STDEXEC_CATCH_ALL - { - __state.__errors_.template emplace(std::current_exception()); - } - } + __state.__completion_.arrive(set_error, static_cast<_Error&&>(__err)); break; case __error:; // We're already in the "error" state. Ignore the error. } @@ -850,20 +910,16 @@ namespace STDEXEC } else if constexpr (!__same_as<_ValuesTuple, __ignore>) { - auto& __opt_values = STDEXEC::__get<_Index::value>(__state.__values_); - using _Tuple = __decayed_tuple<_Args...>; - static_assert(__same_as, - "One of the senders in this when_all() is fibbing about what types it " - "sends"); - if constexpr ((__nothrow_decay_copyable<_Args> && ...)) + auto& __value = STDEXEC::__get<_Index::value>(__state.__values_); + if constexpr (noexcept(__value.arrive(set_value, static_cast<_Args&&>(__args)...))) { - __opt_values.emplace(_Tuple{static_cast<_Args&&>(__args)...}); + __value.arrive(set_value, static_cast<_Args&&>(__args)...); } else { STDEXEC_TRY { - __opt_values.emplace(_Tuple{static_cast<_Args&&>(__args)...}); + __value.arrive(set_value, static_cast<_Args&&>(__args)...); } STDEXEC_CATCH_ALL { diff --git a/test/exec/test_fork_join.cpp b/test/exec/test_fork_join.cpp index 5f1635242..71e7b99dd 100644 --- a/test/exec/test_fork_join.cpp +++ b/test/exec/test_fork_join.cpp @@ -104,9 +104,10 @@ namespace return (is + ...); })); STATIC_REQUIRE(sender_in>); - STATIC_REQUIRE(set_equivalent< - completion_signatures_of_t>, - completion_signatures>); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); auto [i1, i2] = sync_wait(sndr).value(); CHECK(i1 == 42); diff --git a/test/stdexec/algos/adaptors/test_when_all.cpp b/test/stdexec/algos/adaptors/test_when_all.cpp index 202d0a702..1f851f34e 100644 --- a/test/stdexec/algos/adaptors/test_when_all.cpp +++ b/test/stdexec/algos/adaptors/test_when_all.cpp @@ -68,6 +68,93 @@ namespace } }; + struct error_that_throws_when_persisted + { + error_that_throws_when_persisted() = default; + + error_that_throws_when_persisted(error_that_throws_when_persisted &&) + { + throw std::runtime_error("error persistence failed"); + } + }; + + struct throws_when_error_is_persisted_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + using operation_state_concept = ex::operation_state_tag; + + Receiver receiver_; + + void start() & noexcept + { + ex::set_error(std::move(receiver_), error_that_throws_when_persisted{}); + } + }; + + template + auto connect(Receiver receiver) const -> operation + { + return {std::move(receiver)}; + } + }; + + template + struct advertises_value_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + }; + + struct not_move_constructible + { + not_move_constructible() = default; + not_move_constructible(not_move_constructible &&) = delete; + not_move_constructible(not_move_constructible const &) = delete; + }; + + struct expect_rvalue_value_receiver + { + using receiver_concept = ex::receiver_t; + + void set_value(int &&i, double &&d) && noexcept + { + CHECK(i == 3); + CHECK(d == 0.1415); + } + + template + void set_error(Error &&) && noexcept + { + FAIL("unexpected error"); + } + + void set_stopped() && noexcept + { + FAIL("unexpected stopped"); + } + + auto get_env() const noexcept -> ex::env<> + { + return {}; + } + }; + TEST_CASE("when_all returns a sender", "[adaptors][when_all]") { auto snd = ex::when_all(ex::just(3), ex::just(0.1415)); @@ -120,6 +207,15 @@ namespace ex::start(op); } + TEST_CASE("when_all sends by-value completions as rvalues", "[adaptors][when_all]") + { + auto snd = ex::when_all(ex::just(3), ex::just(0.1415)); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto op = ex::connect(std::move(snd), expect_rvalue_value_receiver{}); + ex::start(op); + } + TEST_CASE("when_all returning two values can we waited on", "[adaptors][when_all]") { ex::sender auto snd = ex::when_all(ex::just(2), ex::just(3)); @@ -325,7 +421,7 @@ namespace CHECK(cancelled); } - TEST_CASE("when_all has the values_type based on the children, decayed and as rvalue references", + TEST_CASE("when_all has the values_type based on the children, preserving references", "[adaptors][when_all]") { check_val_types>>(ex::when_all(ex::just(13))); @@ -342,11 +438,35 @@ namespace check_val_types>>( ex::when_all(ex::just(3), ex::just(), ex::just(0.14))); - // if children send references, they get decayed - check_val_types>>( + // unary when_all coalesces to the child, so references are preserved + check_val_types>>(ex::when_all(exec::split(ex::just(3)))); + + // if multiple children send references, they are preserved + check_val_types>>( ex::when_all(exec::split(ex::just(3)), exec::split(ex::just(0.14)))); } + TEST_CASE("when_all validates every value completion storage", "[adaptors][when_all]") + { +#if STDEXEC_NO_STDCPP_CONSTEXPR_EXCEPTIONS() + using invalid_sender = + decltype(ex::__make_sexpr(ex::__{}, + advertises_value_sender{}, + ex::just())); + using invalid_completions = + decltype(invalid_sender::template get_completion_signatures>()); + STATIC_REQUIRE(ex::__merror); +#endif + + using reference_sender = + decltype(ex::when_all(advertises_value_sender{}, ex::just())); + using reference_completions = + decltype(ex::get_completion_signatures>()); + STATIC_REQUIRE( + set_equivalent>); + } + TEST_CASE("when_all has the error_types based on the children", "[adaptors][when_all]") { check_err_types>(ex::when_all(ex::just_error(13))); @@ -362,6 +482,44 @@ namespace ex::when_all(ex::just(13), ex::just_error(std::exception_ptr{}), ex::just_stopped())); } + TEST_CASE("when_all reports exception_ptr if persisting an error throws", "[adaptors][when_all]") + { + auto snd = ex::when_all(ex::just(), throws_when_error_is_persisted_sender{}); + STATIC_REQUIRE(exec::sender_for); + check_err_types>(snd); + + std::exception_ptr ex; + struct receiver + { + using receiver_concept = ex::receiver_t; + + void set_value() noexcept + { + FAIL("Unexpected value completion"); + } + + void set_stopped() noexcept + { + FAIL("Unexpected stopped completion"); + } + + void set_error(error_that_throws_when_persisted &&) noexcept + { + FAIL("Unexpected original error completion"); + } + + void set_error(std::exception_ptr err) noexcept + { + *ex_ = std::move(err); + } + + std::exception_ptr *ex_; + }; + auto op = ex::connect(std::move(snd), receiver{&ex}); + ex::start(op); + CHECK(ex); + } + TEST_CASE("when_all has sends_stopped == true if and only if at least one child sends stopped", "[adaptors][when_all]") { From b5ae50deecab5d095250cd8d31ea37999e0633d5 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 27 Jun 2026 04:43:59 -0400 Subject: [PATCH 06/15] as_awaitable: Reference Aware See P4288. --- include/stdexec/__detail/__as_awaitable.hpp | 191 ++++++++++++++------ test/stdexec/types/test_task.cpp | 163 +++++++++++++++++ 2 files changed, 301 insertions(+), 53 deletions(-) diff --git a/include/stdexec/__detail/__as_awaitable.hpp b/include/stdexec/__detail/__as_awaitable.hpp index 304118b11..6a3254381 100644 --- a/include/stdexec/__detail/__as_awaitable.hpp +++ b/include/stdexec/__detail/__as_awaitable.hpp @@ -32,17 +32,21 @@ import stdexec; # include "__completion_signatures_of.hpp" # include "__concepts.hpp" # include "__connect.hpp" +# include "__manual_lifetime.hpp" # include "__meta.hpp" # include "__queries.hpp" +# include "__scope.hpp" # include "__spin_loop_pause.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__type_traits.hpp" -# include "__variant.hpp" # if !STDEXEC_USE_MODULES() # include # include // for std::identity # include # include +# include +# include # endif # include "__prologue.hpp" @@ -58,8 +62,11 @@ namespace STDEXEC namespace __as_awaitable { + template + using __value_tuple_t = std::tuple<_Values...>; + template - extern __q<__decayed_std_tuple> const __as_single; + extern __q<__value_tuple_t> const __as_single; template <> inline constexpr __q<__midentity> __as_single<1>; @@ -71,8 +78,8 @@ namespace STDEXEC using __single_value_t = __minvoke), _Values...>; template - using __value_t = __decay_t< - __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value_t>, __msingle_or>>; + using __value_t = + __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value_t>, __msingle_or>; inline constexpr auto __get_await_completion_adaptor = __with_default{get_await_completion_adaptor, std::identity{}}; @@ -91,14 +98,46 @@ namespace STDEXEC using __adapted_sender_t = __remove_rvalue_reference_t<__call_result_t<__adapt_completion_t<_Sender>, _Sender>>; - struct __void - {}; + template + using __set_value_sig_t = completion_signatures; - template - using __value_or_void_t = __if_c<__same_as<_Value, void>, __void, _Value>; + template + using __value_signatures_t = + __value_types_of_t<_Sender, + env_of_t<_Promise&>, + __q<__set_value_sig_t>, + __msingle_or>>; + + template + using __expected_t = storage_for_completion_signatures< + __concat_completion_signatures_t<__value_signatures_t<_Sender, _Promise>, + completion_signatures>>; template - using __expected_t = __variant<__value_or_void_t<_Value>, std::exception_ptr>; + struct __await_result + { + template + constexpr auto operator()(_Arg&& __arg) const -> _Value + { + return static_cast<_Arg&&>(__arg); + } + }; + + template <> + struct __await_result + { + constexpr void operator()() const noexcept {} + }; + + template + struct __await_result> + { + template + constexpr auto operator()(_Args&&... __args) const -> std::tuple<_Values...> + { + return std::tuple<_Values...>{static_cast<_Args&&>(__args)...}; + } + }; using __connect_await::__has_as_awaitable_member; @@ -111,11 +150,11 @@ namespace STDEXEC && __completes_inline_for && __completes_inline_for; - template + template struct __sender_awaiter_base; - template - struct __sender_awaiter_base<_Value, true> + template + struct __sender_awaiter_base<_Sender, _Promise, _Value, true> { static constexpr auto await_ready() noexcept -> bool { @@ -124,27 +163,65 @@ namespace STDEXEC constexpr auto await_resume() -> _Value { - // If the operation completed with set_stopped (as denoted by the result variant - // being valueless), we should not be resuming this coroutine at all. - STDEXEC_ASSERT(!__result_.__is_valueless()); - if (__result_.index() == 1) - { - // The operation completed with set_error, so we need to rethrow the exception. - std::rethrow_exception(std::move(__var::__get<1>(__result_))); - } + __manual_lifetime<_Value> __value; + bool const __has_result = visit_stored_completion( + __overload{ + []() -> bool + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto&& __completion) -> bool + { + using __completion_t = std::remove_cvref_t; + auto __args = static_cast(__completion).forward_arguments(); + if constexpr (__same_as) + { + static_assert(std::tuple_size_v == 1); + std::apply([](auto&& __err) -> void + { std::rethrow_exception(static_cast(__err)); }, + static_cast(__args)); + } + else + { + std::apply( + [&](auto&&... __as) -> void + { + __value.__construct_from(__await_result<_Value>{}, + static_cast(__as)...); + }, + static_cast(__args)); + } + return true; + }}, + static_cast<__expected_t<_Sender, _Promise>&&>(__result_)); + + // If the operation completed with set_stopped, we should not be resuming this coroutine at all. + STDEXEC_ASSERT(__has_result); // The operation completed with set_value, so we can just return the value, which // may be void. - using __reference_t = std::add_rvalue_reference_t<_Value>; - return static_cast<__reference_t>(__var::__get<0>(__result_)); + if constexpr (__same_as<_Value, void>) + { + return; + } + else if constexpr (std::is_reference_v<_Value>) + { + return __value.__get(); + } + else + { + auto __guard = __scope_guard{[&]() noexcept { __value.__destroy(); }}; + return static_cast<_Value&&>(__value.__get()); + } } [[nodiscard]] constexpr auto __get_continuation() const noexcept -> __std::coroutine_handle<> { - // If the operation was stopped (__result_ is valueless), we should use the + // If the operation was stopped (__result_ is empty), we should use the // unhandled_stopped() continuation. Otherwise, should resume the __continuation_ // as normal. - if (__result_.__is_valueless()) + if (!__result_.has_completion()) { return STDEXEC::__coroutine_unhandled_stopped(__continuation_); } @@ -154,14 +231,15 @@ namespace STDEXEC } } - __coroutine_handle<> __continuation_; - __expected_t<_Value> __result_{__no_init}; + __coroutine_handle<> __continuation_; + __expected_t<_Sender, _Promise> __result_{}; }; // When the sender is not statically known to complete inline, we need to use atomic // state to guard against too many inline completions causing a stack overflow. - template - struct __sender_awaiter_base<_Value, false> : __sender_awaiter_base<_Value, true> + template + struct __sender_awaiter_base<_Sender, _Promise, _Value, false> + : __sender_awaiter_base<_Sender, _Promise, _Value, true> { // This is used to coordinate between await_suspend (T1) and the receiver (T2). // It can hold three different values: @@ -183,7 +261,7 @@ namespace STDEXEC __std::atomic __thread_id_{std::this_thread::get_id()}; }; - template + template struct __receiver_base { using receiver_concept = receiver_tag; @@ -193,11 +271,11 @@ namespace STDEXEC { STDEXEC_TRY { - __awaiter_.__result_.template emplace<0>(static_cast<_Us&&>(__us)...); + __awaiter_.__result_.arrive(set_value_t{}, static_cast<_Us&&>(__us)...); } STDEXEC_CATCH_ALL { - __awaiter_.__result_.template emplace<1>(std::current_exception()); + __awaiter_.__result_.arrive(set_error_t{}, std::current_exception()); } } @@ -205,25 +283,31 @@ namespace STDEXEC void set_error(_Error&& __err) noexcept { if constexpr (__decays_to<_Error, std::exception_ptr>) - __awaiter_.__result_.template emplace<1>(static_cast<_Error&&>(__err)); + { + __awaiter_.__result_.arrive(set_error_t{}, static_cast<_Error&&>(__err)); + } else if constexpr (__decays_to<_Error, std::error_code>) - __awaiter_.__result_.template emplace<1>( - std::make_exception_ptr(std::system_error(__err))); + { + __awaiter_.__result_.arrive(set_error_t{}, + std::make_exception_ptr(std::system_error(__err))); + } else - __awaiter_.__result_.template emplace<1>( - std::make_exception_ptr(static_cast<_Error&&>(__err))); + { + __awaiter_.__result_.arrive(set_error_t{}, + std::make_exception_ptr(static_cast<_Error&&>(__err))); + } } - __sender_awaiter_base<_Value, true>& __awaiter_; + __sender_awaiter_base<_Sender, _Promise, _Value, true>& __awaiter_; }; - template - struct __sync_receiver : __receiver_base<_Value> + template + struct __sync_receiver : __receiver_base<_Sender, _Promise, _Value> { - using __awaiter_t = __sender_awaiter_base<_Value, true>; + using __awaiter_t = __sender_awaiter_base<_Sender, _Promise, _Value, true>; constexpr explicit __sync_receiver(__awaiter_t& __awaiter) noexcept - : __receiver_base<_Value>{__awaiter} + : __receiver_base<_Sender, _Promise, _Value>{__awaiter} {} void set_stopped() noexcept @@ -243,26 +327,26 @@ namespace STDEXEC }; // The receiver type used to connect to senders that could complete asynchronously. - template - struct __async_receiver : __sync_receiver<_Promise, _Value> + template + struct __async_receiver : __sync_receiver<_Sender, _Promise, _Value> { - using __awaiter_t = __sender_awaiter_base<_Value, false>; + using __awaiter_t = __sender_awaiter_base<_Sender, _Promise, _Value, false>; constexpr explicit __async_receiver(__awaiter_t& __awaiter) noexcept - : __sync_receiver<_Promise, _Value>{__awaiter} + : __sync_receiver<_Sender, _Promise, _Value>{__awaiter} {} template void set_value(_Us&&... __us) noexcept { - this->__sync_receiver<_Promise, _Value>::set_value(static_cast<_Us&&>(__us)...); + this->__sync_receiver<_Sender, _Promise, _Value>::set_value(static_cast<_Us&&>(__us)...); __done(); } template void set_error(_Error&& __err) noexcept { - this->__sync_receiver<_Promise, _Value>::set_error(static_cast<_Error&&>(__err)); + this->__sync_receiver<_Sender, _Promise, _Value>::set_error(static_cast<_Error&&>(__err)); __done(); } @@ -306,23 +390,24 @@ namespace STDEXEC }; template - using __sync_receiver_t = __sync_receiver<_Promise, __value_t<_Sender, _Promise>>; + using __sync_receiver_t = __sync_receiver<_Sender, _Promise, __value_t<_Sender, _Promise>>; template - using __async_receiver_t = __async_receiver<_Promise, __value_t<_Sender, _Promise>>; + using __async_receiver_t = __async_receiver<_Sender, _Promise, __value_t<_Sender, _Promise>>; ////////////////////////////////////////////////////////////////////////////////////// // __sender_awaiter: awaitable type returned by as_awaitable when given a sender // that does not have an as_awaitable member function template > _Sender> - struct __sender_awaiter : __sender_awaiter_base<__value_t<_Sender, _Promise>, false> + struct __sender_awaiter + : __sender_awaiter_base<_Sender, _Promise, __value_t<_Sender, _Promise>, false> { using __value_t = __as_awaitable::__value_t<_Sender, _Promise>; constexpr explicit __sender_awaiter(_Sender&& __sndr, __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_connectable<_Sender, __receiver_t>) - : __sender_awaiter_base<__value_t, false>{__hcoro} + : __sender_awaiter_base<_Sender, _Promise, __value_t, false>{__hcoro} , __opstate_(STDEXEC::connect(static_cast<_Sender&&>(__sndr), __receiver_t(*this))) {} @@ -365,14 +450,14 @@ namespace STDEXEC template > _Sender> requires __completes_inline<_Sender, env_of_t<_Promise&>> struct __sender_awaiter<_Promise, _Sender> - : __sender_awaiter_base<__value_t<_Sender, _Promise>, true> + : __sender_awaiter_base<_Sender, _Promise, __value_t<_Sender, _Promise>, true> { using __value_t = __as_awaitable::__value_t<_Sender, _Promise>; constexpr explicit __sender_awaiter(_Sender&& __sndr, __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_move_constructible<_Sender>) - : __sender_awaiter_base<__value_t, true>{__hcoro} + : __sender_awaiter_base<_Sender, _Promise, __value_t, true>{__hcoro} , __sndr_(static_cast<_Sender&&>(__sndr)) {} diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 5fb0b4e3d..811b444b8 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -26,6 +26,8 @@ # include # include +# include +# include # include @@ -330,9 +332,138 @@ namespace } constinit int global_int = 0; + constinit double global_double = 0.0; constexpr auto wrap_ref = ex::then([](auto &i) noexcept { return std::ref(i); }); + template + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + if constexpr (Inline) + { + return ex::__completion_behavior::__inline_completion; + } + else + { + return ex::__completion_behavior::__asynchronous_affine; + } + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int *value_; + }; + + template + struct references_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * int_value_; + double * double_value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), int_value_, double_value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + if constexpr (Inline) + { + return ex::__completion_behavior::__inline_completion; + } + else + { + return ex::__completion_behavior::__asynchronous_affine; + } + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int * int_value_; + double * double_value_; + }; + + template + auto task_awaits_reference_sender() -> ex::task + { + decltype(auto) i = co_await reference_sender{&global_int}; + STATIC_REQUIRE(std::same_as); + CHECK(&i == &global_int); + co_return i; + } + + template + auto task_awaits_references_sender() -> ex::task + { + decltype(auto) values = co_await references_sender{&global_int, &global_double}; + STATIC_REQUIRE(std::same_as>); + CHECK(&std::get<0>(values) == &global_int); + CHECK(&std::get<1>(values) == &global_double); + } + auto test_task_of_reference_type() -> ex::task { int &i = co_await []() -> ex::task @@ -365,6 +496,38 @@ namespace CHECK(i == 42); } + TEST_CASE("task co_await preserves a single reference from inline sender", + "[types][task]") + { + global_int = 42; + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + CHECK(i == 42); + } + + TEST_CASE("task co_await preserves a single reference from async sender", + "[types][task]") + { + global_int = 42; + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + CHECK(i == 42); + } + + TEST_CASE("task co_await preserves reference tuple from inline sender", + "[types][task]") + { + global_int = 42; + global_double = 0.125; + ex::sync_wait(task_awaits_references_sender()); + } + + TEST_CASE("task co_await preserves reference tuple from async sender", + "[types][task]") + { + global_int = 42; + global_double = 0.125; + ex::sync_wait(task_awaits_references_sender()); + } + struct inline_affine_stopped_sender { using sender_concept = ex::sender_tag; From 3aaf2f500d30d80c9aa253f73b77323b173a6c5e Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 27 Jun 2026 05:16:10 -0400 Subject: [PATCH 07/15] finally: Reference Aware In addition to adding reference awareness also fixes (and brings under test) a previous defect: If decay-copying the result datums threw the successor operation was previously skipped. --- include/stdexec/__detail/__finally.hpp | 55 +++--- test/stdexec/algos/adaptors/test_finally.cpp | 171 +++++++++++++++++++ test/stdexec/types/test_task.cpp | 60 +++++++ 3 files changed, 254 insertions(+), 32 deletions(-) diff --git a/include/stdexec/__detail/__finally.hpp b/include/stdexec/__detail/__finally.hpp index 994bcb2c4..1c46ea53f 100644 --- a/include/stdexec/__detail/__finally.hpp +++ b/include/stdexec/__detail/__finally.hpp @@ -24,9 +24,8 @@ #include "__schedulers.hpp" #include "__sender_adaptor_closure.hpp" #include "__senders.hpp" -#include "__storage.hpp" +#include "__storage_for_completion_signatures.hpp" #include "__transform_completion_signatures.hpp" -#include "__tuple.hpp" #include "__utility.hpp" #include "__prologue.hpp" @@ -77,14 +76,15 @@ namespace STDEXEC struct __result_variant_fn { template - using __f = __storage_for_t<_InitialSender, env_of_t<_Receiver>>; + using __f = storage_for_completion_signatures< + __completion_signatures_of_t<_InitialSender, env_of_t<_Receiver>>>; }; template <> struct __result_variant_fn { template - using __f = __results_storage<>; + using __f = storage_for_completion_signatures>; }; // If the final sender has no value completions, then we don't need to store the @@ -101,7 +101,7 @@ namespace STDEXEC _Receiver __rcvr_{}; STDEXEC_ATTRIBUTE(no_unique_address) _Env2 const __env2_{}; - _Storage __result_; // __variant<__tuple, ...> + _Storage __result_; }; using __mk_secondary_env_t = @@ -122,7 +122,7 @@ namespace STDEXEC constexpr void set_value() noexcept { - std::move(__opstate_->__result_).__complete(__opstate_->__rcvr_); + std::move(__opstate_->__result_).complete(static_cast<_Receiver&&>(__opstate_->__rcvr_)); } template @@ -177,21 +177,9 @@ namespace STDEXEC } else { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Args...>; - this->__result_.template emplace<__tuple_t>(static_cast<_Args&&>(__args)...); - (*__cleanup_callback_)(this); - STDEXEC::start(this->__final_opstate_); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_Args...>) - { - (*__cleanup_callback_)(this); - STDEXEC::set_error(static_cast<_Receiver&&>(this->__rcvr_), std::current_exception()); - } - } + this->__result_.arrive(static_cast<_Args&&>(__args)...); + (*__cleanup_callback_)(this); + STDEXEC::start(this->__final_opstate_); } } @@ -301,8 +289,7 @@ namespace STDEXEC { // If the finally sender doesn't have set_value completions, then we // don't need to worry about the initial sender's value types not being - // nothrow decay-copyable, because they won't be propagated to the - // receiver. + // storable, because they won't be propagated to the receiver. return STDEXEC::__transform_completion_signatures(__initial_completions, __ignore_completion(), {}, @@ -323,15 +310,19 @@ namespace STDEXEC } else { - // The finally sender's completion signatures are ... - return STDEXEC::__concat_completion_signatures( - // ... the initial sender's completions with value types decayed ... - STDEXEC::__transform_completion_signatures( - __initial_completions, - __decay_arguments()), - // ... and the final sender's error and stopped completions ... - STDEXEC::__transform_completion_signatures(__final_completions, - __ignore_completion())); + using __initial_storage_t = + storage_for_completion_signatures; + auto __stored_initial_completions = __initial_storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_initial_completions) + { + // The finally sender's completion signatures are ... + return STDEXEC::__concat_completion_signatures( + // ... the initial sender's completions as stored for replay ... + __stored_initial_completions, + // ... and the final sender's error and stopped completions ... + STDEXEC::__transform_completion_signatures(__final_completions, + __ignore_completion())); + } } } } diff --git a/test/stdexec/algos/adaptors/test_finally.cpp b/test/stdexec/algos/adaptors/test_finally.cpp index 05b67bf45..6d9b8c1cd 100644 --- a/test/stdexec/algos/adaptors/test_finally.cpp +++ b/test/stdexec/algos/adaptors/test_finally.cpp @@ -19,10 +19,121 @@ #include +#include + using namespace STDEXEC; namespace { + constinit int global_int = 0; + constinit double global_double = 0.0; + + struct reference_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + int* value_; + }; + + struct references_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* int_value_; + double* double_value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), int_value_, double_value_}; + } + + int* int_value_; + double* double_value_; + }; + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct throws_on_move + { + throws_on_move() = default; + throws_on_move(throws_on_move const &) = delete; + throws_on_move(throws_on_move&&) + { + throw std::runtime_error("Throwing as requested"); + } + }; + + struct throwing_value_sender + { + using sender_concept = sender_tag; + + template + static consteval auto get_completion_signatures() + { + return completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + throws_on_move* value_; + + void start() & noexcept + { + set_value(static_cast(receiver_), std::move(*value_)); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + throws_on_move* value_; + }; +#endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("finally is a sender", "[adaptors][finally]") { @@ -65,7 +176,67 @@ namespace CHECK(i == 42); } + TEST_CASE("finally preserves a single reference completion", "[adaptors][finally]") + { + bool called = false; + global_int = 42; + auto raw = exec::finally(reference_sender{&global_int}, + just() | then([&called]() noexcept { called = true; })); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + auto s = std::move(raw) + | then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + + sync_wait(s); + CHECK(called); + } + + TEST_CASE("finally preserves multiple reference completion arguments", "[adaptors][finally]") + { + bool called = false; + global_int = 42; + global_double = 0.125; + auto raw = exec::finally(references_sender{&global_int, &global_double}, + just() | then([&called]() noexcept { called = true; })); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + auto s = std::move(raw) + | then( + [](auto&& i, auto&& d) noexcept + { + CHECK(&i == &global_int); + CHECK(&d == &global_double); + CHECK(i == 42); + CHECK(d == 0.125); + }); + STATIC_REQUIRE(set_equivalent>, + completion_signatures>); + + sync_wait(s); + CHECK(called); + } + #if !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("finally executes the final action when storing the initial completion throws", + "[adaptors][finally]") + { + bool called = false; + throws_on_move value; + + auto s = exec::finally(throwing_value_sender{&value}, + just() | then([&called]() noexcept { called = true; })); + + CHECK_THROWS_AS(sync_wait(s), std::runtime_error); + CHECK(called); + } + TEST_CASE("finally does not execute the final action and throws integer", "[adaptors][finally]") { bool called = false; diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 811b444b8..00dfe21cc 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -446,6 +446,52 @@ namespace double * double_value_; }; + struct non_affine_reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int * value_; + + void start() & noexcept + { + ex::set_value(std::move(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {std::move(receiver), value_}; + } + + struct attrs + { + [[nodiscard]] + static constexpr auto query(ex::__get_completion_behavior_t) noexcept + { + return ex::__completion_behavior::__asynchronous; + } + }; + + [[nodiscard]] + auto get_env() const noexcept -> attrs + { + return {}; + } + + int *value_; + }; + template auto task_awaits_reference_sender() -> ex::task { @@ -464,6 +510,13 @@ namespace CHECK(&std::get<1>(values) == &global_double); } + auto task_awaits_non_affine_reference_sender() -> ex::task + { + auto &&i = co_await non_affine_reference_sender{&global_int}; + CHECK(&i == &global_int); + CHECK(i == 42); + } + auto test_task_of_reference_type() -> ex::task { int &i = co_await []() -> ex::task @@ -528,6 +581,13 @@ namespace ex::sync_wait(task_awaits_references_sender()); } + TEST_CASE("task co_await preserves a reference when affine inserts finally", + "[types][task]") + { + global_int = 42; + ex::sync_wait(task_awaits_non_affine_reference_sender()); + } + struct inline_affine_stopped_sender { using sender_concept = ex::sender_tag; From b04f8c04572c63fa5f43e27131f08450842712e8 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 27 Jun 2026 06:09:41 -0400 Subject: [PATCH 08/15] continues_on: Reference Aware See P4288. Also brings the implementation into alignment with the standard: The standard specifies that if storage the value result datums throws the schedule operation is started whereas the previous implementation delivered such a failure inline. --- include/stdexec/__detail/__continues_on.hpp | 122 ++---------- .../algos/adaptors/test_continues_on.cpp | 186 ++++++++++++++++++ test/stdexec/algos/adaptors/test_on.cpp | 85 ++++++++ 3 files changed, 290 insertions(+), 103 deletions(-) diff --git a/include/stdexec/__detail/__continues_on.hpp b/include/stdexec/__detail/__continues_on.hpp index d9fb2866b..1cc5e8311 100644 --- a/include/stdexec/__detail/__continues_on.hpp +++ b/include/stdexec/__detail/__continues_on.hpp @@ -27,9 +27,8 @@ #include "__schedulers.hpp" #include "__sender_adaptor_closure.hpp" #include "__senders.hpp" -#include "__storage.hpp" +#include "__storage_for_completion_signatures.hpp" #include "__transform_completion_signatures.hpp" -#include "__tuple.hpp" #include "__utility.hpp" #include "__prologue.hpp" @@ -40,32 +39,14 @@ namespace STDEXEC // [exec.continues.on] namespace __trnsfr { - template - using __decay_value_sig = set_value_t (*)(__decay_t<_Values>...); - - template - using __decay_error_sig = set_error_t (*)(__decay_t<_Error>); - - template - using __completions_impl_t = __mtry_q<__concat_completion_signatures_t>::__f< - __transform_reduce_completion_signatures_t<_Completions, - __decay_value_sig, - __decay_error_sig, - set_stopped_t (*)(), - __completion_signature_ptrs_t>, - __transform_completion_signatures_t< - __completion_signatures_of_t, _Env...>, - __eptr_completion_unless_t<__nothrow_decay_copyable_results_t<_Completions>>, - __mconst>::__f>>; - - template - using __completions_t = - __completions_impl_t<_Scheduler, __completion_signatures_of_t<_CvSender, _Env...>, _Env...>; + template + using __completion_storage_t = + storage_for_completion_signatures<__completion_signatures_of_t<_Sender, _Env...>>; template struct __state_base { - using __storage_t = __storage_for_t<__child_of<_Sexpr>, env_of_t<_Receiver>>; + using __storage_t = __completion_storage_t<__child_of<_Sexpr>, env_of_t<_Receiver>>; _Receiver __rcvr_; __storage_t __data_; @@ -82,7 +63,7 @@ namespace STDEXEC constexpr void set_value() noexcept { - std::move(__state_->__data_).__complete(__state_->__rcvr_); + std::move(__state_->__data_).complete(static_cast<_Receiver&&>(__state_->__rcvr_)); } template @@ -128,26 +109,6 @@ namespace STDEXEC struct __attrs { private: - //! @brief Returns `true` when: - //! - _SetTag is set_error_t, and - //! - _Sender has value completions, and - //! - at least one of the value completions is not nothrow decay-copyable. - //! In that case, error completions can come from the sender's value completions. - template - static consteval bool __has_decay_copy_errors() noexcept - { - if constexpr (__same_as<_SetTag, set_error_t>) - { - if constexpr (__sends...>) - { - return !__cmplsigs::__partitions_of_t...>>::__nothrow_decay_copyable::__values::value; - } - } - return false; - } - _Scheduler __sch_; env_of_t<_Sender> __attrs_; @@ -165,10 +126,9 @@ namespace STDEXEC //! @note If @c _SetTag is @c set_value_t, then we are in the happy path: everything //! succeeded and execution continues on @c _Scheduler. //! - //! Otherwise, if @c _Sender never completes with @c _SetTag, and either @c _SetTag is - //! @c set_stopped_t or decay-copying @c _Sender's value results cannot throw, then a - //! @c _SetTag completion can only come from the scheduler's sender. In this case, return - //! the scheduler's completion scheduler if it has one. + //! Otherwise, if @c _Sender never completes with @c _SetTag, then a @c _SetTag completion + //! can only happen on the scheduler's execution resource. In this case, return the + //! scheduler's completion scheduler if it has one. //! //! Otherwise, if the scheduler's sender never completes with @c _SetTag, then a //! @c _SetTag completion can only come from the original sender, so return the @@ -176,7 +136,6 @@ namespace STDEXEC template requires(__same_as<_SetTag, set_value_t> || __never_sends<_SetTag, _Sender, __fwd_env_t<_Env>...>) - && (!__has_decay_copy_errors<_SetTag, _Env...>()) [[nodiscard]] constexpr auto query(get_completion_scheduler_t<_SetTag>, _Env const &... __env) const noexcept @@ -205,14 +164,8 @@ namespace STDEXEC //! @note If @c _SetTag is @c set_value_t, then we are in the happy path: everything //! succeeded and execution continues on @c _Scheduler. //! - //! Otherwise, if @c _SetTag is @c set_stopped_t or if decay-copying @c _Sender's value - //! results cannot throw, then a @c _SetTag completion can happen on the sender's - //! completion domain (if it has one) or the scheduler's completion domain (if it has - //! one). - //! - //! @note Otherwise, @c _SetTag is @c set_error_t and decay-copying @c _Sender's value - //! results can throw, so error completions can also come from the sender's value - //! completions. + //! Otherwise, a @c _SetTag completion can happen on the sender's completion domain (if it + //! has one) or the scheduler's completion domain (if it has one). template <__same_as _SetTag, class... _Env> [[nodiscard]] constexpr auto @@ -225,7 +178,6 @@ namespace STDEXEC //! @overload template <__one_of _SetTag, class... _Env> - requires(!__has_decay_copy_errors<_SetTag, _Env...>()) [[nodiscard]] constexpr auto query(get_completion_domain_t<_SetTag>, _Env const &...) const noexcept -> __unless_one_of_t< @@ -237,21 +189,6 @@ namespace STDEXEC return {}; } - //! @overload - template - requires(__has_decay_copy_errors<_SetTag, _Env...>()) - [[nodiscard]] - constexpr auto - query(get_completion_domain_t<_SetTag>, _Env const &...) const noexcept -> __unless_one_of_t< - __common_domain_t< - __completion_domain_of_t<_SetTag, _Sender, __fwd_env_t<_Env>...>, - __completion_domain_of_t<_SetTag, schedule_result_t<_Scheduler>, __fwd_env_t<_Env>...>, - __completion_domain_of_t...>>, - indeterminate_domain<>> - { - return {}; - } - //! @brief Queries the completion behavior of the combined sender. //! @tparam _Env The environment to consider when querying for the completion behavior. //! @note The completion behavior is the minimum between the scheduler's sender and @@ -295,12 +232,8 @@ namespace STDEXEC STDEXEC::get_completion_signatures<_Child, __fwd_env_t<_Env>...>(); STDEXEC_IF_OK(__child_completions) { - // continues_on has the completions of the child sender, but with value and - // error result types decayed. - return __transform_completion_signatures( - __child_completions, - __decay_arguments(), - __decay_arguments()); + using __storage_t = __completion_storage_t<_Child, __fwd_env_t<_Env>...>; + return __storage_t::get_completion_signatures(); } } @@ -339,9 +272,7 @@ namespace STDEXEC auto __child_completions = __get_child_completions<__child_t, _Env...>(); return __concat_completion_signatures( __child_completions, - __get_scheduler_completions<__scheduler_t, _Env...>(), - __eptr_completion_unless_t< - __nothrow_decay_copyable_results_t>()); + __get_scheduler_completions<__scheduler_t, _Env...>()); } static constexpr auto __get_state = @@ -364,22 +295,7 @@ namespace STDEXEC { // Write the tag and the args into the operation state so that we can forward the completion // from within the scheduler's execution context. - if constexpr (__nothrow_callable<__mktuple_t, _Tag, _Args...>) - { - __state.__data_.__emplace_from(__mktuple, __tag, static_cast<_Args&&>(__args)...); - } - else - { - STDEXEC_TRY - { - __state.__data_.__emplace_from(__mktuple, __tag, static_cast<_Args&&>(__args)...); - } - STDEXEC_CATCH_ALL - { - STDEXEC::set_error(static_cast<_State&&>(__state).__rcvr_, std::current_exception()); - return; - } - } + __state.__data_.arrive(__tag, static_cast<_Args&&>(__args)...); // Enqueue the schedule operation so the completion happens on the scheduler's execution // context. @@ -423,15 +339,15 @@ namespace STDEXEC //! the sender produced by continues_on(sndr, sched) has the same //! completion signatures as @c sndr, except that a //! @c set_error_t(std::exception_ptr) completion may be added if any of - //! @c sndr's completion datums are not @c nothrow decay-copyable (the - //! datums must be stored across the scheduling hop). + //! @c sndr's completion datums cannot be stored without throwing (the datums + //! must be stored across the scheduling hop). //! //! @c continues_on does *not* alter @c sndr's values or errors; it only //! changes the execution context on which the completion is delivered. //! //! **Exception behavior.** //! - //! If decay-copying a completion datum across the scheduling hop throws, + //! If storing a completion datum across the scheduling hop throws, //! the exception is delivered through @c set_error_t(std::exception_ptr). //! If scheduling onto @c sched fails, an error completion is delivered on //! an unspecified execution agent. @@ -483,7 +399,7 @@ namespace STDEXEC //! //! @returns A sender with the same completion signatures as @c __sndr //! (plus a possible @c set_error_t(std::exception_ptr) for - //! decay-copy failures during the hop). The completions are + //! storage failures during the hop). The completions are //! delivered on @c __sched's execution resource. template constexpr auto diff --git a/test/stdexec/algos/adaptors/test_continues_on.cpp b/test/stdexec/algos/adaptors/test_continues_on.cpp index c4a829af3..535242b96 100644 --- a/test/stdexec/algos/adaptors/test_continues_on.cpp +++ b/test/stdexec/algos/adaptors/test_continues_on.cpp @@ -31,6 +31,144 @@ using namespace std::chrono_literals; namespace { + constinit int global_int = 0; + constinit double global_double = 0.0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int *value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + int *value_; + }; + + struct references_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int *int_value_; + double *double_value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *int_value_, *double_value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), int_value_, double_value_}; + } + + int *int_value_; + double *double_value_; + }; + + struct value_domain + {}; + + struct error_domain + {}; + + struct throwing_value_domain_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + struct attrs + { + constexpr auto + query(ex::get_completion_domain_t) const noexcept -> value_domain + { + return {}; + } + + constexpr auto + query(ex::get_completion_domain_t) const noexcept -> error_domain + { + return {}; + } + }; + + constexpr auto get_env() const noexcept -> attrs + { + return {}; + } + }; + + struct error_domain_scheduler + { + using scheduler_concept = ex::scheduler_tag; + + struct sender + { + using sender_concept = ex::sender_tag; + using completion_signatures = + ex::completion_signatures; + + struct attrs + { + template + constexpr auto query(ex::get_completion_domain_t<_Tag>) const noexcept -> error_domain + { + return {}; + } + }; + + constexpr auto get_env() const noexcept -> attrs + { + return {}; + } + }; + + constexpr auto schedule() const noexcept -> sender + { + return {}; + } + + auto operator==(error_domain_scheduler const &) const noexcept -> bool = default; + }; + struct potentially_throwing_connect_scheduler { using scheduler_concept = ex::scheduler_tag; @@ -101,6 +239,54 @@ namespace // The receiver checks if we receive the right value } + TEST_CASE("continues_on preserves a single reference completion", "[adaptors][continues_on]") + { + global_int = 42; + auto raw = ex::continues_on(reference_sender{&global_int}, inline_scheduler{}); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto &&i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("continues_on preserves multiple reference completion arguments", + "[adaptors][continues_on]") + { + global_int = 42; + global_double = 0.125; + auto raw = ex::continues_on(references_sender{&global_int, &global_double}, inline_scheduler{}); + STATIC_REQUIRE(set_equivalent>, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto &&i, auto &&d) noexcept + { + CHECK(&i == &global_int); + CHECK(&d == &global_double); + CHECK(i == 42); + CHECK(d == 0.125); + }); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("continues_on does not report the source value domain for persistence errors", + "[adaptors][continues_on]") + { + using attrs = ex::__trnsfr::__attrs; + + auto domain = attrs{error_domain_scheduler{}, throwing_value_domain_sender::attrs{}} + .query(ex::get_completion_domain, ex::env<>{}); + STATIC_REQUIRE(std::same_as); + } + TEST_CASE("continues_on can be piped", "[adaptors][continues_on]") { // Just transfer a value to the impulse scheduler diff --git a/test/stdexec/algos/adaptors/test_on.cpp b/test/stdexec/algos/adaptors/test_on.cpp index 6300b09cd..f0f0b18ed 100644 --- a/test/stdexec/algos/adaptors/test_on.cpp +++ b/test/stdexec/algos/adaptors/test_on.cpp @@ -20,11 +20,50 @@ #include #include #include +#include namespace ex = STDEXEC; namespace { + constinit int global_int = 0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && -> operation + { + return {static_cast(receiver), value_}; + } + + auto get_env() const noexcept + { + return ex::prop{ex::get_completion_scheduler, inline_scheduler{}}; + } + + int* value_; + }; + template inline auto _make_env_with_sched(Sched sched = {}) { @@ -119,6 +158,52 @@ namespace REQUIRE(called); } + TEST_CASE("STDEXEC::on preserves reference completions in whole-sender form", "[adaptors][on]") + { + global_int = 42; + auto raw = ex::on(inline_scheduler{}, reference_sender{&global_int}); + STATIC_REQUIRE( + set_equivalent< + ex::completion_signatures_of_t, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }) + | _with_scheduler(); + + ex::sync_wait(std::move(snd)); + } + + TEST_CASE("STDEXEC::on preserves reference completions in closure form", "[adaptors][on]") + { + global_int = 42; + auto raw = reference_sender{&global_int} + | ex::on(inline_scheduler{}, + ex::then( + [](int& i) noexcept -> int& + { + CHECK(&i == &global_int); + CHECK(i == 42); + return i; + })); + STATIC_REQUIRE(set_equivalent, + ex::completion_signatures>); + auto snd = std::move(raw) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }) + | _with_scheduler(); + + ex::sync_wait(std::move(snd)); + } + TEST_CASE("STDEXEC::on can be called with rvalue ref scheduler", "[adaptors][on]") { auto env = _make_env_with_sched(); From 8617cd3e72100fc1fd2a42f04cb097fc1af04b5a Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Mon, 29 Jun 2026 09:07:50 -0400 Subject: [PATCH 09/15] spawn_future: Reference Aware See P4288. --- include/stdexec/__detail/__spawn_future.hpp | 95 ++++++++++--------- .../algos/adaptors/test_spawn_future.cpp | 57 ++++++++++- 2 files changed, 103 insertions(+), 49 deletions(-) diff --git a/include/stdexec/__detail/__spawn_future.hpp b/include/stdexec/__detail/__spawn_future.hpp index 624025cbe..48e2fe654 100644 --- a/include/stdexec/__detail/__spawn_future.hpp +++ b/include/stdexec/__detail/__spawn_future.hpp @@ -35,8 +35,7 @@ import stdexec; # include "__scope_concepts.hpp" # include "__senders.hpp" # include "__spawn_common.hpp" -# include "__storage.hpp" -# include "__tuple.hpp" +# include "__storage_for_completion_signatures.hpp" # include "__type_traits.hpp" # if !STDEXEC_USE_MODULES() @@ -49,13 +48,6 @@ import stdexec; namespace STDEXEC { - template - using __decayed_signature_t = __mapply<__mtransform<__q1<__decay_t>, __q<__fn_t>>, _Signature>; - - template - inline constexpr bool __nothrow_storable_signature = - __mapply<__qq<__nothrow_decay_copyable_t>, _Signature>::value; - ///////////////////////////////////////////////////////////////////////////// // [exec.spawn.future] namespace __spawn_future @@ -96,24 +88,41 @@ namespace STDEXEC template struct __future_completions { - // this case handles _NothrowSyncWork == true - // this case is applicable when both conditions are true: - // - the results of all possible completions can be decay-copied into the decayed-tuple that - // stores the results for later consumption by the future; and - // - the stop token provided by the future's receiver can no-throw-construct a stop callback - // in the future's operation state - using type = __mcall<__munique<__qq>, - set_stopped_t(), - __decayed_signature_t<_Sigs>...>; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + // This case handles _NothrowSyncWork == true, which means the stop token provided by the + // future's receiver can no-throw-construct a stop callback in the future's operation state. + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; + using type = typename __storage_t::completion_signatures; + + static consteval auto get_completion_signatures() + { + return __storage_t::get_completion_signatures(); + } }; template struct __future_completions { - using type = __mcall<__munique<__qq>, - set_stopped_t(), - set_error_t(std::exception_ptr), - __decayed_signature_t<_Sigs>...>; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; + using type = + __concat_completion_signatures_t>; + + static consteval auto get_completion_signatures() + { + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return __concat_completion_signatures( + __stored_completions, + completion_signatures{}); + } + } }; template @@ -124,9 +133,7 @@ namespace STDEXEC template using __future_completions_t = - __future_completions<__stop_callback_is_nothrow_constructible<_Env> - && (__nothrow_storable_signature<_Sigs> && ...), - _Sigs...>::type; + __future_completions<__stop_callback_is_nothrow_constructible<_Env>, _Sigs...>::type; // [exec.spawn.future] paragraph 3 template @@ -135,11 +142,21 @@ namespace STDEXEC template struct __spawn_future_state_base> : __try_cancelable { - using __variant_t = __results_storage; + using __stored_signatures_t = + __concat_completion_signatures_t, + completion_signatures<_Sigs...>>; + using __storage_t = storage_for_completion_signatures<__stored_signatures_t>; template using __completions_t = __future_completions_t<_Env, _Sigs...>; - __variant_t __result_; + template + static consteval auto __get_completion_signatures() + { + return __future_completions<__stop_callback_is_nothrow_constructible<_Env>, + _Sigs...>::get_completion_signatures(); + } + + __storage_t __result_; explicit __spawn_future_state_base( void (*__try_cancel)(__try_cancelable*) noexcept, @@ -191,20 +208,7 @@ namespace STDEXEC template void __set_complete(_Ts&&... __ts) noexcept { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Tag, _Ts...>; - __state_->__result_.template emplace<__tuple_t>(_Tag{}, static_cast<_Ts&&>(__ts)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_Ts...>) - { - using tuple_t = __decayed_tuple; - __state_->__result_.template emplace(set_error_t{}, std::current_exception()); - } - } - + __state_->__result_.arrive(_Tag{}, static_cast<_Ts&&>(__ts)...); __state_->__complete(); } }; @@ -229,7 +233,7 @@ namespace STDEXEC using __op_t = connect_result_t<__future_spawned_sender<_Sender, _Env>, __receiver_t>; - using __base = __spawn_future_state_base>; + using __base = __spawn_future_state_base<__sigs_t>; __spawn_future_state(_Alloc __alloc, _Sender&& __sndr, _Token __token, _Env __env) : __base(__do_try_cancel, __do_complete) @@ -391,7 +395,7 @@ namespace STDEXEC // NOTE: __rcvr's type is unconstrained because the thing we pass doesn't satisfy receiver void __do_consume(auto& __rcvr) noexcept { - std::move(this->__result_).__complete(__rcvr); + std::move(this->__result_).complete(std::move(__rcvr)); } static void __do_complete(__base* __base_ptr) noexcept @@ -693,10 +697,9 @@ namespace STDEXEC using __completions_t = __spawn_future_state_t<_Sender>::template __completions_t<_Env>; template - static consteval auto __get_completion_signatures() // - -> __completions_t<_Sender, _Env> + static consteval auto __get_completion_signatures() { - return {}; + return __spawn_future_state_t<_Sender>::template __get_completion_signatures<_Env>(); }; static constexpr auto __get_state = diff --git a/test/stdexec/algos/adaptors/test_spawn_future.cpp b/test/stdexec/algos/adaptors/test_spawn_future.cpp index d44931853..2fe84ed4b 100644 --- a/test/stdexec/algos/adaptors/test_spawn_future.cpp +++ b/test/stdexec/algos/adaptors/test_spawn_future.cpp @@ -31,6 +31,39 @@ namespace ex = STDEXEC; namespace { + constinit int global_int = 0; + + struct reference_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + return ex::completion_signatures{}; + } + + template + struct operation + { + Receiver receiver_; + int* value_; + + void start() & noexcept + { + ex::set_value(static_cast(receiver_), *value_); + } + }; + + template + auto connect(Receiver receiver) && noexcept -> operation + { + return {static_cast(receiver), value_}; + } + + int* value_; + }; + TEST_CASE("future completion signature calculation works", "[adaptors][spawn_future]") { { @@ -56,9 +89,8 @@ namespace } { - using expected = ex::completion_signatures; + using expected = + ex::completion_signatures; using actual = ex::__spawn_future::__future_completions_t, ex::set_value_t(std::string const &)>; @@ -66,6 +98,25 @@ namespace } } + TEST_CASE("spawn_future preserves reference completions", "[adaptors][spawn_future]") + { + global_int = 42; + auto future = ex::spawn_future(reference_sender{&global_int}, null_token{}); + using expected = ex::completion_signatures; + using actual = ex::completion_signatures_of_t>; + STATIC_REQUIRE(actual{} == expected{}); + + auto snd = std::move(future) + | ex::then( + [](auto&& i) noexcept + { + CHECK(&i == &global_int); + CHECK(i == 42); + }); + + ex::sync_wait(std::move(snd)); + } + TEST_CASE("spawn_future(just(...)) is equivalent to just(...)", "[adaptors][spawn_future]") { constexpr auto checkEquivalence = [](ex::sender auto const & sender) From 49fbcb3522e6c84ddcc3a980745ca9e081f8621a Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 11:57:16 -0400 Subject: [PATCH 10/15] task: Reference Aware See P4288. --- test/stdexec/types/test_task.cpp | 37 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 00dfe21cc..35f360f9c 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -331,7 +331,7 @@ namespace })); } - constinit int global_int = 0; + constinit int global_int = 0; constinit double global_double = 0.0; constexpr auto wrap_ref = ex::then([](auto &i) noexcept { return std::ref(i); }); @@ -351,7 +351,7 @@ namespace struct operation { Receiver receiver_; - int * value_; + int *value_; void start() & noexcept { @@ -405,8 +405,8 @@ namespace struct operation { Receiver receiver_; - int * int_value_; - double * double_value_; + int *int_value_; + double *double_value_; void start() & noexcept { @@ -442,8 +442,8 @@ namespace return {}; } - int * int_value_; - double * double_value_; + int *int_value_; + double *double_value_; }; struct non_affine_reference_sender @@ -460,7 +460,7 @@ namespace struct operation { Receiver receiver_; - int * value_; + int *value_; void start() & noexcept { @@ -549,40 +549,35 @@ namespace CHECK(i == 42); } - TEST_CASE("task co_await preserves a single reference from inline sender", - "[types][task]") + TEST_CASE("task co_await preserves a single reference from inline sender", "[types][task]") { global_int = 42; - auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); CHECK(i == 42); } - TEST_CASE("task co_await preserves a single reference from async sender", - "[types][task]") + TEST_CASE("task co_await preserves a single reference from async sender", "[types][task]") { global_int = 42; - auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); + auto [i] = ex::sync_wait(task_awaits_reference_sender()).value(); CHECK(i == 42); } - TEST_CASE("task co_await preserves reference tuple from inline sender", - "[types][task]") + TEST_CASE("task co_await preserves reference tuple from inline sender", "[types][task]") { - global_int = 42; + global_int = 42; global_double = 0.125; ex::sync_wait(task_awaits_references_sender()); } - TEST_CASE("task co_await preserves reference tuple from async sender", - "[types][task]") + TEST_CASE("task co_await preserves reference tuple from async sender", "[types][task]") { - global_int = 42; + global_int = 42; global_double = 0.125; ex::sync_wait(task_awaits_references_sender()); } - TEST_CASE("task co_await preserves a reference when affine inserts finally", - "[types][task]") + TEST_CASE("task co_await preserves a reference when affine inserts finally", "[types][task]") { global_int = 42; ex::sync_wait(task_awaits_non_affine_reference_sender()); From 04722a779adc0ca4967d7c1c1a0a692d5cafc462 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 13:43:35 -0400 Subject: [PATCH 11/15] exec::when_any: Reference Aware --- include/exec/when_any.hpp | 73 +++++++++---------------------------- test/exec/test_when_any.cpp | 19 +++++++++- 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/include/exec/when_any.hpp b/include/exec/when_any.hpp index 94d8e6918..6664d5b09 100644 --- a/include/exec/when_any.hpp +++ b/include/exec/when_any.hpp @@ -16,7 +16,7 @@ */ #pragma once -#include "../stdexec/__detail/__storage.hpp" +#include "../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../stdexec/execution.hpp" #include "../stdexec/stop_token.hpp" @@ -34,46 +34,22 @@ 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...); - - // 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++. + // We 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>>...>; - template using __f = __mtry_q<__concat_completion_signatures_t>::__f< - __eptr_completion_unless_t<__all_value_args_nothrow_decay_copyable<_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>...>; + __completion_signatures_of_t<_CvSenders, __env_t<_Env>...>...>; }; template using __results_storage_t = - __mapply_q<__results_storage, __minvoke<__completions_fn<_Env>, _CvSenders...>>; + storage_for_completion_signatures<__minvoke<__completions_fn<_Env>, _CvSenders...>>; - template + template struct __opstate_base { __opstate_base(_Receiver&& __rcvr, std::size_t __n_senders) @@ -112,35 +88,19 @@ namespace experimental::execution __std::atomic __count_{}; _Receiver __rcvr_; - _ResultVariant __result_; + _ResultStorage __result_; template void notify(_Tag, _Args&&... __args) noexcept { - using __result_t = __decayed_tuple<_Tag, _Args...>; - bool __expect = false; + bool __expect = false; if (__emplaced_.compare_exchange_strong(__expect, true, __std::memory_order_relaxed, __std::memory_order_relaxed)) { - // This emplacement can happen only once - if constexpr ((__nothrow_decay_copyable<_Args> && ...)) - { - __result_.template emplace<__result_t>(_Tag{}, static_cast<_Args&&>(__args)...); - } - else - { - STDEXEC_TRY - { - __result_.template emplace<__result_t>(_Tag{}, static_cast<_Args&&>(__args)...); - } - STDEXEC_CATCH_ALL - { - using __error_t = __tuple; - __result_.template emplace<__error_t>(set_error_t{}, std::current_exception()); - } - } + // This arrival can happen only once. + __result_.arrive(_Tag{}, static_cast<_Args&&>(__args)...); // stop pending operations __stop_source_.request_stop(); } @@ -160,18 +120,18 @@ namespace experimental::execution STDEXEC::set_stopped(static_cast<_Receiver&&>(__rcvr_)); return; } - std::move(__result_).__complete(__rcvr_); + std::move(__result_).complete(static_cast<_Receiver&&>(__rcvr_)); } } }; - template + template struct __receiver { public: using receiver_concept = STDEXEC::receiver_tag; - explicit __receiver(__opstate_base<_Receiver, _ResultVariant>* __op) noexcept + explicit __receiver(__opstate_base<_Receiver, _ResultStorage>* __op) noexcept : __op_{__op} {} @@ -199,7 +159,7 @@ namespace experimental::execution } private: - __opstate_base<_Receiver, _ResultVariant>* __op_; + __opstate_base<_Receiver, _ResultStorage>* __op_; }; template @@ -249,6 +209,9 @@ namespace experimental::execution template using __completions_t = __minvoke<__completions_fn<_Env...>, __copy_cvref_t<_Self, _Senders>...>; + + template + using __storage_t = storage_for_completion_signatures<__completions_t<_Self, _Env...>>; public: using sender_concept = STDEXEC::sender_tag; @@ -274,7 +237,7 @@ namespace experimental::execution template <__decay_copyable _Self, class... _Env> static consteval auto get_completion_signatures() { - return __completions_t<_Self, _Env...>{}; + return __storage_t<_Self, _Env...>::get_completion_signatures(); } template diff --git a/test/exec/test_when_any.cpp b/test/exec/test_when_any.cpp index 58df5951c..70577e8e8 100644 --- a/test/exec/test_when_any.cpp +++ b/test/exec/test_when_any.cpp @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -54,6 +55,16 @@ namespace ex::start(op); } + TEST_CASE("when_any preserves reference value completions", "[adaptors][when_any]") + { + auto source = exec::split(ex::just(42)); + auto snd = exec::when_any(std::move(source)); + static_assert(set_equivalent, + completion_signatures>); + auto op = ex::connect(std::move(snd), expect_value_receiver{42}); + ex::start(op); + } + TEST_CASE("when_any completes with only one sender", "[adaptors][when_any]") { ex::sender auto snd = exec::when_any(completes_if{false} | ex::then([] { return 1; }), @@ -215,9 +226,13 @@ namespace } }; - TEST_CASE("when_any - with duplicate completions", "[adaptors][when_any]") + TEST_CASE("when_any rejects ambiguous normalized completions", "[adaptors][when_any]") { - REQUIRE_THROWS(STDEXEC::sync_wait(exec::when_any(dup_sender{}))); + using sender_t = decltype(exec::when_any(dup_sender{})); + using completions_t = + decltype(sender_t::template get_completion_signatures>()); + + static_assert(ex::__merror); } #endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() } // namespace From 607d6a41bcead0567a5927254c8f62a3a392918f Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 13:53:44 -0400 Subject: [PATCH 12/15] exec::fork_join: Reference Aware --- include/exec/fork_join.hpp | 78 ++++++++++++++++++------------------ test/exec/test_fork_join.cpp | 24 +++++++++++ 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/include/exec/fork_join.hpp b/include/exec/fork_join.hpp index 33fa9b495..eb5f67eff 100644 --- a/include/exec/fork_join.hpp +++ b/include/exec/fork_join.hpp @@ -16,18 +16,13 @@ #pragma once #include "../stdexec/__detail/__receiver_ref.hpp" -#include "../stdexec/__detail/__storage.hpp" +#include "../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../stdexec/execution.hpp" -#include - namespace experimental::execution { struct fork_join_t; - struct PREDECESSOR_RESULTS_ARE_NOT_DECAY_COPYABLE - {}; - struct INVALID_ARGUMENTS_TO_FORK_JOIN {}; @@ -44,7 +39,7 @@ namespace experimental::execution }; template - using _variant_t = STDEXEC::__mapply_q; + using _storage_t = STDEXEC::storage_for_completion_signatures; template struct _env_t @@ -70,7 +65,14 @@ namespace experimental::execution using _cache_sndr_completions_t = STDEXEC::completion_signatures, AsyncResults>...>; - template + template + consteval auto _cache_sndr_completions(STDEXEC::completion_signatures) + -> _cache_sndr_completions_t + { + return {}; + } + + template struct _cache_sndr_t { using sender_concept = STDEXEC::sender_tag; @@ -82,18 +84,39 @@ namespace experimental::execution STDEXEC_ATTRIBUTE(host, device) void start() noexcept { - std::as_const(*_results_).__complete(_rcvr_); + STDEXEC::visit_stored_completion( + STDEXEC::__overload{[]() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&](auto const & __completion) noexcept -> void + { + STDEXEC::__apply( + [&](auto&&... __args) noexcept + { + __completion.tag()(static_cast(_rcvr_), + static_cast( + __args)...); + }, + __completion.__forward_arguments()); + }}, + *_results_); } Rcvr _rcvr_; - Variant const * _results_; + Storage const * _results_; }; template STDEXEC_ATTRIBUTE(host, device) static consteval auto get_completion_signatures() { - return STDEXEC::__mapply, Variant>{}; + auto _stored_completions = Storage::get_completion_signatures(); + STDEXEC_IF_OK(_stored_completions) + { + return _cache_sndr_completions(_stored_completions); + } } template @@ -109,14 +132,14 @@ namespace experimental::execution return {}; } - Variant const * _results_; + Storage const * _results_; }; template using _when_all_sndr_t = STDEXEC::__apply_result_t<_mk_when_all_fn, Closures, - _cache_sndr_t<_variant_t, Domain>>; + _cache_sndr_t<_storage_t, Domain>>; template struct _opstate_t @@ -131,7 +154,7 @@ namespace experimental::execution STDEXEC::connect_result_t>; using _fork_opstate_t = STDEXEC::connect_result_t<_when_all_sndr_t, STDEXEC::__receiver_ref>; - using _cache_sndr_t = _fork_join::_cache_sndr_t<_variant_t<_child_completions_t>, _domain_t>; + using _cache_sndr_t = _fork_join::_cache_sndr_t<_storage_t<_child_completions_t>, _domain_t>; STDEXEC_ATTRIBUTE(host, device) constexpr explicit _opstate_t(Sndr&& sndr, Closures&& closures, Rcvr rcvr) noexcept @@ -152,7 +175,7 @@ namespace experimental::execution constexpr ~_opstate_t() { // If this opstate was never started, we must explicitly destroy the _child_opstate_. - if (_cache_.__is_valueless()) + if (!_cache_.has_completion()) { _child_opstate_.__destroy(); } @@ -168,20 +191,7 @@ namespace experimental::execution STDEXEC_ATTRIBUTE(host, device) constexpr void _complete(Tag, Args&&... args) noexcept { - STDEXEC_TRY - { - using _tuple_t = STDEXEC::__decayed_tuple; - _cache_.template emplace<_tuple_t>(Tag{}, static_cast(args)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!STDEXEC::__nothrow_decay_copyable) - { - using _tuple_t = STDEXEC::__tuple; - _cache_._results_.template emplace<_tuple_t>(STDEXEC::set_error, - ::std::current_exception()); - } - } + _cache_.arrive(Tag{}, static_cast(args)...); _child_opstate_.__destroy(); STDEXEC::start(_fork_opstate_); } @@ -213,7 +223,7 @@ namespace experimental::execution } Rcvr _rcvr_; - _variant_t<_child_completions_t> _cache_; + _storage_t<_child_completions_t> _cache_; STDEXEC::__manual_lifetime<_child_opstate_t> _child_opstate_{}; _fork_opstate_t _fork_opstate_; }; @@ -238,18 +248,10 @@ namespace experimental::execution using _domain_t = __completion_domain_of_t; using _child_t = __copy_cvref_t; using _child_completions_t = __completion_signatures_of_t<_child_t, __fwd_env_t...>; - using __decay_copyable_results_t = __decay_copyable_results_t<_child_completions_t>; - if constexpr (!__valid_completion_signatures<_child_completions_t>) { return _child_completions_t{}; } - else if constexpr (!__decay_copyable_results_t::value) - { - return STDEXEC::__throw_compile_time_error< // - _WHAT_(PREDECESSOR_RESULTS_ARE_NOT_DECAY_COPYABLE), - _IN_ALGORITHM_(exec::fork_join_t)>(); - } else { using _sndr_t = diff --git a/test/exec/test_fork_join.cpp b/test/exec/test_fork_join.cpp index 71e7b99dd..26e089e9c 100644 --- a/test/exec/test_fork_join.cpp +++ b/test/exec/test_fork_join.cpp @@ -114,6 +114,30 @@ namespace CHECK(i2 == 42); } + TEST_CASE("fork_join preserves reference results", "[adaptors][fork_join]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return completion_signatures{}; + }); + auto check_reference = then([&](immovable &result) noexcept { CHECK(&result == &value); }); + + auto sndr = exec::fork_join(std::move(source), check_reference, check_reference); + STATIC_REQUIRE(sender_in>); + sync_wait(std::move(sndr)); + } + TEST_CASE("fork_join with empty value channel", "[adaptors][fork_join]") { auto sndr = ::STDEXEC::just() | ::STDEXEC::then([]() noexcept -> void {}) From 8891e674fe7b9c1f9124019216f254b3d7b9e599 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 14:19:29 -0400 Subject: [PATCH 13/15] exec::split & ::ensure_started: Reference Aware --- include/exec/detail/shared.hpp | 129 ++++++++++++++++++------------ test/exec/test_any_sender.cpp | 2 +- test/exec/test_ensure_started.cpp | 30 +++++++ test/exec/test_split.cpp | 42 +++++++++- 4 files changed, 148 insertions(+), 55 deletions(-) diff --git a/include/exec/detail/shared.hpp b/include/exec/detail/shared.hpp index 58407d93a..604635928 100644 --- a/include/exec/detail/shared.hpp +++ b/include/exec/detail/shared.hpp @@ -27,14 +27,15 @@ #include "../../stdexec/__detail/__optional.hpp" #include "../../stdexec/__detail/__queries.hpp" #include "../../stdexec/__detail/__receivers.hpp" -#include "../../stdexec/__detail/__storage.hpp" +#include "../../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../../stdexec/__detail/__transform_completion_signatures.hpp" #include "../../stdexec/__detail/__tuple.hpp" #include "../../stdexec/__detail/__variant.hpp" // IWYU pragma: keep #include "../../stdexec/stop_token.hpp" -#include #include +#include +#include #include //////////////////////////////////////////////////////////////////////////// @@ -69,7 +70,7 @@ namespace experimental::execution::__shared { using namespace STDEXEC; - template + template struct __shared_state_base; template @@ -79,7 +80,7 @@ namespace experimental::execution::__shared using __env_t = __join_env_t, _Env>; //////////////////////////////////////////////////////////////////////////////////////// - template + template struct __receiver { using receiver_concept = receiver_tag; @@ -110,14 +111,23 @@ namespace experimental::execution::__shared } // The receiver does not hold a reference to the shared state. - __shared_state_base<_Env, _Variant>* __sh_state_; + __shared_state_base<_Env, _Storage>* __sh_state_; }; //////////////////////////////////////////////////////////////////////////////////////// template - using __result_variant_t = - __mapply<__mbind_front_q<__results_storage, set_stopped_t(), set_error_t(std::exception_ptr)>, - __completion_signatures_of_t<_CvSender, _Env>>; + using __result_storage_t = storage_for_completion_signatures< + __transform_completion_signatures_of_t<_CvSender, + __env_t<_Env>, + completion_signatures>>; + + // Owned results from split are replayed by const reference. Results that are already + // references retain the type advertised by the child. ensure_started consumes the cache, so + // it replays every result with its stored type. + template + using __replayed_result_t = + __minvoke<__if_c<__same_as<_Tag, split_t> && !std::is_reference_v<_Result>, __cpclr, __cp>, + _Result>; //////////////////////////////////////////////////////////////////////////////////////// template @@ -241,13 +251,37 @@ namespace experimental::execution::__shared // __local_state::operator(). constexpr void __notify() noexcept final { - // The split algorithm sends by T const&. ensure_started sends by T&&. - constexpr bool __is_split = __std::same_as; - using __variant_t = decltype(__sh_state_->__results_); - using __cv_variant_t = __if_c<__is_split, __variant_t const &, __variant_t>; - __on_stop_.reset(); - static_cast<__cv_variant_t&&>(__sh_state_->__results_).__complete(__rcvr_); + if constexpr (__std::same_as) + { + STDEXEC::visit_stored_completion( + STDEXEC::__overload{ + []() noexcept -> void + { + STDEXEC_ASSERT(false); + STDEXEC_UNREACHABLE(); + }, + [&]( + storage_for_completion_signature<_SetTag(_Results...)> const & __completion) noexcept + -> void + { + STDEXEC::__apply( + [&](auto&&... __results) noexcept + { + _SetTag{}(static_cast<_Receiver&&>(__rcvr_), + static_cast<__replayed_result_t>(__results)...); + }, + __completion.__forward_arguments()); + }}, + std::as_const(__sh_state_->__results_)); + } + else + { + bool const __completed = + std::move(__sh_state_->__results_).complete(static_cast<_Receiver&&>(__rcvr_)); + STDEXEC_ASSERT(__completed); + (void) __completed; + } } _Receiver __rcvr_; @@ -257,7 +291,7 @@ namespace experimental::execution::__shared //////////////////////////////////////////////////////////////////////////////////////// //! Base class for heap-allocatable shared state for `split` and `ensure_started`. - template + template struct __shared_state_base : __local_state_base { using __waiters_list_t = __intrusive_slist<&__local_state_base::__next_>; @@ -265,9 +299,7 @@ namespace experimental::execution::__shared constexpr explicit __shared_state_base(_Env __env) : __env_(__env::__join(prop{get_stop_token, __stop_source_.get_token()}, static_cast<_Env&&>(__env))) - { - __results_.template emplace<__tuple>(); - } + {} virtual ~__shared_state_base() = 0; @@ -276,20 +308,7 @@ namespace experimental::execution::__shared template void __complete(_Tag, _As&&... __as) noexcept { - STDEXEC_TRY - { - using __tuple_t = __decayed_tuple<_Tag, _As...>; - __results_.template emplace<__tuple_t>(_Tag(), static_cast<_As&&>(__as)...); - } - STDEXEC_CATCH_ALL - { - if constexpr (!__nothrow_decay_copyable<_As...>) - { - using __tuple_t = __decayed_tuple; - __results_.template emplace<__tuple_t>(set_error, std::current_exception()); - } - } - + __results_.arrive(_Tag(), static_cast<_As&&>(__as)...); __notify_waiters(); } @@ -328,20 +347,20 @@ namespace experimental::execution::__shared __waiters_list_t __waiters_{}; inplace_stop_source __stop_source_{}; __env_t<_Env> __env_; - _Variant __results_; // Initialized to the "set_stopped" state in the ctor. + _Storage __results_; }; - template - __shared_state_base<_Env, _Variant>::~__shared_state_base() = default; + template + __shared_state_base<_Env, _Storage>::~__shared_state_base() = default; //////////////////////////////////////////////////////////////////////////////////////// //! Heap-allocatable shared state for `stdexec::split` and `stdexec::ensure_started`. template struct STDEXEC_ATTRIBUTE(empty_bases) __shared_state final : std::enable_shared_from_this<__shared_state<_CvSender, _Env>> - , __shared_state_base<_Env, __result_variant_t<_CvSender, _Env>> + , __shared_state_base<_Env, __result_storage_t<_CvSender, _Env>> { - using __receiver_t = __receiver<_Env, __result_variant_t<_CvSender, _Env>>; + using __receiver_t = __receiver<_Env, __result_storage_t<_CvSender, _Env>>; using __waiters_list_t = __shared_state::__shared_state_base::__waiters_list_t; constexpr explicit __shared_state(_CvSender&& __sndr, _Env __env) @@ -380,7 +399,7 @@ namespace experimental::execution::__shared // 1. Sets __waiters_ to a known "tombstone" value. // 2. Notifies all the waiters that the operation has stopped. // 3. Sets the "is running" bit in the ref count to 0. - this->__notify_waiters(); + this->__complete(set_stopped); } else { @@ -437,19 +456,6 @@ namespace experimental::execution::__shared connect_result_t<_CvSender, __receiver_t> __shared_op_; }; - template - using __make_completions_t = __transform_completion_signatures_of_t< - _CvSender, - __env_t<_Env>, - completion_signatures), set_stopped_t()>, - __mtransform<_Cv, __mcompose<__qq, __qf>>::template __f, - __mtransform<_Cv, __mcompose<__qq, __qf>>::template __f>; - - // split completes with const T&. ensure_started completes with T&&. - template - using __cvref_results_t = - __mcompose<__if_c<__same_as<_Tag, split_t>, __cpclr, __cp>, __q1<__decay_t>>; - template struct __impls : __sexpr_defaults { @@ -460,7 +466,26 @@ namespace experimental::execution::__shared // TODO: update this for constant evaluation if constexpr (__decay_copyable<_CvChild>) { - return __make_completions_t<__cvref_results_t<_Tag>, _CvChild, _Env>(); + auto __child_completions = STDEXEC::get_completion_signatures<_CvChild, __env_t<_Env>>(); + STDEXEC_IF_OK(__child_completions) + { + auto __input_completions = + STDEXEC::__concat_completion_signatures(__child_completions, + completion_signatures{}); + using __storage_t = storage_for_completion_signatures; + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return STDEXEC::__transform_completion_signatures( + __stored_completions, + []() + { + return completion_signatures...)>{}; + }, + []() + { return completion_signatures)>{}; }); + } + } } else { diff --git a/test/exec/test_any_sender.cpp b/test/exec/test_any_sender.cpp index 74b82e058..8c988991c 100644 --- a/test/exec/test_any_sender.cpp +++ b/test/exec/test_any_sender.cpp @@ -282,7 +282,7 @@ namespace #if !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("any_sender uses overload rules for completion signatures", "[types][any_sender]") { - auto split_sender = exec::split(ex::just(42)); + auto split_sender = exec::split(ex::just(42) | ex::then([](int value) { return value; })); STATIC_REQUIRE( ex::sender_of); STATIC_REQUIRE(ex::sender_of); diff --git a/test/exec/test_ensure_started.cpp b/test/exec/test_ensure_started.cpp index bc5139741..7218fb9a6 100644 --- a/test/exec/test_ensure_started.cpp +++ b/test/exec/test_ensure_started.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -111,6 +112,35 @@ namespace ex::start(op); } + TEST_CASE("ensure_started preserves reference results", "[adaptors][ensure_started]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return ex::completion_signatures{}; + }); + auto sndr = exec::ensure_started(std::move(source)); + using sndr_t = decltype(sndr); + static_assert( + set_equivalent>, + ex::completion_signatures>); + + auto checked = std::move(sndr) + | ex::then([&](immovable &result) noexcept { CHECK(&result == &value); }); + auto op = ex::connect(std::move(checked), expect_void_receiver{}); + ex::start(op); + } + TEST_CASE("ensure_started multiple values early", "[adaptors][ensure_started]") { bool called{false}; diff --git a/test/exec/test_split.cpp b/test/exec/test_split.cpp index 5e9353db9..24f959cef 100644 --- a/test/exec/test_split.cpp +++ b/test/exec/test_split.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ +#include #include #include #include @@ -118,6 +119,36 @@ namespace // The receiver will ensure that the right value is produced } + TEST_CASE("split preserves reference results", "[adaptors][split]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } value; + + auto source = exec::just_from( + [&](auto sink) noexcept + { + sink(value); + return ex::completion_signatures{}; + }); + auto split = exec::split(std::move(source)); + using split_t = decltype(split); + static_assert( + set_equivalent>, + ex::completion_signatures>); + + auto check_reference = ex::then([&](immovable &result) noexcept { CHECK(&result == &value); }); + auto op1 = ex::connect(split | check_reference, expect_void_receiver{}); + auto op2 = ex::connect(split | check_reference, expect_void_receiver{}); + ex::start(op1); + ex::start(op2); + } + TEST_CASE("split forwards errors", "[adaptors][split]") { SECTION("of exception_ptr type") @@ -137,11 +168,18 @@ namespace auto split = exec::split(ex::just_error(42)); using split_t = decltype(split); using error_t = ex::error_types_of_t, ex::__mmake_set>; - static_assert(ex::__mset_eq>); + static_assert(ex::__mset_eq>); auto op = ex::connect(split, expect_error_receiver{}); ex::start(op); } + + SECTION("from storing a potentially throwing result") + { + using split_t = decltype(exec::split(ex::just(potentially_throwing{}))); + using error_t = ex::error_types_of_t, ex::__mmake_set>; + static_assert(ex::__mset_eq>); + } } TEST_CASE("split forwards stop signal", "[adaptors][split]") @@ -536,7 +574,7 @@ namespace REQUIRE(v1 == 1); REQUIRE(v2 == 2); - REQUIRE(v3 == 1); + REQUIRE(v3 == 2); } TEST_CASE("split doesn't advertise completion scheduler", "[adaptors][split]") From 24f8765fe60c53b82eb3142a0539f7f507d6345b Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 14:42:51 -0400 Subject: [PATCH 14/15] exec::ignore_all_values: Reference Aware --- include/exec/sequence/ignore_all_values.hpp | 88 ++++++++----------- test/exec/sequence/test_ignore_all_values.cpp | 26 ++++++ 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/include/exec/sequence/ignore_all_values.hpp b/include/exec/sequence/ignore_all_values.hpp index 0ca0e9ddb..98469fba5 100644 --- a/include/exec/sequence/ignore_all_values.hpp +++ b/include/exec/sequence/ignore_all_values.hpp @@ -20,7 +20,7 @@ // include these after execution.hpp #include "../../stdexec/__detail/__senders.hpp" -#include "../../stdexec/__detail/__tuple.hpp" +#include "../../stdexec/__detail/__storage_for_completion_signatures.hpp" #include "../sender_for.hpp" #include "../sequence_senders.hpp" @@ -28,46 +28,20 @@ namespace experimental::execution { - template - concept __variant_emplaceable = requires(_Variant& __var, _Args&&... __args) { - __var.template emplace<_Type>(static_cast<_Args&&>(__args)...); - }; - - template - concept __nothrow_variant_emplaceable = requires(_Variant& __var, _Args&&... __args) { - { __var.template emplace<_Type>(static_cast<_Args&&>(__args)...) } noexcept; - }; - namespace __ignore_all_values { using namespace STDEXEC; - constexpr auto __complete_fn = - [](_Receiver&& __rcvr, - _Tag, - _Args&&... __args) noexcept - { - _Tag()(static_cast<_Receiver&&>(__rcvr), static_cast<_Args&&>(__args)...); - }; - - constexpr auto __visit_fn = - [](_Receiver&& __rcvr, _Tuple&& __tupl) noexcept - { - STDEXEC::__apply(__complete_fn, - static_cast<_Tuple&&>(__tupl), - static_cast<_Receiver&&>(__rcvr)); - }; - template struct __result_type { - template - void __emplace(_Args&&... __args) noexcept + template + void __arrive(_Tag __tag, _Args&&... __args) noexcept { int __expected = 0; if (__emplaced_.compare_exchange_strong(__expected, 1, __std::memory_order_relaxed)) { - __result_.template emplace<__decayed_tuple<_Args...>>(static_cast<_Args&&>(__args)...); + __result_.arrive(__tag, static_cast<_Args&&>(__args)...); __emplaced_.store(2, __std::memory_order_release); } } @@ -83,9 +57,12 @@ namespace experimental::execution { STDEXEC::set_value(static_cast<_Receiver&&>(__rcvr)); } - else if constexpr (STDEXEC::__mapply::value != 0) + else { - static_cast<_ResultsStorage&&>(__result_).__complete(__rcvr); + bool const __completed = static_cast<_ResultsStorage&&>(__result_).complete( + static_cast<_Receiver&&>(__rcvr)); + STDEXEC_ASSERT(__completed); + (void) __completed; } } @@ -114,26 +91,22 @@ namespace experimental::execution } template - requires __variant_emplaceable<_ResultsStorage, - __decayed_tuple, - set_error_t, - _Error> - && __callable + requires requires(_ResultsStorage& __storage, _Error&& __error) { + __storage.arrive(set_error, static_cast<_Error&&>(__error)); + } && __callable void set_error(_Error&& __error) noexcept { // store error and signal stop - __op_->__result_->__emplace(set_error_t(), static_cast<_Error&&>(__error)); + __op_->__result_->__arrive(set_error_t(), static_cast<_Error&&>(__error)); STDEXEC::set_stopped(static_cast<_ItemReceiver&&>(__op_->__rcvr_)); } void set_stopped() noexcept - requires __variant_emplaceable<_ResultsStorage, - __decayed_tuple, - set_stopped_t> + requires requires(_ResultsStorage& __storage) { __storage.arrive(set_stopped); } && __callable { // stop without error - __op_->__result_->__emplace(set_stopped_t()); + __op_->__result_->__arrive(set_stopped_t()); STDEXEC::set_stopped(static_cast<_ItemReceiver&&>(__op_->__rcvr_)); } @@ -251,19 +224,23 @@ namespace experimental::execution using __is_set_value_signature_t = __mbool<__same_as>>; + template + using __nonvalue_completion_signatures_t = + __mapply<__mremove_if<__q1<__is_set_value_signature_t>, __qq>, + _CompletionSignatures>; + // Storage for the non-set_value completions template - using __result_variant_t = - __mapply<__mremove_if<__q1<__is_set_value_signature_t>, __qq<__results_storage>>, - __sequence_completion_signatures_of_t<_Sender, _Env>>; + using __result_storage_t = storage_for_completion_signatures< + __nonvalue_completion_signatures_t<__sequence_completion_signatures_of_t<_Sender, _Env>>>; template struct __operation - : __operation_base<_Receiver, __result_variant_t<_Sender, env_of_t<_Receiver>>> + : __operation_base<_Receiver, __result_storage_t<_Sender, env_of_t<_Receiver>>> { - using __variant_t = __result_variant_t<_Sender, env_of_t<_Receiver>>; - using __base_t = __operation_base<_Receiver, __variant_t>; - using __receiver_t = __receiver<_Receiver, __variant_t>; + using __storage_t = __result_storage_t<_Sender, env_of_t<_Receiver>>; + using __base_t = __operation_base<_Receiver, __storage_t>; + using __receiver_t = __receiver<_Receiver, __storage_t>; explicit __operation(_Sender&& __sndr, _Receiver __rcvr) noexcept(__nothrow_subscribable<_Sender, __receiver_t>) @@ -317,7 +294,18 @@ namespace experimental::execution static consteval auto __get_completion_signatures() { static_assert(sender_for<_Sender, ignore_all_values_t>); - return __sequence_completion_signatures_of<__child_of<_Sender>, _Env...>(); + auto __completions = __sequence_completion_signatures_of<__child_of<_Sender>, _Env...>(); + STDEXEC_IF_OK(__completions) + { + using __storage_t = storage_for_completion_signatures< + __nonvalue_completion_signatures_t>; + auto __stored_completions = __storage_t::get_completion_signatures(); + STDEXEC_IF_OK(__stored_completions) + { + return STDEXEC::__concat_completion_signatures(completion_signatures{}, + __stored_completions); + } + } } static constexpr auto __connect = diff --git a/test/exec/sequence/test_ignore_all_values.cpp b/test/exec/sequence/test_ignore_all_values.cpp index 935774f30..ef2fbb217 100644 --- a/test/exec/sequence/test_ignore_all_values.cpp +++ b/test/exec/sequence/test_ignore_all_values.cpp @@ -15,6 +15,7 @@ * limitations under the License. */ +#include "exec/just_from.hpp" #include "exec/sequence/empty_sequence.hpp" #include "exec/sequence/ignore_all_values.hpp" @@ -65,6 +66,31 @@ namespace CHECK_FALSE(STDEXEC::sync_wait(sndr)); } + TEST_CASE("ignore_all_values - preserve error references", + "[sequence_senders][ignore_all_values]") + { + struct immovable + { + immovable() = default; + immovable(immovable const &) = delete; + immovable(immovable &&) = delete; + immovable &operator=(immovable const &) = delete; + immovable &operator=(immovable &&) = delete; + } error; + + auto source = exec::just_error_from([&](auto sink) noexcept { return sink(error); }); + auto sndr = exec::ignore_all_values(std::move(source)); + using sender_t = decltype(sndr); + using actual_t = STDEXEC::completion_signatures_of_t>; + using expected_t = STDEXEC::__mset; + STATIC_REQUIRE(STDEXEC::__mset_eq); + + auto recovered = std::move(sndr) + | STDEXEC::upon_error([&](immovable &actual) noexcept + { CHECK(&actual == &error); }); + CHECK(STDEXEC::sync_wait(std::move(recovered))); + } + #if !STDEXEC_NO_STDCPP_EXCEPTIONS() TEST_CASE("ignore_all_values - ignore just_error()", "[sequence_senders][ignore_all_values]") { From 990160d2a5cb4bb03f822bc98248e41c694ba763 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Thu, 30 Jul 2026 14:42:55 -0400 Subject: [PATCH 15/15] Remove __storage.hpp No longer used in favor of storage_for_completion_signatures (which is reference aware). --- include/stdexec/__detail/__storage.hpp | 86 -------------------------- include/stdexec/execution.hpp | 1 - 2 files changed, 87 deletions(-) delete mode 100644 include/stdexec/__detail/__storage.hpp diff --git a/include/stdexec/__detail/__storage.hpp b/include/stdexec/__detail/__storage.hpp deleted file mode 100644 index fa83cb490..000000000 --- a/include/stdexec/__detail/__storage.hpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2021-2025 NVIDIA Corporation - * - * Licensed under the Apache License Version 2.0 with LLVM Exceptions - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * https://llvm.org/LICENSE.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#pragma once - -#include "__execution_fwd.hpp" - -#include "__get_completion_signatures.hpp" -#include "__meta.hpp" -#include "__sender_concepts.hpp" -#include "__tuple.hpp" -#include "__variant.hpp" - -#include "__prologue.hpp" - -namespace STDEXEC -{ - namespace __detail - { - struct __applier - { - template - constexpr void operator()(_Receiver& __rcvr, _Tag, _Args&&... __args) noexcept - { - _Tag()(static_cast<_Receiver&&>(__rcvr), static_cast<_Args&&>(__args)...); - } - }; - - struct __visitor - { - template - constexpr void operator()(_Receiver& __rcvr, _Tuple&& __tupl) noexcept - { - __apply(__applier(), static_cast<_Tuple&&>(__tupl), __rcvr); - } - }; - - // Add storage for an exception_ptr if the result datums are not all nothrow - // decay-copyable. - template - using __tuples_for_t = - __if<__nothrow_decay_copyable_t<_Args...>, - __mlist<__decayed_tuple<_Tag, _Args...>>, - __mlist<__decayed_tuple<_Tag, _Args...>, __tuple>>; - } // namespace __detail - - // A variant type that is capable of storing the result datums of the specified - // completion signatures. - template - struct __results_storage - : __mcall<__mconcat<__qq<__uniqued_variant>>, - __mapply_q<__detail::__tuples_for_t, _Signatures>...> - { - constexpr __results_storage() noexcept - : __results_storage::__variant(__no_init) - {} - - template - STDEXEC_ATTRIBUTE(host, device) - constexpr STDEXEC_EXPLICIT_THIS_BEGIN(void __complete)(this _Self&& __self, - _Receiver& __rcvr) noexcept - { - __visit(__detail::__visitor(), static_cast<_Self&&>(__self), __rcvr); - } - STDEXEC_EXPLICIT_THIS_END(__complete) - }; - - template - requires sender_in<_CvSender, _Env> - using __storage_for_t = - __mapply_q<__results_storage, __completion_signatures_of_t<_CvSender, _Env>>; -} // namespace STDEXEC - -#include "__epilogue.hpp" diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index d686ebc2e..fe6070b84 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -56,7 +56,6 @@ #include "__detail/__starts_on.hpp" #include "__detail/__stopped_as_error.hpp" #include "__detail/__stopped_as_optional.hpp" -#include "__detail/__storage.hpp" #include "__detail/__storage_for_completion_signatures.hpp" #include "__detail/__submit.hpp" #include "__detail/__sync_wait.hpp"