From a09a11776d092835b84af272271731a4c72d1c35 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 23 May 2026 18:30:20 -0700 Subject: [PATCH 01/17] Start supporting get_completion_domain of a function --- include/exec/function.hpp | 93 ++++++++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 90375c8dd..7ef802677 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -17,6 +17,7 @@ #include "../stdexec/__detail/__completion_signatures.hpp" #include "../stdexec/__detail/__concepts.hpp" +#include "../stdexec/__detail/__domain.hpp" #include "../stdexec/__detail/__meta.hpp" #include "../stdexec/__detail/__read_env.hpp" #include "../stdexec/__detail/__receivers.hpp" @@ -55,6 +56,11 @@ // queries to pick the frame allocator from the environment without relying on TLS. namespace experimental::execution { + // for specifying required sender attributes in exec::function + template <_query::_query_signature... Sigs> + struct attrs + {}; + namespace __func { using namespace STDEXEC; @@ -192,7 +198,7 @@ namespace experimental::execution } }; - template + template class __function; //! the main implementation of the type-erasing sender function<...> @@ -206,8 +212,8 @@ namespace experimental::execution //! not, as appropriate //! //! \tparam _Args The argument types used to construct the erased sender - template - class __function<_Sigs, queries<_Queries...>, _Args...> + template + class __function<_Sigs, queries<_Queries...>, attrs<_Attrs...>, _Args...> { using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, queries<_Queries...>>>; @@ -342,6 +348,23 @@ namespace experimental::execution completion_signatures<__single_value_sig_t<_Return>, set_stopped_t()>, __eptr_completion_unless_t<__mbool<_NoExcept>>>>; + //! computes the set of get_completion_domain queries that must be supported by any + //! sender that might be erased by the corresponding function + //! + //! we should support get_completion_domain only if _Sigs contains a completion + //! of type Tag + //! + //! the query form should be + //! + //! default_domain(get_completion_domain_t, Env) + //! + //! where Env is the environment type we'll be synthesizing from _Queries + template + using __default_attrs = + __canonical_t), + default_domain(get_completion_domain_t), + default_domain(get_completion_domain_t)>>; + //! Map a variety of function<...> specifications into the canonical type-erased //! contract represented by the user-provided specification. //! @@ -362,48 +385,74 @@ namespace experimental::execution //! The order of Args... is obviously important, but Sigs... and Queries... are both //! canonicalized into a sorted and uniqued list to ensure order is irrelevant. template - struct __make_function; + class __make_function; template - struct __make_function<_Return(_Args...)> + class __make_function<_Return(_Args...)> { - using type = __function<__sigs_from_t<_Return, false>, queries<>, _Args...>; + using __sigs = __sigs_from_t<_Return, false>; + using __queries = queries<>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; template - struct __make_function<_Return(_Args...) noexcept> + class __make_function<_Return(_Args...) noexcept> { - using type = __function<__sigs_from_t<_Return, true>, queries<>, _Args...>; + using __sigs = __sigs_from_t<_Return, true>; + using __queries = queries<>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; template - struct __make_function> + class __make_function> { - using type = __function<__canonical_t>, queries<>, _Args...>; + using __sigs = __canonical_t>; + using __queries = queries<>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; template - struct __make_function<_Return(_Args...), queries<_Queries...>> + class __make_function<_Return(_Args...), queries<_Queries...>> { - using type = - __function<__sigs_from_t<_Return, false>, __canonical_t>, _Args...>; + using __sigs = __sigs_from_t<_Return, false>; + using __queries = __canonical_t>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; template - struct __make_function<_Return(_Args...) noexcept, queries<_Queries...>> + class __make_function<_Return(_Args...) noexcept, queries<_Queries...>> { - using type = - __function<__sigs_from_t<_Return, true>, __canonical_t>, _Args...>; + using __sigs = __sigs_from_t<_Return, true>; + using __queries = __canonical_t>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; template - struct __make_function, - queries<_Queries...>> + class __make_function, + queries<_Queries...>> { - using type = __function<__canonical_t>, - __canonical_t>, - _Args...>; + using __sigs = __canonical_t>; + using __queries = __canonical_t>; + using __attrs = __default_attrs<__sigs, __queries>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; }; } // namespace __func From 9a9c1dc56bdc2364be4e1499945e5235e9344bc2 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 23 May 2026 19:44:21 -0700 Subject: [PATCH 02/17] Start computing the default sender attributes I realized that the completion domains reported by a `function` will be receiver environment-independant, so I deleted `__default_attrs`'s `_Queries` parameter, and I've updated the default to be a function of the given completion signatures using `completion_signatures<...>::__transform_reduce`. --- include/exec/function.hpp | 51 ++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 7ef802677..aad96872c 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -348,6 +348,33 @@ namespace experimental::execution completion_signatures<__single_value_sig_t<_Return>, set_stopped_t()>, __eptr_completion_unless_t<__mbool<_NoExcept>>>>; + //! maps a completion signature to the default completion domain query + struct __domain_query_from_sig + { + template + consteval auto operator()(_Tag (*)(_Args...)) const noexcept // + -> default_domain (*)(get_completion_domain_t<_Tag>) + { + return nullptr; + } + }; + + //! maps a pack of domain queries produced by __domain_query_from_sig to the + //! corresponding attrs<_Attrs...> type + class __attrs_from_domain_queries + { + template + using __query_sig = default_domain (*)(get_completion_domain_t<_Tag>); + + public: + template + consteval auto operator()(__query_sig<_Tag>...) const noexcept // + -> __canonical_t)...>> + { + return {}; + } + }; + //! computes the set of get_completion_domain queries that must be supported by any //! sender that might be erased by the corresponding function //! @@ -356,14 +383,10 @@ namespace experimental::execution //! //! the query form should be //! - //! default_domain(get_completion_domain_t, Env) - //! - //! where Env is the environment type we'll be synthesizing from _Queries - template - using __default_attrs = - __canonical_t), - default_domain(get_completion_domain_t), - default_domain(get_completion_domain_t)>>; + //! default_domain(get_completion_domain_t) + template + using __default_attrs = decltype(_Sigs::__transform_reduce(__domain_query_from_sig(), + __attrs_from_domain_queries())); //! Map a variety of function<...> specifications into the canonical type-erased //! contract represented by the user-provided specification. @@ -392,7 +415,7 @@ namespace experimental::execution { using __sigs = __sigs_from_t<_Return, false>; using __queries = queries<>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; @@ -403,7 +426,7 @@ namespace experimental::execution { using __sigs = __sigs_from_t<_Return, true>; using __queries = queries<>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; @@ -414,7 +437,7 @@ namespace experimental::execution { using __sigs = __canonical_t>; using __queries = queries<>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; @@ -425,7 +448,7 @@ namespace experimental::execution { using __sigs = __sigs_from_t<_Return, false>; using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; @@ -436,7 +459,7 @@ namespace experimental::execution { using __sigs = __sigs_from_t<_Return, true>; using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; @@ -449,7 +472,7 @@ namespace experimental::execution { using __sigs = __canonical_t>; using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs, __queries>; + using __attrs = __default_attrs<__sigs>; public: using type = __function<__sigs, __queries, __attrs, _Args...>; From fad582a6003fa55ef2d947c431de6ce2839611b2 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 23 May 2026 20:05:02 -0700 Subject: [PATCH 03/17] Maybe implement function's get_env It build and the existing tests pass, but I don't know if it works. --- include/exec/function.hpp | 45 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index aad96872c..99e4cf877 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -198,6 +198,41 @@ namespace experimental::execution } }; + template + struct __make_domain + {}; + + template + struct __make_domain<_Tag, _Domain(get_completion_domain_t<_Tag>)> + { + constexpr _Domain operator()() const noexcept + { + return _Domain(); + } + }; + + template + inline constexpr auto __get_completion_domain = + __first_callable<__make_domain<_Tag, _Attrs>...>(); + + template + struct __attrs + { + template + constexpr auto query(get_completion_domain_t<>, _Env &&...) const noexcept + -> decltype(__get_completion_domain) + { + return __get_completion_domain(); + } + + template + constexpr auto query(get_completion_domain_t<_Tag>, _Env &&...) const noexcept + -> decltype(__get_completion_domain<_Tag, _Attrs...>) + { + return __get_completion_domain<_Tag, _Attrs...>(); + } + }; + template class __function; @@ -226,8 +261,9 @@ namespace experimental::execution -> _any::_any_opstate_base { auto &__make_sender = *__std::start_lifetime_as<_Factory>(__storage); - using __alloc_t = decltype(__choose_frame_allocator(get_env(__rcvr))); - auto __alloc = __frame_allocator_t<__alloc_t>(__choose_frame_allocator(get_env(__rcvr))); + using __alloc_t = decltype(__choose_frame_allocator(STDEXEC::get_env(__rcvr))); + auto __alloc = __frame_allocator_t<__alloc_t>( + __choose_frame_allocator(STDEXEC::get_env(__rcvr))); return _any::_any_opstate_base(__in_place_from, std::allocator_arg, __alloc, @@ -285,6 +321,11 @@ namespace experimental::execution return _Sigs(); } + constexpr __attrs<_Attrs...> get_env() const noexcept + { + return {}; + } + template constexpr auto connect(_Receiver __rcvr) && // -> __opstate_t<_Receiver> From 38d91706507f92833250b0032a12c098fdadf34c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 24 May 2026 07:51:54 -0700 Subject: [PATCH 04/17] Add tests for get_completion_domain of a function The tests confirm that we only report a completion domain on the channels we might complete on, and I had to fix some bugs in `function.hpp` to make the tests pass. --- include/exec/function.hpp | 16 ++++---- test/exec/test_function.cpp | 78 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 99e4cf877..f9683786d 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -211,6 +211,13 @@ namespace experimental::execution } }; + //! get_completion_domain<> is a special case; its type parameter is void + //! and it's equivalent to get_completion_domain. + template + struct __make_domain)> + : __make_domain)> + {}; + template inline constexpr auto __get_completion_domain = __first_callable<__make_domain<_Tag, _Attrs>...>(); @@ -218,16 +225,9 @@ namespace experimental::execution template struct __attrs { - template - constexpr auto query(get_completion_domain_t<>, _Env &&...) const noexcept - -> decltype(__get_completion_domain) - { - return __get_completion_domain(); - } - template constexpr auto query(get_completion_domain_t<_Tag>, _Env &&...) const noexcept - -> decltype(__get_completion_domain<_Tag, _Attrs...>) + -> decltype(__get_completion_domain<_Tag, _Attrs...>()) { return __get_completion_domain<_Tag, _Attrs...>(); } diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 6283df70e..2227bef02 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -411,4 +411,82 @@ namespace STATIC_REQUIRE(std::assignable_from); } } + + struct none_such + {}; + + template + inline constexpr auto get_completion_domain = + ex::__first_callable{ex::get_completion_domain, ex::__always{none_such()}}; + + TEST_CASE("function reports a default completion domain by default", "[types][function]") + { + SECTION("throwing function reports a completion domain for all three channels") + { + exec::function fn(ex::just); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("no-throw function reports a completion domain for value and stop channels only") + { + exec::function fn(ex::just); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("infallible function reports a completion domain for value channel only") + { + exec::function> fn(ex::just); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("just_error function reports a completion domain for error channel only") + { + exec::function> fn( + 42, + ex::just_error); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("just_stopped function reports a completion domain for stop channel only") + { + exec::function> fn( + ex::just_stopped); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + } } // namespace From 93b55fe8dd99f2b551032beb3182bc31f5b25ac3 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 24 May 2026 08:44:37 -0700 Subject: [PATCH 05/17] Rename the implementation details of __func::__attrs This diff just renames the bits and pieces that compute the completion domain of a `function`. --- include/exec/function.hpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index f9683786d..97f9be5bf 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -199,11 +199,11 @@ namespace experimental::execution }; template - struct __make_domain + struct __make_domain_impl {}; template - struct __make_domain<_Tag, _Domain(get_completion_domain_t<_Tag>)> + struct __make_domain_impl<_Tag, _Domain(get_completion_domain_t<_Tag>)> { constexpr _Domain operator()() const noexcept { @@ -214,22 +214,21 @@ namespace experimental::execution //! get_completion_domain<> is a special case; its type parameter is void //! and it's equivalent to get_completion_domain. template - struct __make_domain)> - : __make_domain)> + struct __make_domain_impl)> + : __make_domain_impl)> {}; template - inline constexpr auto __get_completion_domain = - __first_callable<__make_domain<_Tag, _Attrs>...>(); + inline constexpr auto __make_domain = __first_callable<__make_domain_impl<_Tag, _Attrs>...>(); template struct __attrs { template constexpr auto query(get_completion_domain_t<_Tag>, _Env &&...) const noexcept - -> decltype(__get_completion_domain<_Tag, _Attrs...>()) + -> decltype(__make_domain<_Tag, _Attrs...>()) { - return __get_completion_domain<_Tag, _Attrs...>(); + return __make_domain<_Tag, _Attrs...>(); } }; From caa404ec0d8ec1004c91a727caf4411ee82916b0 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 24 May 2026 08:46:04 -0700 Subject: [PATCH 06/17] Restrict function's ctor based on the erased sender's completion domains This is a partial implementation of constraining `function`'s constructor to accept only factories that produce senders that completion in the expected domain. It's partial because: - we're currently checking for a "matching" domain with `__same_as`, but we should be checking with some function of `common_domain`; and - there's no way to specify any expected domain other than `default_domain`. The current tests pass, which means we correctly allow factories that produce senders that completion in the default domain. We need tests that show we reject senders that *don't* complete in the default domain, and we need a way to use and validate the use of non-default domains. --- include/exec/function.hpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 97f9be5bf..cf9a89f04 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -232,6 +232,28 @@ namespace experimental::execution } }; + template + using __completion_domain_t = __call_result_or_t< + get_completion_domain_t<_Tag>, + __call_result_or_t, indeterminate_domain<>, _Attrs>, + _Attrs, + _Env const &...>; + + template + concept __completion_domain_matches = + __same_as<__completion_domain_t<_Tag, _ActualAttrs, _Env...>, + __completion_domain_t<_Tag, _ExpectedAttrs, _Env...>>; + + template + concept __completion_domains_match_impl = + __completion_domain_matches + && __completion_domain_matches + && __completion_domain_matches; + + template + concept __completion_domains_match = + __completion_domains_match_impl, env_of_t<_Expected>, _Env...>; + template class __function; @@ -298,6 +320,9 @@ namespace experimental::execution && (STDEXEC_IS_TRIVIALLY_COPYABLE(_Factory)) // && (sizeof(_Factory) <= sizeof(__make_sender_)) // && sender_to<__invoke_result_t<_Factory, _Args...>, __receiver_t> + && __completion_domains_match<__invoke_result_t<_Factory, _Args...>, + __function, + env_of_t> constexpr explicit __function(_Args &&...__args, _Factory __factory) noexcept(__nothrow_move_constructible<_Args...>) : __args_(static_cast<_Args &&>(__args)...) From f68280e15d63359697aac11ca5c85a01453fdfed Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 24 May 2026 10:52:47 -0700 Subject: [PATCH 07/17] First test confirming non-default domain support The implementation is incomplete because we check for identical domains but should be checking for compatible domains with `common_domain`. --- include/exec/function.hpp | 39 ++++++++++++++++++++++++++++--------- test/exec/test_function.cpp | 29 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index cf9a89f04..2c7d633ea 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -112,13 +112,13 @@ namespace experimental::execution {} constexpr auto get_env() const noexcept // - -> __join_env_t<__prop_t, env_of_t<_Receiver>> + -> __join_env_t<__prop_t const &, env_of_t<_Receiver>> { return __env::__join(*__env_, STDEXEC::get_env(*static_cast<_Receiver const *>(this))); } private: - __prop_t *__env_; + __prop_t const *__env_; }; template @@ -203,7 +203,7 @@ namespace experimental::execution {}; template - struct __make_domain_impl<_Tag, _Domain(get_completion_domain_t<_Tag>)> + struct __make_domain_impl<_Tag, _Domain(get_completion_domain_t<_Tag>) noexcept> { constexpr _Domain operator()() const noexcept { @@ -214,8 +214,15 @@ namespace experimental::execution //! get_completion_domain<> is a special case; its type parameter is void //! and it's equivalent to get_completion_domain. template - struct __make_domain_impl)> - : __make_domain_impl)> + struct __make_domain_impl) noexcept> + : __make_domain_impl) noexcept> + {}; + + //! get_completion_domain ought to be no-throw, so make it optional to specify + //! noexcept on the signature provided with attrs<...> + template + struct __make_domain_impl<_Tag1, _Domain(get_completion_domain_t<_Tag2>)> + : __make_domain_impl<_Tag1, _Domain(get_completion_domain_t<_Tag2>) noexcept> {}; template @@ -322,7 +329,7 @@ namespace experimental::execution && sender_to<__invoke_result_t<_Factory, _Args...>, __receiver_t> && __completion_domains_match<__invoke_result_t<_Factory, _Args...>, __function, - env_of_t> + env_of_t<__receiver_t>> constexpr explicit __function(_Args &&...__args, _Factory __factory) noexcept(__nothrow_move_constructible<_Args...>) : __args_(static_cast<_Args &&>(__args)...) @@ -418,7 +425,7 @@ namespace experimental::execution { template consteval auto operator()(_Tag (*)(_Args...)) const noexcept // - -> default_domain (*)(get_completion_domain_t<_Tag>) + -> default_domain (*)(get_completion_domain_t<_Tag>) noexcept { return nullptr; } @@ -429,12 +436,12 @@ namespace experimental::execution class __attrs_from_domain_queries { template - using __query_sig = default_domain (*)(get_completion_domain_t<_Tag>); + using __query_sig = default_domain (*)(get_completion_domain_t<_Tag>) noexcept; public: template consteval auto operator()(__query_sig<_Tag>...) const noexcept // - -> __canonical_t)...>> + -> __canonical_t) noexcept...>> { return {}; } @@ -542,6 +549,20 @@ namespace experimental::execution public: using type = __function<__sigs, __queries, __attrs, _Args...>; }; + + template + class __make_function, + queries<_Queries...>, + attrs<_Attrs...>> + { + using __sigs = __canonical_t>; + using __queries = __canonical_t>; + using __attrs = __canonical_t>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; + }; } // namespace __func //! the user-facing interface to exec::function that supports several different diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 2227bef02..0838b058e 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -489,4 +489,33 @@ namespace STATIC_REQUIRE(std::same_as); } } + + struct domain : ex::default_domain + {}; + + TEST_CASE("function's constructor is constrained based on the common domain", "[types][function]") + { + using queries = exec::queries; + + SECTION("the constraint applies to set_value") + { + using function = + exec::function, + queries, + exec::attrs) noexcept>>; + + STATIC_REQUIRE(std::constructible_from); + + function fn(ex::just); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + } } // namespace From efe031eca38d42338c641364ab2c1cba4e81bdb2 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 1 Jun 2026 14:09:30 -0700 Subject: [PATCH 08/17] Modify domain-matching to use common-domain This diff modifies the `function` test that validates the domain-related constraints on `function`'s constructor. The old test required that `function` only erase senders with completion domains identitcal to those specified in the `function`'s type parameters; the intended design is that the erased sender's completion domains be _derived from_ the advertised domains. The new test requires the correct relationship, which forced a change in `function` to make the new test pass. --- include/exec/function.hpp | 8 +++++-- test/exec/test_function.cpp | 44 ++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 2c7d633ea..0fbfc9955 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -246,10 +246,14 @@ namespace experimental::execution _Attrs, _Env const &...>; + template + concept __completion_domain_matches_impl = + __same_as<_ExpectedDomain, __common_domain_t<_ActualDomain, _ExpectedDomain>>; + template concept __completion_domain_matches = - __same_as<__completion_domain_t<_Tag, _ActualAttrs, _Env...>, - __completion_domain_t<_Tag, _ExpectedAttrs, _Env...>>; + __completion_domain_matches_impl<__completion_domain_t<_Tag, _ActualAttrs, _Env...>, + __completion_domain_t<_Tag, _ExpectedAttrs, _Env...>>; template concept __completion_domains_match_impl = diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 0838b058e..13160fc70 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -500,10 +500,7 @@ namespace SECTION("the constraint applies to set_value") { using function = - exec::function, - queries, - exec::attrs) noexcept>>; + exec::function, queries>; STATIC_REQUIRE(std::constructible_from); @@ -513,9 +510,46 @@ namespace auto error_domain = get_completion_domain(attrs); auto stop_domain = get_completion_domain(attrs); - STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); STATIC_REQUIRE(std::same_as); STATIC_REQUIRE(std::same_as); } + + SECTION("the constraint applies to set_error") + { + using function = exec::function, + queries>; + + STATIC_REQUIRE(std::constructible_from); + + function fn(42, ex::just_error); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("the constraint applies to set_stopped") + { + using function = + exec::function, queries>; + + STATIC_REQUIRE(std::constructible_from); + + function fn(ex::just_stopped); + auto attrs = ex::get_env(fn); + auto value_domain = get_completion_domain(attrs); + auto error_domain = get_completion_domain(attrs); + auto stop_domain = get_completion_domain(attrs); + + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } } } // namespace From c98a15aea8b690ffedd011d0b61ab5efc4100abe Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 3 Jun 2026 14:29:41 -0700 Subject: [PATCH 09/17] Add negative tests for function's ctor constraints Add tests to make sure that `function`'s constructor rejects senders whose completion domain is unsupported by the `function`'s declared requirements. --- test/exec/test_function.cpp | 176 ++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 13160fc70..0d456b6f5 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -552,4 +552,180 @@ namespace STATIC_REQUIRE(std::same_as); } } + + //! a sender that wraps another, and tells the inner sender it's starting in domain + //! Domain, with the expectation that saying so will influence the inner sender's + //! completion domain, which this sender forwards out through its attributes + template + class inject_domain + { + //! our inner sender will be connected to a receiver that prepends this type to + //! the outer receiver's environment, ensuring that the inner sender believes it's + //! running in the specified domain + struct env + { + constexpr Domain query(ex::get_domain_t) const noexcept + { + return Domain{}; + } + }; + + //! advertise that we complete on whichever domain the inner sender completes on + //! + //! as used, this is expected to be Domain because the just family of senders + //! "complete where they start" so our receiver environment should ensure our + //! desired result + struct attrs + { + template + using domain_t = + ex::__call_result_t, ex::env_of_t, env>; + + template + constexpr auto query(ex::get_completion_domain_t, Env const &...) const noexcept // + -> domain_t + { + return domain_t{}; + } + }; + + template + class opstate + { + struct receiver + { + using receiver_concept = ex::receiver_tag; + + template + constexpr void set_value(T &&...t) && noexcept + { + ex::set_value(std::move(self_->rcvr_), std::forward(t)...); + } + + template + constexpr void set_error(E &&e) && noexcept + { + ex::set_error(std::move(self_->rcvr_), std::forward(e)); + } + + constexpr void set_stopped() && noexcept + { + ex::set_stopped(std::move(self_->rcvr_)); + } + + constexpr auto get_env() const noexcept // + -> ex::__join_env_t> + { + return ex::__env::__join(env{}, ex::get_env(self_->rcvr_)); + } + + opstate *self_; + }; + + Receiver rcvr_; + ex::connect_result_t op_; + + public: + using operation_state_concept = ex::operation_state_tag; + + constexpr explicit opstate(Sender sndr, Receiver rcvr) noexcept + : rcvr_(std::move(rcvr)) + , op_(ex::connect(std::move(sndr), receiver(this))) + {} + + void start() & noexcept + { + ex::start(op_); + } + }; + + Sender sndr; + + public: + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() // + -> decltype(ex::get_completion_signatures()) + { + return ex::get_completion_signatures(); + } + + explicit inject_domain(Sender sndr, Domain) noexcept + : sndr(std::move(sndr)) + {} + + constexpr attrs get_env() const noexcept + { + return {}; + } + + template + constexpr opstate connect(Receiver rcvr) && noexcept + { + return opstate(std::move(sndr), std::move(rcvr)); + } + }; + + template + using custom_domain_for = exec::attrs)>; + + TEST_CASE("function can't be constructed with a sender that completes in the wrong domain", + "[types][function]") + { + //! convert the given sender factory to a factory that wraps the given factory's + //! result in an inject_domain sender + auto change_domain = [](auto &factory) noexcept + { + return [&](auto... values) noexcept + { + return inject_domain(factory(std::move(values)...), domain{}); + }; + }; + + SECTION("the constraint applies to set_value") + { + using function = exec::function, + exec::queries<>, + custom_domain_for>; + + STATIC_REQUIRE(!std::constructible_from); + + // double check that it *would* work if the sender reported a custom domain + using custom_just_t = decltype(change_domain(ex::just)); + + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("the constraint applies to set_error") + { + using function = exec::function, + exec::queries<>, + custom_domain_for>; + + STATIC_REQUIRE(!std::constructible_from); + + // double check that it *would* work if the sender reported a custom domain + using custom_just_error_t = decltype(change_domain(ex::just_error)); + + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("the constraint applies to set_stopped") + { + using function = exec::function, + exec::queries<>, + custom_domain_for>; + + STATIC_REQUIRE(!std::constructible_from); + + // double check that it *would* work if the sender reported a custom domain + using custom_just_stopped_t = decltype(change_domain(ex::just_stopped)); + + STATIC_REQUIRE(std::constructible_from); + } + } } // namespace From 9a22618798fdc2883a4cc1116d072a062f644216 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 4 Jun 2026 11:02:34 -0700 Subject: [PATCH 10/17] Add tests demanding new __make_function overloads Add new test cases that require new "overloads" of `__make_function` to allow specifying specializations of `function` with various combinations of required sender attributes and the attendant overloads. --- include/exec/function.hpp | 42 +++++++++++++- test/exec/test_function.cpp | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 0fbfc9955..e64d2f687 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -472,7 +472,8 @@ namespace experimental::execution //! function< //! sender_tag(Args...), //! completion_signatures, - //! queries> + //! queries, + //! attrs> //! //! where: //! - Args... is the type-erased sender factory's parameter list @@ -480,9 +481,11 @@ namespace experimental::execution //! to advertise //! - Queries... is the set of queries that the eventual receiver's environment must //! support + //! - Attrs... is the set of attributes the type-erased sender must report; only + //! supports the specification of the sender's completion domains //! - //! The order of Args... is obviously important, but Sigs... and Queries... are both - //! canonicalized into a sorted and uniqued list to ensure order is irrelevant. + //! The order of Args... is obviously important, but Sigs..., Queries..., and Attrs... + //! are all canonicalized into a sorted and uniqued list to ensure order is irrelevant. template class __make_function; @@ -554,6 +557,39 @@ namespace experimental::execution using type = __function<__sigs, __queries, __attrs, _Args...>; }; + template + class __make_function<_Return(_Args...), attrs<_Attrs...>> + { + using __sigs = __sigs_from_t<_Return, false>; + using __queries = queries<>; + using __attrs = __canonical_t>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; + }; + + template + class __make_function<_Return(_Args...) noexcept, attrs<_Attrs...>> + { + using __sigs = __sigs_from_t<_Return, true>; + using __queries = queries<>; + using __attrs = __canonical_t>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; + }; + + template + class __make_function, attrs<_Attrs...>> + { + using __sigs = __canonical_t>; + using __queries = queries<>; + using __attrs = __canonical_t>; + + public: + using type = __function<__sigs, __queries, __attrs, _Args...>; + }; + template class __make_function, diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 0d456b6f5..966e04a35 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -27,6 +27,73 @@ namespace ex = STDEXEC; namespace { + template + struct domain_sender_t + { + template + class sender + { + struct attrs + { + template + constexpr Domain query(ex::get_completion_domain_t, Env const &...) const noexcept + { + return {}; + } + }; + + template + struct opstate + { + using operation_state_concept = ex::operation_state_tag; + + void start() & noexcept + { + ex::__apply(Channel(), std::move(values), std::move(rcvr)); + } + + Receiver rcvr; + ex::__tuple values; + }; + + ex::__tuple values_; + + public: + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() noexcept // + -> ex::completion_signatures + { + return {}; + } + + constexpr attrs get_env() const noexcept + { + return {}; + } + + constexpr explicit sender(Values... values) noexcept + : values_(values...) + {} + + template + opstate connect(Receiver rcvr) && noexcept + { + return opstate(std::move(rcvr), std::move(values_)); + } + }; + + template + constexpr sender operator()(Values... values) const noexcept + { + return sender(std::move(values)...); + } + }; + + template + inline constexpr domain_sender_t domain_sender{}; + TEST_CASE("exec::function is constructible", "[types][function]") { SECTION("void()") @@ -90,6 +157,49 @@ namespace sndr(5, [](int) noexcept { return ex::just(); }); STATIC_REQUIRE(STDEXEC::sender); } + + struct domain + {}; + + SECTION("void() with attrs but no queries") + { + exec::function)>> + sndr(domain_sender); + + STATIC_REQUIRE(ex::sender); + } + + SECTION("void() noexcept with attrs but no queries") + { + exec::function)>> + sndr(domain_sender); + + STATIC_REQUIRE(ex::sender); + } + + SECTION("sender_tag(int) with set_value_t(int) and attrs but no queries") + { + // TODO: validate that the required completion domains "match" the permitted completions + exec::function, + exec::attrs)>> + sndr(42, domain_sender); + + STATIC_REQUIRE(ex::sender); + } + + SECTION("sender_tag(int) with set_error_t(int), attrs, and trivial queries") + { + // TODO: validate that the required completion domains "match" the permitted completions + exec::function, + exec::queries<>, + exec::attrs)>> + sndr(42, domain_sender); + + STATIC_REQUIRE(ex::sender); + } } TEST_CASE("exec::function is connectable", "[types][function]") From 7153b36ff856c4075821f9ba3accc6fb13cf483f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 4 Jun 2026 11:06:57 -0700 Subject: [PATCH 11/17] Simplify function's completion domain tests Delete the `inject_domain` nonsense and just use `domain_sender`. --- test/exec/test_function.cpp | 139 ++---------------------------------- 1 file changed, 5 insertions(+), 134 deletions(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 966e04a35..8936b00b1 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -194,7 +194,7 @@ namespace // TODO: validate that the required completion domains "match" the permitted completions exec::function, - exec::queries<>, + exec::queries<>, exec::attrs)>> sndr(42, domain_sender); @@ -663,136 +663,12 @@ namespace } } - //! a sender that wraps another, and tells the inner sender it's starting in domain - //! Domain, with the expectation that saying so will influence the inner sender's - //! completion domain, which this sender forwards out through its attributes - template - class inject_domain - { - //! our inner sender will be connected to a receiver that prepends this type to - //! the outer receiver's environment, ensuring that the inner sender believes it's - //! running in the specified domain - struct env - { - constexpr Domain query(ex::get_domain_t) const noexcept - { - return Domain{}; - } - }; - - //! advertise that we complete on whichever domain the inner sender completes on - //! - //! as used, this is expected to be Domain because the just family of senders - //! "complete where they start" so our receiver environment should ensure our - //! desired result - struct attrs - { - template - using domain_t = - ex::__call_result_t, ex::env_of_t, env>; - - template - constexpr auto query(ex::get_completion_domain_t, Env const &...) const noexcept // - -> domain_t - { - return domain_t{}; - } - }; - - template - class opstate - { - struct receiver - { - using receiver_concept = ex::receiver_tag; - - template - constexpr void set_value(T &&...t) && noexcept - { - ex::set_value(std::move(self_->rcvr_), std::forward(t)...); - } - - template - constexpr void set_error(E &&e) && noexcept - { - ex::set_error(std::move(self_->rcvr_), std::forward(e)); - } - - constexpr void set_stopped() && noexcept - { - ex::set_stopped(std::move(self_->rcvr_)); - } - - constexpr auto get_env() const noexcept // - -> ex::__join_env_t> - { - return ex::__env::__join(env{}, ex::get_env(self_->rcvr_)); - } - - opstate *self_; - }; - - Receiver rcvr_; - ex::connect_result_t op_; - - public: - using operation_state_concept = ex::operation_state_tag; - - constexpr explicit opstate(Sender sndr, Receiver rcvr) noexcept - : rcvr_(std::move(rcvr)) - , op_(ex::connect(std::move(sndr), receiver(this))) - {} - - void start() & noexcept - { - ex::start(op_); - } - }; - - Sender sndr; - - public: - using sender_concept = ex::sender_tag; - - template - static consteval auto get_completion_signatures() // - -> decltype(ex::get_completion_signatures()) - { - return ex::get_completion_signatures(); - } - - explicit inject_domain(Sender sndr, Domain) noexcept - : sndr(std::move(sndr)) - {} - - constexpr attrs get_env() const noexcept - { - return {}; - } - - template - constexpr opstate connect(Receiver rcvr) && noexcept - { - return opstate(std::move(sndr), std::move(rcvr)); - } - }; - template using custom_domain_for = exec::attrs)>; TEST_CASE("function can't be constructed with a sender that completes in the wrong domain", "[types][function]") { - //! convert the given sender factory to a factory that wraps the given factory's - //! result in an inject_domain sender - auto change_domain = [](auto &factory) noexcept - { - return [&](auto... values) noexcept - { - return inject_domain(factory(std::move(values)...), domain{}); - }; - }; - SECTION("the constraint applies to set_value") { using function = exec::function); // double check that it *would* work if the sender reported a custom domain - using custom_just_t = decltype(change_domain(ex::just)); - - STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from>); } SECTION("the constraint applies to set_error") @@ -818,9 +692,8 @@ namespace STATIC_REQUIRE(!std::constructible_from); // double check that it *would* work if the sender reported a custom domain - using custom_just_error_t = decltype(change_domain(ex::just_error)); - - STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE( + std::constructible_from>); } SECTION("the constraint applies to set_stopped") @@ -833,9 +706,7 @@ namespace STATIC_REQUIRE(!std::constructible_from); // double check that it *would* work if the sender reported a custom domain - using custom_just_stopped_t = decltype(change_domain(ex::just_stopped)); - - STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from>); } } } // namespace From bd1d026a03fc1dc82be43630298e40d92ebcaac8 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 4 Jun 2026 14:58:46 -0700 Subject: [PATCH 12/17] Simplify the declaration of __function a bit I'm about to add constraints to the `__function` class template declaration to make sure the `_Attrs` type parameter only requires completion domains on completion channels that are advertised as posible by the `_Sigs` type parameter. I'm expecting this to be easier, or at least less verbose, if the `__function` declaration doesn't expand the type lists in its parameter list. Also, this makes the definition of `__function` less verbose. --- include/exec/function.hpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index e64d2f687..d4256f7fd 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -228,8 +228,11 @@ namespace experimental::execution template inline constexpr auto __make_domain = __first_callable<__make_domain_impl<_Tag, _Attrs>...>(); + template + struct __attrs; + template - struct __attrs + struct __attrs> { template constexpr auto query(get_completion_domain_t<_Tag>, _Env &&...) const noexcept @@ -265,8 +268,17 @@ namespace experimental::execution concept __completion_domains_match = __completion_domains_match_impl, env_of_t<_Expected>, _Env...>; - template - class __function; + template + struct __check_queries; + + template + struct __check_queries, _Env...> + { + using type = __mfind_error<_any::_check_query_t<_Queries, _Env...>...>; + }; + + template + using __check_queries_t = __check_queries<_Queries, _Env...>::type; //! the main implementation of the type-erasing sender function<...> // @@ -279,13 +291,13 @@ namespace experimental::execution //! not, as appropriate //! //! \tparam _Args The argument types used to construct the erased sender - template - class __function<_Sigs, queries<_Queries...>, attrs<_Attrs...>, _Args...> + template + class __function { - using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, queries<_Queries...>>>; + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; template - using __opstate_t = __opstate<_Receiver, _Sigs, queries<_Queries...>>; + using __opstate_t = __opstate<_Receiver, _Sigs, _Queries>; template static constexpr auto @@ -349,14 +361,13 @@ namespace experimental::execution { static_assert(__decays_to_derived_from<_Self, __function>); //! throw if _Env does not contain the queries needed to type-erase the receiver: - using __check_queries_t = __mfind_error<_any::_check_query_t<_Queries, _Env...>...>; - if constexpr (__merror<__check_queries_t>) - return __throw_compile_time_error(__check_queries_t()); + if constexpr (__merror<__check_queries_t<_Queries, _Env...>>) + return __throw_compile_time_error(__check_queries_t<_Queries, _Env...>()); else return _Sigs(); } - constexpr __attrs<_Attrs...> get_env() const noexcept + constexpr __attrs<_Attrs> get_env() const noexcept { return {}; } From 1cb83531018cdddf259dbfaaeb7567b005e0751d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 4 Jun 2026 15:53:30 -0700 Subject: [PATCH 13/17] Statically assert that __function's type parameters are sensible Add some `static_assert`s that validate that the type parameters that are expected to be type lists of a certain kind are actually type lists of the expected kind. --- include/exec/function.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d4256f7fd..3aa30821b 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -294,6 +294,13 @@ namespace experimental::execution template class __function { + // check these with asserts rather than requires because the only way to violate + // them is to circumvent the exec::function alias template so any violation is + // a user hitting themselves + static_assert(__is_instance_of<_Sigs, completion_signatures>); + static_assert(__is_instance_of<_Queries, queries>); + static_assert(__is_instance_of<_Attrs, attrs>); + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; template From 562695c13bfa27338860aaea9a54ca8608fed0d8 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 5 Jun 2026 08:39:40 -0700 Subject: [PATCH 14/17] Constrain combinations of completion signatures and domains It's UB for a sender's attributes to advertise a completion domain for a completion channel upon which it will never complete. Constrain `function` to ensure we never declare a `function` that exhibits such UB, and add tests confirming it. --- include/exec/function.hpp | 49 +++++++++++++++++++++++++ test/exec/test_function.cpp | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 3aa30821b..f0ba12fd4 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -280,6 +280,46 @@ namespace experimental::execution template using __check_queries_t = __check_queries<_Queries, _Env...>::type; + template + struct __get_completion_domain_tag; + + template + struct __get_completion_domain_tag<_Domain(get_completion_domain_t<_Tag>)> + { + using type = _Tag; + }; + + template + struct __get_completion_domain_tag<_Domain(get_completion_domain_t<>)> + { + using type = set_value_t; + }; + + template + struct __get_completion_domain_tag<_Domain(get_completion_domain_t<_Tag>) noexcept> + : __get_completion_domain_tag<_Domain(get_completion_domain_t<_Tag>)> + {}; + + template + using __get_completion_domain_tag_t = __get_completion_domain_tag<_Attr>::type; + + template + inline constexpr bool __has_completion_domain = false; + + template + requires __one_of<_Tag, __get_completion_domain_tag_t<_Attrs>...> + inline constexpr bool __has_completion_domain, _Tag> = true; + + //! it is undefined behaviour for a sender to advertise a completion domain for a + //! completion channel that it never completes on so make sure there are no + //! completion domains required by _Attrs that correspond to completion channels + //! not advertised as possible by _Sigs + template + concept __completion_signatures_and_domains_are_compatible = + ((!__has_completion_domain<_Attrs, set_value_t>) || _Sigs::__count(set_value) > 0) // + && ((!__has_completion_domain<_Attrs, set_error_t>) || _Sigs::__count(set_error) > 0) // + && ((!__has_completion_domain<_Attrs, set_stopped_t>) || _Sigs::__count(set_stopped) > 0); + //! the main implementation of the type-erasing sender function<...> // //! \tparam _Sigs The supported completion signatures @@ -300,6 +340,7 @@ namespace experimental::execution static_assert(__is_instance_of<_Sigs, completion_signatures>); static_assert(__is_instance_of<_Queries, queries>); static_assert(__is_instance_of<_Attrs, attrs>); + static_assert(__completion_signatures_and_domains_are_compatible<_Sigs, _Attrs>); using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; @@ -576,6 +617,8 @@ namespace experimental::execution }; template + requires __completion_signatures_and_domains_are_compatible<__sigs_from_t<_Return, false>, + attrs<_Attrs...>> class __make_function<_Return(_Args...), attrs<_Attrs...>> { using __sigs = __sigs_from_t<_Return, false>; @@ -587,6 +630,8 @@ namespace experimental::execution }; template + requires __completion_signatures_and_domains_are_compatible<__sigs_from_t<_Return, true>, + attrs<_Attrs...>> class __make_function<_Return(_Args...) noexcept, attrs<_Attrs...>> { using __sigs = __sigs_from_t<_Return, true>; @@ -598,6 +643,8 @@ namespace experimental::execution }; template + requires __completion_signatures_and_domains_are_compatible, + attrs<_Attrs...>> class __make_function, attrs<_Attrs...>> { using __sigs = __canonical_t>; @@ -609,6 +656,8 @@ namespace experimental::execution }; template + requires __completion_signatures_and_domains_are_compatible, + attrs<_Attrs...>> class __make_function, queries<_Queries...>, diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 8936b00b1..6dcf6eedf 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -709,4 +709,75 @@ namespace STATIC_REQUIRE(std::constructible_from>); } } + + template + concept function_exists = requires { typename exec::function; }; + + TEST_CASE("function can't be specialized with invalid completion specifications") + { + SECTION("specifying a completion signature with no corresponding completion domain is fine") + { + STATIC_REQUIRE(function_exists, exec::attrs<>>); + STATIC_REQUIRE( + function_exists, exec::attrs<>>); + STATIC_REQUIRE( + function_exists, exec::attrs<>>); + } + + SECTION("specifying a completion domain is fine if you also specify a corresponding signature") + { + STATIC_REQUIRE( + function_exists, + exec::attrs)>>); + STATIC_REQUIRE( + function_exists, + exec::attrs)>>); + STATIC_REQUIRE( + function_exists, + exec::attrs)>>); + } + + SECTION("you may not specify a completion domain if there's no corresponding signature") + { + STATIC_REQUIRE( + !function_exists, + exec::attrs)>>); + STATIC_REQUIRE( + !function_exists, + exec::attrs)>>); + STATIC_REQUIRE( + !function_exists, + exec::attrs)>>); + } + + SECTION("you may specify only some completion domains") + { + STATIC_REQUIRE( + function_exists< + ex::completion_signatures, + exec::attrs)>>); + STATIC_REQUIRE( + function_exists< + ex::completion_signatures, + exec::attrs)>>); + STATIC_REQUIRE( + function_exists< + ex::completion_signatures, + exec::attrs)>>); + } + + SECTION("sender attributes other than completion domain queries don't break") + { + // TODO: it's not obvious that it makes sense to support sender attributes other than + // completion domain queries so this may be silly.... + auto query = [](auto const &) + { + return 0; + }; + using query_t = decltype(query); + + STATIC_REQUIRE( + function_exists, exec::attrs>); + } + } } // namespace From ab6ed4b083d2565b5fea2544c830ca4163ff3e00 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 5 Jun 2026 08:44:29 -0700 Subject: [PATCH 15/17] Delete TODOs that are TODone --- test/exec/test_function.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 6dcf6eedf..f4883078b 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -180,7 +180,6 @@ namespace SECTION("sender_tag(int) with set_value_t(int) and attrs but no queries") { - // TODO: validate that the required completion domains "match" the permitted completions exec::function, exec::attrs)>> @@ -191,7 +190,6 @@ namespace SECTION("sender_tag(int) with set_error_t(int), attrs, and trivial queries") { - // TODO: validate that the required completion domains "match" the permitted completions exec::function, exec::queries<>, From 5f7e99b0fef7dcdcdb00e8d25984d3b10e3612c0 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 5 Jun 2026 10:39:07 -0700 Subject: [PATCH 16/17] Fix build errors Looks like Clang 16 needs help doing CTAD, and gcc 12 trips over the CPO instances being `const`. Maybe this'll work? --- test/exec/test_function.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index f4883078b..ebab8fef8 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -87,12 +87,12 @@ namespace template constexpr sender operator()(Values... values) const noexcept { - return sender(std::move(values)...); + return sender(std::move(values)...); } }; template - inline constexpr domain_sender_t domain_sender{}; + inline constexpr domain_sender_t, Domain> domain_sender{}; TEST_CASE("exec::function is constructible", "[types][function]") { From f9bb44a142f84055554c2a2af66354fbb43bda88 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 5 Jun 2026 16:57:24 -0700 Subject: [PATCH 17/17] Moar build fixes Looks like I missed a spot where the constness of the completion CPOs trips up gcc 12; this should fix it. --- test/exec/test_function.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index ebab8fef8..4babab041 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -662,7 +662,8 @@ namespace } template - using custom_domain_for = exec::attrs)>; + using custom_domain_for = + exec::attrs>)>; TEST_CASE("function can't be constructed with a sender that completes in the wrong domain", "[types][function]")