From dac31d1fa832fc14e8c7dc82afff62188ca40423 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 5 Oct 2026 05:26:54 +0000 Subject: [PATCH 01/19] Let __msort accept an empty type list `__msort` builds its array of type ids with `std::array{__mtypeid<_Ts>...}`, but class template argument deduction can't deduce a `std::array` from an empty initializer list, so `__msort<...>::__f<>` is ill-formed. This diff names the array type explicitly and adds tests for `__msort`'s ordering and for empty lists, both alone and composed with `__munique`. Co-Authored-By: Claude Opus 5.5 --- include/stdexec/__detail/__meta.hpp | 6 ++++-- test/stdexec/detail/test_utility.cpp | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/include/stdexec/__detail/__meta.hpp b/include/stdexec/__detail/__meta.hpp index 13232c635..8ac1f7dc9 100644 --- a/include/stdexec/__detail/__meta.hpp +++ b/include/stdexec/__detail/__meta.hpp @@ -1115,8 +1115,10 @@ namespace STDEXEC struct __msort { template - using __f = __detail::__msort_apply<_Continuation, - __detail::__msort_impl(std::array{__mtypeid<_Ts>...})>::__t; + using __f = __detail::__msort_apply< + _Continuation, + // name the array type explicitly: CTAD can't deduce one from an empty pack + __detail::__msort_impl(std::array<__type_index, sizeof...(_Ts)>{__mtypeid<_Ts>...})>::__t; }; } // namespace STDEXEC diff --git a/test/stdexec/detail/test_utility.cpp b/test/stdexec/detail/test_utility.cpp index a909e983f..40f8fd98d 100644 --- a/test/stdexec/detail/test_utility.cpp +++ b/test/stdexec/detail/test_utility.cpp @@ -64,4 +64,18 @@ namespace using res = __minvoke; static_assert(is_same_v, optional>>); } + TEST_CASE("sort orders a type list independently of the input order", "[detail][sort]") + { + using sort = __msort<>; + static_assert( + is_same_v<__minvoke, __minvoke>); + static_assert(is_same_v<__minvoke, __mlist>); + } + + TEST_CASE("sort accepts an empty type list", "[detail][sort]") + { + static_assert(is_same_v<__minvoke<__msort<>>, __mlist<>>); + static_assert(is_same_v<__minvoke<__munique<__msort<>>>, __mlist<>>); + } + } // namespace From edcc41986c6807d1512b0207acabc0fc5831f20e Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 5 Oct 2026 05:37:37 +0000 Subject: [PATCH 02/19] Test frame-allocator use and factory exceptions in function This diff adds two tests of existing behaviour: * an erased operation state that's too large for the type-erased operation state's inline buffer is allocated exactly once, through the frame allocator found in the receiver's environment; and * an exception thrown by the sender factory propagates out of `connect`. The existing "forwards get_frame_allocator" test only confirms that the frame allocator is visible to the erased sender; the first new test confirms that `function` uses it for its own allocation, which is the path fixed in #2249. Co-Authored-By: Claude Opus 5.5 --- test/exec/test_function.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 650e50c48..3b9cb814e 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -21,9 +21,11 @@ #include +#include #include #include #include +#include namespace ex = STDEXEC; @@ -310,6 +312,36 @@ namespace REQUIRE(ret == 42); } + TEST_CASE("exec::function allocates a large operation state with the frame allocator", + "[types][function]") + { + // big enough that the erased operation state can't be stored inline + using big = std::array; + + counting_resource res; + std::pmr::polymorphic_allocator alloc{&res}; + + exec::function sndr([]() noexcept { return ex::just(big{}); }); + + auto [ret] = ex::sync_wait(std::move(sndr) + | ex::write_env(ex::prop(exec::get_frame_allocator, alloc))) + .value(); + + REQUIRE(ret == big{}); + REQUIRE(res.count == 1); + } + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("an exception thrown by the sender factory propagates out of connect", + "[types][function]") + { + exec::function sndr([]() -> decltype(ex::just(0)) + { throw std::runtime_error("factory failed"); }); + + REQUIRE_THROWS_AS(ex::sync_wait(std::move(sndr)), std::runtime_error); + } +#endif + TEST_CASE("exec::function is conditionally lvalue connectable", "[types][function]") { exec::function sndr([]() noexcept { return ex::just(42); }); From 6e396ac0b8bb5dc2bdd2f943657765cbda95a32b Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 8 Oct 2026 20:50:02 -0700 Subject: [PATCH 03/19] Require function's sender factory to be stateless `function` stores its sender factory as raw bytes and reconstitutes it in `connect`. Any small, trivially-copyable callable has qualified as a factory, so a lambda capturing `this` (or a reference to a local) could hide a borrowed object inside the factory, where neither the `function`'s type nor the code constructing it shows the dependency. This diff requires the factory to be a pointer to function, a pointer to member, or an empty, trivially-copyable callable (like a captureless lambda or a CPO like `ex::just`). Any state the factory needs must now be passed as a curried argument, which the `function`'s type records. The constraints are factored into named concepts (`__is_callable_pointer`, `__is_empty_callable`, `__is_factory_of_suitable_sender`, and `__is_suitable_factory`), and `__not_decays_to` is checked first so that copies and moves don't instantiate the more expensive factory checks. A stateless factory is at most a pointer to member, so the size check becomes a `static_assert` in the constructor rather than a constraint. The tests that captured state now curry it instead: the frame-allocator forwarding test curries the memory resource, and the virtual-member-function tests curry an `iface const *`. The next diff gives the latter a nicer spelling. Co-Authored-By: Claude Opus 5.5 --- include/exec/function.hpp | 73 ++++++++++++++++++++++----- test/exec/test_function.cpp | 98 +++++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 27 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 4b6f8d0b1..b5211cf8b 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -326,6 +326,57 @@ namespace experimental::execution && ((!__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 sender factory passed to function must be one of: + //! 1. a pointer-to-function, + //! 2. a pointer-to-member (function or object), or + //! 3. an empty, trivially-copyable, callable object (like a captureless lambda or a + //! CPO like STDEXEC::just). + //! + //! Concept __is_callable_pointer matches options 1 and 2, and concept + //! __is_empty_callable matches option 3. + + //! Satisfied when _Ty is a pointer-to-function or pointer-to-member + template + concept __is_callable_pointer = (std::is_pointer_v<_Ty> + && std::is_function_v>) + || std::is_member_pointer_v<_Ty>; + + //! Satisfied when _Ty is an empty, trivially-copyable object; callability is not + //! actually checked here because __is_suitable_factory checks __invocable before + //! checking is_empty_callable + template + concept __is_empty_callable = std::is_empty_v<_Ty> && (STDEXEC_IS_TRIVIALLY_COPYABLE(_Ty)); + + //! Defines the constraints on a function's sender factory argument; satisfied when + //! _Factory: + //! - is invocable as a const lvalue with the given argument types (function + //! stores the factory and always invokes it that way; a stateless factory + //! has nothing to mutate or consume), + //! - returns a sender that is connectable to the given receiver type, and + //! - returns a sender whose completion domains match the declared completion + //! domains of the function that will use it. + //! + //! \tparam _Factory the ostensible factory to check + //! \tparam _Func the function specialization that will use _Factory + //! \tparam _Receiver the type of the receiver to which the sender returned from the + //! factory will be connected + //! \tparam _Args... the factory arguments + template + concept __is_factory_of_suitable_sender = + __invocable<_Factory const &, _Args...> + && sender_to<__invoke_result_t<_Factory const &, _Args...>, _Receiver> + && __completion_domains_match<__invoke_result_t<_Factory const &, _Args...>, + _Func, + env_of_t<_Receiver>>; + + //! Defines the constraints on a function's sender factory; satisfied when _Factory: + //! - produces a suitable sender as defined by __is_factory_of_suitable_sender, and + //! - is either of the above-defined kinds of callable. + template + concept __is_suitable_factory = + __is_factory_of_suitable_sender<_Factory, _Func, _Receiver, _Args...> + && (__is_callable_pointer<_Factory> || __is_empty_callable<_Factory>); + //! the main implementation of the type-erasing sender function<...> // //! @tparam _Sigs The supported completion signatures @@ -358,9 +409,9 @@ namespace experimental::execution __mk_opstate(void *__storage, __receiver_t __rcvr, _Args &&...__args) // -> _any::_any_opstate_base { - auto &__make_sender = *__std::start_lifetime_as<_Factory>(__storage); - using __alloc_t = decltype(__choose_frame_allocator(STDEXEC::get_env(__rcvr))); - auto __alloc = __frame_allocator_t<__alloc_t>( + auto const &__make_sender = *__std::start_lifetime_as<_Factory>(__storage); + 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, @@ -392,19 +443,19 @@ namespace experimental::execution public: using sender_concept = sender_tag; - template <__invocable<_Args...> _Factory> - requires __not_decays_to<_Factory, __function> // - && (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<__receiver_t>> + // check __not_decays_to first: the conjunction short-circuits, so + // overload resolution for an ordinary copy or move never instantiates + // the much more expensive __is_suitable_factory check + template + requires __not_decays_to<_Factory, __function> + && __is_suitable_factory<_Factory, __function, __receiver_t, _Args...> constexpr explicit __function(_Args &&...__args, _Factory __factory) noexcept(__nothrow_move_constructible<_Args...>) : __args_(static_cast<_Args &&>(__args)...) , __make_opstate_(&__mk_opstate<_Factory>) { + static_assert(sizeof(_Factory) <= sizeof(__make_sender_)); + std::memcpy(__make_sender_, std::addressof(__factory), sizeof(_Factory)); } diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 3b9cb814e..0a93835b9 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -274,9 +274,10 @@ namespace TEST_CASE("exec::function forwards get_frame_allocator", "[types][function]") { - counting_resource res; - exec::function sndr( - [&res]() noexcept + counting_resource res; + exec::function sndr( + res, + [](auto &res) noexcept { return ex::read_env(exec::get_frame_allocator) | ex::then( @@ -362,7 +363,7 @@ namespace struct iface { - virtual exec::function get_i_virtually() const noexcept = 0; + virtual exec::function get_i_virtually() const noexcept = 0; }; struct iface2 @@ -372,7 +373,7 @@ namespace return exec::function(this, &iface2::get_i_virtually); } - virtual exec::function get_i_virtually() const noexcept = 0; + virtual exec::function get_i_virtually() const noexcept = 0; }; struct impl @@ -393,34 +394,98 @@ namespace return self->just_i(); } - exec::function get_i_with_capture() const noexcept - { - return exec::function([this]() noexcept { return just_i(); }); - } - exec::function get_i_with_pmfn() const noexcept { return exec::function(this, &impl::just_i); } - exec::function get_i_virtually() const noexcept override + exec::function get_i_virtually() const noexcept override { - return get_i_with_capture(); + return exec::function( + this, + [](iface const *self) noexcept { return static_cast(self)->just_i(); }); } private: int i_; }; - TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + struct sender_holder { - SECTION("function accepts a lambda capturing this") + decltype(ex::just(42)) sndr = ex::just(42); + }; + + struct move_only_sender_holder + { + decltype(ex::just(std::unique_ptr{})) sndr = ex::just(std::unique_ptr{}); + }; + + TEST_CASE("exec::function accepts only stateless sender factories", "[types][function]") + { + SECTION("function accepts a pointer to member data") { - auto [ret] = ex::sync_wait(impl{42}.get_i_with_capture()).value(); + sender_holder h; + auto [ret] = ex::sync_wait( + exec::function(&h, &sender_holder::sndr)) + .value(); REQUIRE(ret == 42); } + SECTION("a pointer to member data yields an lvalue, so the sender must be copyable") + { + using function = + exec::function(move_only_sender_holder const *) noexcept>; + using factory = decltype(&move_only_sender_holder::sndr); + + STATIC_REQUIRE(!std::constructible_from); + } + + SECTION("a callable with state is rejected, however small") + { + using function = exec::function; + + int i = 42; + auto stateful = [i]() noexcept + { + return ex::just(i); + }; + auto stateless = []() noexcept + { + return ex::just(42); + }; + + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("the factory must be invocable as a const lvalue") + { + using function = exec::function; + + struct non_const_call + { + auto operator()() noexcept + { + return ex::just(42); + } + }; + + struct rvalue_call + { + auto operator()() && noexcept + { + return ex::just(42); + } + }; + + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + } + } + + TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + { SECTION("function accepts a pointer-to-member function") { auto [ret] = ex::sync_wait(impl{42}.get_i_with_pmfn()).value(); @@ -438,7 +503,8 @@ namespace REQUIRE(ret == 42); } - SECTION("function can be the return type of a virtual member function") + SECTION("function can be the return type of a virtual member " + "function") { auto [ret] = ex::sync_wait(impl{42}.get_i_virtually()).value(); From ecd174e1b289718b5dd2a24013bb6e25c7abac21 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 8 Oct 2026 20:52:35 -0700 Subject: [PATCH 04/19] Support member functions in function with lvalue reference qualifiers This diff teaches `function` to represent asynchronous member functions, declared as `function` or `function` (optionally `noexcept`, and also in the `sender_tag` form with explicit completions, queries, and attrs). The constructor takes the object, which must be an lvalue, as its first argument; the `function` stores a pointer to it, and `connect` passes it to the factory, with the declared qualifiers, ahead of the curried arguments. Since factories are stateless, the ref-qualifier is a guarantee rather than an annotation: a `function`'s type shows whether it borrows an object, and whether it may modify it. It also makes virtual member functions that return `function` natural to write. A `const &` `function` also accepts a non-const lvalue, as a synchronous `const &` member function would; the factory still receives a const reference. Rvalues are never accepted because the `function` would then borrow an object that's about to expire, so something like `auto f = make_example().get_int();` would dangle. For the same reason, `&&` and `const &&` function types are rejected, as is `const` without a ref-qualifier (which binds rvalues, too), and so are the `volatile` forms. The `function` alias is constrained on `__is_supported_function_type` so that unsupported forms produce a constraint failure naming the concept rather than an incomplete-type error, and so that `function` stays SFINAE-friendly. Under the hood, `__make_function` maps a ref-qualified function type to one whose first parameter is a `__self_box` that records the qualifiers. The matching `__function` specialization stores a (possibly const) `void*` and wraps the factory in a `__self_adapting_factory`, which casts the pointer back to `__declared_self_t` before invoking the user's factory. `__binds_to_self` decides which self arguments are accepted. Supporting member functions motivated a reorganization of `__make_function`: its specializations are regrouped (return-type-driven ones, then completion-signature-driven ones), they express the variants through inheritance rather than repetition, and `__make_function` is now a struct template. `__sigs_from_t` is inlined, and some unnecessary `__func::` qualification is removed. The documentation at the top of the file and on the `function` alias template now describes the current interface. The tests cover member functions end to end, `noexcept` construction from pointer factories, which self arguments each form accepts, and the rejection of the unsupported function types. The virtual-interface tests now return `function` instead of currying a pointer. Co-Authored-By: Claude Opus 5.5 --- include/exec/function.hpp | 489 +++++++++++++++++++++++++----------- test/exec/test_function.cpp | 262 ++++++++++++++++++- 2 files changed, 596 insertions(+), 155 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index b5211cf8b..1494eb300 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -38,23 +38,24 @@ #include #include -// This file defines function, which is a type-erased -// sender that can complete with: +// This file defines function, a type-erased sender intended +// for ABI-stable asynchronous API boundaries, including virtual member +// functions. Like a task coroutine, it represents an asynchronous function from +// arguments to results; unlike a coroutine, it allocates nothing when it is +// constructed. // -// - set_value(ReturnType) -// - set_error(std::exception_ptr) -// - set_stopped() +// A function stores its arguments and a sender factory (a pointer to function, +// a pointer to member, or an empty, trivially copyable callable). connect +// invokes the factory with the stored arguments and connects the resulting +// sender to a type-erased receiver; that is why the template parameter is a +// function type rather than just a return type. The type-erased operation +// state is created in connect, so its storage can come from a frame allocator +// found in the receiver's environment (see get_frame_allocator) without relying +// on thread-local state. // -// The type-erased operation state is allocated in connect; to accomplish this -// deferred allocation, the sender holds a tuple of arguments that are passed -// into a sender-factory in connect, which is why the template type parameter is -// a function type rather than just a return type. -// -// The intended use case is an ABI-stable API boundary. The hope is that this is -// a "better task" in that it represents an async function from arguments to -// value, just like a task coroutine, but, by deferring the allocation to -// connect, we can use receiver environment queries to pick the frame allocator -// from the environment without relying on TLS. +// The completions, required environment queries and required completion +// domains are part of the type; see the documentation of the function alias +// template at the end of this file for the accepted forms. namespace experimental::execution { // for specifying required sender attributes in exec::function @@ -326,6 +327,44 @@ namespace experimental::execution && ((!__has_completion_domain<_Attrs, set_error_t>) || _Sigs::__count(set_error) > 0) // && ((!__has_completion_domain<_Attrs, set_stopped_t>) || _Sigs::__count(set_stopped) > 0); + template + struct __self_box + { + using __void_pointer = + __if_c; + + using __self_tag = _Self; + }; + + struct __self_tag + {}; + + //! Satisfied when an object of type _Ty, passed as `self`, can bind to the + //! implicit object parameter whose qualifiers _SelfBox records for + //! __self_tag (which come from the function type's qualifiers, e.g. + //! `int() const &`), as it could for a synchronous member function with + //! those qualifiers, except that rvalues are never accepted: the function + //! stores a pointer to the object, so accepting an rvalue would leave the + //! function borrowing an object that is about to expire. Concretely: + //! - `&` accepts a non-const lvalue, and + //! - `const &` accepts a const or non-const lvalue (a qualification + //! conversion; the factory still receives a const reference). + //! volatile objects are rejected, since no supported function type can + //! bind to them. + template + concept __binds_to_self = + std::is_lvalue_reference_v<_Ty> && (!std::is_volatile_v>) + && (__same_as<__copy_cvref_t<_Ty, __self_tag>, typename _SelfBox::__self_tag> + || __same_as); + + //! The type with which the implicit object parameter is passed to the + //! factory: _Self's unqualified type with the qualifiers the function type + //! declares, so a non-const lvalue passed to a `const &` function reaches + //! the factory as a const reference. + template + using __declared_self_t = + __copy_cvref_t>; + //! The sender factory passed to function must be one of: //! 1. a pointer-to-function, //! 2. a pointer-to-member (function or object), or @@ -377,6 +416,31 @@ namespace experimental::execution __is_factory_of_suitable_sender<_Factory, _Func, _Receiver, _Args...> && (__is_callable_pointer<_Factory> || __is_empty_callable<_Factory>); + //! A class template that adapts a user-provided callable expecting a "self" reference + //! in the first argument to a factory that accepts a pointer to (const) void, which + //! is what's actually invoked by the underlying __function + template + struct __self_adapting_factory + { + using __value = STDEXEC_REMOVE_REFERENCE(_Self); + using __pointer = __value *; + + _Factory __factory; + + template + requires std::is_void_v<_Void> + constexpr decltype(auto) operator()(_Void *__self, _Args &&...__args) const + noexcept(__nothrow_invocable<_Factory const &, _Self, _Args...>) + { + static_assert(__is_callable_pointer<_Factory> || __is_empty_callable<_Factory>); + static_assert(STDEXEC_IS_CONST(__value) == STDEXEC_IS_CONST(_Void)); + + return __invoke(__factory, + static_cast<_Self>(*static_cast<__pointer>(__self)), + static_cast<_Args &&>(__args)...); + } + }; + //! the main implementation of the type-erasing sender function<...> // //! @tparam _Sigs The supported completion signatures @@ -387,6 +451,10 @@ namespace experimental::execution //! query, when given the specified arguments, must return a value //! convertible to Return, and it must be noexcept, or not, as appropriate //! + //! @tparam _Attrs The list of completion domains the erased sender must + //! report; a pack of function types like Domain(get_completion_domain_t) + //! noexcept, also reported by the function's own environment + //! //! @tparam _Args The argument types used to construct the erased sender template class __function @@ -399,6 +467,7 @@ namespace experimental::execution static_assert(__is_instance_of<_Attrs, attrs>); static_assert(__completion_signatures_and_domains_are_compatible<_Sigs, _Attrs>); + protected: using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; template @@ -440,16 +509,11 @@ namespace experimental::execution //! function, which usually requires two pointers. std::byte __make_sender_[2 * sizeof(void *)]{}; - public: - using sender_concept = sender_tag; + struct __tag + {}; - // check __not_decays_to first: the conjunction short-circuits, so - // overload resolution for an ordinary copy or move never instantiates - // the much more expensive __is_suitable_factory check template - requires __not_decays_to<_Factory, __function> - && __is_suitable_factory<_Factory, __function, __receiver_t, _Args...> - constexpr explicit __function(_Args &&...__args, _Factory __factory) + constexpr explicit __function(_Args &&...__args, _Factory __factory, __tag) noexcept(__nothrow_move_constructible<_Args...>) : __args_(static_cast<_Args &&>(__args)...) , __make_opstate_(&__mk_opstate<_Factory>) @@ -459,6 +523,20 @@ namespace experimental::execution std::memcpy(__make_sender_, std::addressof(__factory), sizeof(_Factory)); } + public: + using sender_concept = sender_tag; + + // check __not_decays_to first: the conjunction short-circuits, so + // overload resolution for an ordinary copy or move never instantiates + // the much more expensive __is_suitable_factory check + template + requires __not_decays_to<_Factory, __function> + && __is_suitable_factory<_Factory, __function, __receiver_t, _Args...> + constexpr explicit __function(_Args &&...__args, _Factory __factory) + noexcept(__nothrow_move_constructible<_Args...>) + : __function(static_cast<_Args &&>(__args)..., __factory, __tag{}) + {} + //! this implementation of get_completion_signatures is taken directly //! from the equivalent function on any_sender_of template @@ -501,6 +579,58 @@ namespace experimental::execution } }; + //! This specialization of __function handles "member functions", which are created + //! by specializing __make_function (below) with a cvref-qualified function type to + //! indicate that the argument list includes an implicit self object (with the given + //! cvref qualifiers). _SelfBox is effectively a tag type whose type parameter is a + //! tag type conveying the cvref qualifiers of the implicit object parameter. + //! + //! Member functions are implemented in terms of a (possibly const) void pointer and + //! a sender factory adaptor that captures the real type of the implicit object and + //! casts the stored void pointer back to the correct type upon invocation. + //! + //! \tparam _Sigs the functions possible completion signatures + //! \tparam _Queries the queries required to be supported by the environment of the + //! receiver to which this function is ultimately connected + //! \tparam _Attrs the queries supported by this function's attributes + //! \tparam _SelfBox the tag type conveying the cvref qualifiers of the implicit + //! object parameter + //! \tparam _Args the pack of explicit arguments to the sender factory + template _SelfBox, + class... _Args> + class __function<_Sigs, _Queries, _Attrs, _SelfBox, _Args...> + : public __function<_Sigs, _Queries, _Attrs, typename _SelfBox::__void_pointer, _Args...> + { + using __void_pointer = _SelfBox::__void_pointer; + using __base = __function<_Sigs, _Queries, _Attrs, __void_pointer, _Args...>; + + using __receiver_t = __base::__receiver_t; + using __tag = __base::__tag; + + public: + // check the cheap __binds_to_self first; the conjunction short-circuits + template + requires __binds_to_self<_Self &&, _SelfBox> + && __is_suitable_factory<_Factory, + __function, + __receiver_t, + __declared_self_t<_Self, _SelfBox>, + _Args...> + constexpr explicit __function(_Self &&__self, _Args &&...__args, _Factory __fact) + // like the base's constructor: storing the self pointer and the + // (pointer or empty) factory can't throw, so only the curried + // arguments' moves matter + noexcept(__nothrow_move_constructible<_Args...>) + : __base(static_cast<__void_pointer>(std::addressof(__self)), + static_cast<_Args &&>(__args)..., + __self_adapting_factory<_Factory, __declared_self_t<_Self, _SelfBox>>{__fact}, + __tag{}) + {} + }; + template class _Template, std::size_t... _Is> consteval auto __canonicalize_splice(__indices<_Is...>) noexcept { @@ -520,8 +650,8 @@ namespace experimental::execution consteval auto __canonicalize(_List<_Types...> *) noexcept { using __types_t = __static_vector<__type_index, sizeof...(_Types)>; - constexpr auto __types = __func::__canonicalize_impl(__types_t{__mtypeid<_Types>...}); - return __func::__canonicalize_splice<__types, _List>(__make_indices<__types.size()>()); + constexpr auto __types = __canonicalize_impl(__types_t{__mtypeid<_Types>...}); + return __canonicalize_splice<__types, _List>(__make_indices<__types.size()>()); } //! Map the type-list _Sigs to a canonical form, which sorts and uniques the @@ -531,17 +661,119 @@ namespace experimental::execution //! @tparam _Sigs a type-list of types to be sorted and uniqued; expected to //! be a specialization of completion_signatures or queries. template - using __canonical_t = decltype(__func::__canonicalize(static_cast<_Sigs *>(nullptr))); + using __canonical_t = decltype(__canonicalize(static_cast<_Sigs *>(nullptr))); + + template + struct __function_meta; + + template + struct __function_meta<_Return(_Args...)> + { + using __return_type = _Return; + + static constexpr bool __noexcept = false; + + template + using __make_function = __function<_LeadingArgs..., _Args...>; + }; + + template + struct __function_meta<_Return(_Args...) &> + { + using __return_type = _Return; + + static constexpr bool __noexcept = false; + + template + using __make_function = __function<_LeadingArgs..., __self_box<__self_tag &>, _Args...>; + }; + + template + struct __function_meta<_Return(_Args...) const &> + { + using __return_type = _Return; + + static constexpr bool __noexcept = false; + + template + using __make_function = __function<_LeadingArgs..., __self_box<__self_tag const &>, _Args...>; + }; + + template + struct __function_meta<_Return(_Args...) noexcept> + { + using __return_type = _Return; + + static constexpr bool __noexcept = true; + + template + using __make_function = __function<_LeadingArgs..., _Args...>; + }; + + template + struct __function_meta<_Return(_Args...) & noexcept> + { + using __return_type = _Return; + + static constexpr bool __noexcept = true; + + template + using __make_function = __function<_LeadingArgs..., __self_box<__self_tag &>, _Args...>; + }; + + template + struct __function_meta<_Return(_Args...) const & noexcept> + { + using __return_type = _Return; + + static constexpr bool __noexcept = true; + + template + using __make_function = __function<_LeadingArgs..., __self_box<__self_tag const &>, _Args...>; + }; + + template + using __return_type_t = __function_meta<_Ty>::__return_type; + + template + inline constexpr bool __is_noexcept = __function_meta<_Ty>::__noexcept; + + template + concept __is_function_type = std::is_function_v<_Ty>; + + //! Satisfied when _Ty is one of the function types function accepts: + //! + //! R(A...), R(A...) &, R(A...) const &, each optionally noexcept + //! + //! Rejected forms (__function_meta has no specialization for them): + //! - `&&` and `const &&`: the function would borrow an object that is + //! about to expire; + //! - `const` without a ref-qualifier: as for synchronous member + //! functions, it would bind rvalues too, reopening the same problem; + //! - anything `volatile`. + template + concept __is_supported_function_type = __is_function_type<_Ty> && requires { + typename __function_meta<_Ty>::__return_type; + }; + + template + concept __is_sender_tag_function = __is_function_type<_Ty> + && __same_as> + && (!__is_noexcept<_Ty>); + + template + concept __is_not_sender_tag_function = __is_function_type<_Ty> + && __not_same_as>; //! Given a return type and a bool indicating whether the function is //! noexcept, compute the appropriate completion_signatures. The result is a //! set_value overload taking either Return&& or no args when Return is //! void, set_stopped, and, when the function type is not noexcept, //! set_error(std::exception_ptr) - template - using __sigs_from_t = __canonical_t<__concat_completion_signatures_t< - completion_signatures<__single_value_sig_t<_Return>, set_stopped_t()>, - __eptr_completion_unless_t<__mbool<_NoExcept>>>>; + template + using __completion_sigs_from = __canonical_t<__concat_completion_signatures_t< + completion_signatures<__single_value_sig_t<__return_type_t<_Ty>>, set_stopped_t()>, + __eptr_completion_unless_t<__mbool<__is_noexcept<_Ty>>>>>; //! maps a completion signature to the default completion domain query struct __domain_query_from_sig @@ -606,130 +838,85 @@ namespace experimental::execution //! 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; - - template - class __make_function<_Return(_Args...)> - { - using __sigs = __sigs_from_t<_Return, false>; - using __queries = queries<>; - using __attrs = __default_attrs<__sigs>; - - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; - - template - class __make_function<_Return(_Args...) noexcept> - { - using __sigs = __sigs_from_t<_Return, true>; - using __queries = queries<>; - using __attrs = __default_attrs<__sigs>; - - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; - - template - class __make_function> - { - using __sigs = __canonical_t>; - using __queries = queries<>; - using __attrs = __default_attrs<__sigs>; - - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; - - template - class __make_function<_Return(_Args...), queries<_Queries...>> - { - using __sigs = __sigs_from_t<_Return, false>; - using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs>; + template <__is_function_type, class...> + struct __make_function; - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; - - template - class __make_function<_Return(_Args...) noexcept, queries<_Queries...>> - { - using __sigs = __sigs_from_t<_Return, true>; - using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs>; + //! Handle the cases where the given function signature matches + //! + //! Return(Args...) noexcept(???) + //! + //! Note that none of these specializations accept a fully-specified completion + //! signatures since they are derived from _Signature. - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; + template <__is_not_sender_tag_function _Signature> + struct __make_function<_Signature> + : __make_function<_Signature, queries<>, __default_attrs<__completion_sigs_from<_Signature>>> + {}; - template - class __make_function, - queries<_Queries...>> - { - using __sigs = __canonical_t>; - using __queries = __canonical_t>; - using __attrs = __default_attrs<__sigs>; + template <__is_not_sender_tag_function _Signature, __is_instance_of _Queries> + struct __make_function<_Signature, _Queries> + : __make_function<_Signature, _Queries, __default_attrs<__completion_sigs_from<_Signature>>> + {}; - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; + template <__is_not_sender_tag_function _Signature, __is_instance_of _Attrs> + requires __completion_signatures_and_domains_are_compatible< + __completion_sigs_from<_Signature>, + _Attrs> + struct __make_function<_Signature, _Attrs> : __make_function<_Signature, queries<>, _Attrs> + {}; - template - requires __completion_signatures_and_domains_are_compatible<__sigs_from_t<_Return, false>, - attrs<_Attrs...>> - class __make_function<_Return(_Args...), attrs<_Attrs...>> + template <__is_not_sender_tag_function _Signature, + __is_instance_of _Queries, + __is_instance_of _Attrs> + requires __completion_signatures_and_domains_are_compatible< + __completion_sigs_from<_Signature>, + _Attrs> + struct __make_function<_Signature, _Queries, _Attrs> { - using __sigs = __sigs_from_t<_Return, false>; - using __queries = queries<>; - using __attrs = __canonical_t>; - - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; + using type = + __function_meta<_Signature>::template __make_function<__completion_sigs_from<_Signature>, + __canonical_t<_Queries>, + __canonical_t<_Attrs>>; }; - 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>; - using __queries = queries<>; - using __attrs = __canonical_t>; + //! Handle the cases where the given function signature matches + //! + //! sender_tag(Args...) + //! + //! Note that all these specializations require fully-specified completion signatures + //! since they can't be derived from _Signature. - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; + template <__is_sender_tag_function _Signature, + __is_instance_of _ComplSigs> + struct __make_function<_Signature, _ComplSigs> + : __make_function<_Signature, _ComplSigs, queries<>, __default_attrs<_ComplSigs>> + {}; - template - requires __completion_signatures_and_domains_are_compatible, - attrs<_Attrs...>> - class __make_function, attrs<_Attrs...>> - { - using __sigs = __canonical_t>; - using __queries = queries<>; - using __attrs = __canonical_t>; + template <__is_sender_tag_function _Signature, + __is_instance_of _ComplSigs, + __is_instance_of _Queries> + struct __make_function<_Signature, _ComplSigs, _Queries> + : __make_function<_Signature, _ComplSigs, _Queries, __default_attrs<_ComplSigs>> + {}; - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; - }; + template <__is_sender_tag_function _Signature, + __is_instance_of _ComplSigs, + __is_instance_of _Attrs> + requires __completion_signatures_and_domains_are_compatible<_ComplSigs, _Attrs> + struct __make_function<_Signature, _ComplSigs, _Attrs> + : __make_function<_Signature, _ComplSigs, queries<>, _Attrs> + {}; - template - requires __completion_signatures_and_domains_are_compatible, - attrs<_Attrs...>> - class __make_function, - queries<_Queries...>, - attrs<_Attrs...>> + template <__is_sender_tag_function _Signature, + __is_instance_of _ComplSigs, + __is_instance_of _Queries, + __is_instance_of _Attrs> + requires __completion_signatures_and_domains_are_compatible<_ComplSigs, _Attrs> + struct __make_function<_Signature, _ComplSigs, _Queries, _Attrs> { - using __sigs = __canonical_t>; - using __queries = __canonical_t>; - using __attrs = __canonical_t>; - - public: - using type = __function<__sigs, __queries, __attrs, _Args...>; + using type = __function_meta<_Signature>::template __make_function<__canonical_t<_ComplSigs>, + __canonical_t<_Queries>, + __canonical_t<_Attrs>>; }; } // namespace __func @@ -751,13 +938,29 @@ namespace experimental::execution //! queries>: a fully-specified async function that //! maps (bar, baz) to the specified completions, requiring the specified //! queries in the ultimate receiver's environment + //! - any of the above with a trailing attrs), + //! ...>: additionally requires the erased sender to complete with Tag in + //! Domain, and reports that domain from the function's environment + //! - a signature with an lvalue reference qualifier, like + //! function or function (optionally + //! noexcept): an async member function; the constructor takes the object, + //! which must be an lvalue, as its first argument, the function holds a + //! pointer to it (so the object must outlive the function's operation), + //! and the factory receives it, with the declared cv- and ref-qualifiers, + //! before (bar); a `const &` function also accepts a non-const lvalue + //! + //! `&&`, `const &&`, `const` without a ref-qualifier and `volatile` function + //! types are not supported. + //! + //! When present, the completion signatures, queries and attrs must appear in + //! that order. The sender_tag form can't be declared noexcept. //! //! Future: support C-style ellipsis arguments in the function signature to //! permit type-erased arguments as well, like function (a //! fallible function from (bar, baz) plus unspecified, erased additional //! arguments to int) - template - using function = __func::__make_function<_Ts...>::type; + template <__func::__is_supported_function_type _Signature, class... _Ts> + using function = __func::__make_function<_Signature, _Ts...>::type; } // namespace experimental::execution namespace exec = experimental::execution; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 0a93835b9..1060fc929 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace ex = STDEXEC; @@ -363,17 +364,17 @@ namespace struct iface { - virtual exec::function get_i_virtually() const noexcept = 0; + virtual exec::function get_i_virtually() const noexcept = 0; }; struct iface2 { - exec::function get_i_from_base() const noexcept + exec::function get_i_from_base() const noexcept { - return exec::function(this, &iface2::get_i_virtually); + return exec::function(*this, &iface2::get_i_virtually); } - virtual exec::function get_i_virtually() const noexcept = 0; + virtual exec::function get_i_virtually() const noexcept = 0; }; struct impl @@ -399,11 +400,9 @@ namespace return exec::function(this, &impl::just_i); } - exec::function get_i_virtually() const noexcept override + exec::function get_i_virtually() const noexcept override { - return exec::function( - this, - [](iface const *self) noexcept { return static_cast(self)->just_i(); }); + return exec::function(*this, &impl::just_i); } private: @@ -503,15 +502,14 @@ namespace REQUIRE(ret == 42); } - SECTION("function can be the return type of a virtual member " - "function") + SECTION("function can be the return type of a virtual member function") { auto [ret] = ex::sync_wait(impl{42}.get_i_virtually()).value(); REQUIRE(ret == 42); } - SECTION("function accepts a pointer-to-member function") + SECTION("function accepts a pointer-to-member function") { impl imp{42}; auto [ret] = @@ -520,7 +518,7 @@ namespace REQUIRE(ret == 42); } - SECTION("function works on the base class") + SECTION("function works on the base class") { auto [ret] = ex::sync_wait(impl{42}.get_i_from_base()).value(); @@ -923,4 +921,244 @@ namespace function_exists, exec::attrs>); } } + + struct pointer_factories + { + int i; + + static auto just_i(pointer_factories const &self) noexcept + { + return ex::just(self.i); + } + + auto just_i_memfn() const noexcept + { + return ex::just(i); + } + }; + + TEST_CASE("member-function functions built from pointer factories are nothrow constructible", + "[types][function]") + { + using function = exec::function; + using self = pointer_factories const &; + + SECTION("pointer-to-function factory") + { + using factory = decltype(&pointer_factories::just_i); + STATIC_REQUIRE(std::is_nothrow_constructible_v); + + pointer_factories pf{42}; + auto [ret] = ex::sync_wait(function(std::as_const(pf), &pointer_factories::just_i)).value(); + REQUIRE(ret == 42); + } + + SECTION("pointer-to-member-function factory") + { + using factory = decltype(&pointer_factories::just_i_memfn); + STATIC_REQUIRE(std::is_nothrow_constructible_v); + + pointer_factories pf{42}; + auto [ret] = + ex::sync_wait(function(std::as_const(pf), &pointer_factories::just_i_memfn)).value(); + REQUIRE(ret == 42); + } + + SECTION("pointer-to-member-data factory") + { + using factory = decltype(&sender_holder::sndr); + STATIC_REQUIRE(std::is_nothrow_constructible_v); + + sender_holder h; + auto [ret] = ex::sync_wait(function(std::as_const(h), &sender_holder::sndr)).value(); + REQUIRE(ret == 42); + } + } + + TEST_CASE("member-function functions require factories invocable as const lvalues", + "[types][function]") + { + using function = exec::function; + + struct non_const_call + { + auto operator()(pointer_factories const &self) noexcept + { + return ex::just(self.i); + } + }; + + struct const_call + { + auto operator()(pointer_factories const &self) const noexcept + { + return ex::just(self.i); + } + }; + + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + + pointer_factories pf{42}; + auto [ret] = ex::sync_wait(function(pf, const_call{})).value(); + REQUIRE(ret == 42); + } + + TEST_CASE("member-function functions accept the self arguments a synchronous member function " + "would, except rvalues", + "[types][function]") + { + using factory = decltype(&pointer_factories::just_i); + + SECTION("const & accepts const and non-const lvalues") + { + using fn = exec::function; + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + } + + SECTION("& accepts only non-const lvalues") + { + using fn = exec::function; + using mfactory = decltype(ex::just(0)) (*)(pointer_factories &) noexcept; + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + STATIC_REQUIRE(!std::constructible_from); + } + + SECTION("a non-const lvalue reaches a const & function's factory as a const reference") + { + pointer_factories pf{42}; + auto [ret] = ex::sync_wait(exec::function( + pf, + [](auto &self) noexcept + { + static_assert( + std::is_const_v>); + return ex::just(self.i); + })) + .value(); + REQUIRE(ret == 42); + } + } + + template + concept valid_function_type = requires { typename exec::function; }; + + TEST_CASE("function rejects rvalue, unqualified const, and volatile function types", + "[types][function]") + { + using sigs = ex::completion_signatures; + + STATIC_REQUIRE(valid_function_type); + STATIC_REQUIRE(valid_function_type); + STATIC_REQUIRE(valid_function_type); + STATIC_REQUIRE(valid_function_type); + STATIC_REQUIRE(valid_function_type); + + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type < int() && noexcept >); + STATIC_REQUIRE(!valid_function_type < int() const && noexcept >); + STATIC_REQUIRE(!valid_function_type); + + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type); + + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type); + STATIC_REQUIRE(!valid_function_type); + } + + TEST_CASE("support for member functions works as expected", "[types][function]") + { + struct example + { + exec::function get_int() const & + { + return exec::function(*this, + [](example const &self) { return ex::just(self.i_); }); + } + + exec::function set_int(int i) & + { + return exec::function(*this, + int(i), + [](auto &self, int i) + { + return ex::just(i) + | ex::then( + [&self](int i) -> decltype(auto) + { + self.i_ = i; + return self; + }); + }); + } + + private: + int i_{42}; + }; + + { + example e; + + auto [result] = + ex::sync_wait(e.set_int(34) | ex::let_value([](auto &e) { return e.get_int(); })).value(); + + REQUIRE(result == 34); + } + } + + template + using async_getter = + exec::function>; + + template + using async_setter = + exec::function>; + + TEST_CASE("specifying more parameters of a function hiding a member function works", + "[types][function]") + { + struct example + { + async_getter get() const & noexcept + { + return async_getter(*this, [](auto &self) noexcept { return ex::just(self.i_); }); + } + + async_setter set(int i) & noexcept + { + return async_setter(*this, + int(i), + [](auto &self, int i) noexcept + { + return ex::just(i) + | ex::then( + [&self](int i) noexcept -> example & + { + self.i_ = i; + return self; + }); + }); + } + + private: + int i_{}; + }; + + example e; + + auto [result] = + ex::sync_wait(e.set(42) | ex::let_value([](auto &e) noexcept { return e.get(); })).value(); + + REQUIRE(result == 42); + } } // namespace From f30b0fc2f7e161f08725a7be195ee1d19f700bf8 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 5 Oct 2026 05:28:00 +0000 Subject: [PATCH 05/19] Canonicalize function's type lists with __munique and __msort `__canonical_t` sorts and deduplicates completion signatures, queries, and attrs with its own `consteval` machinery (`__canonicalize`, `__canonicalize_impl`, and `__canonicalize_splice` over a `__static_vector` of type ids). `__meta.hpp` provides `__munique` and `__msort`, and `__execution_fwd.hpp` already composes them the same way, so this diff replaces the bespoke machinery with `__munique<__msort<__q<_List>>>`. Both approaches order by `__mtypeid`, so the canonical types don't change. The `__static_vector.hpp` and `__typeinfo.hpp` includes are no longer needed (`__meta.hpp` includes the latter for `__msort`), so they're removed. New tests confirm that duplicate completion signatures collapse and that the order of attrs doesn't matter, alongside the existing order-independence tests for completion signatures and queries. Co-Authored-By: Claude Opus 5.5 --- include/exec/function.hpp | 30 +++++++----------------------- test/exec/test_function.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 1494eb300..0fd3546ff 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -22,9 +22,7 @@ #include "../stdexec/__detail/__read_env.hpp" #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" -#include "../stdexec/__detail/__static_vector.hpp" #include "../stdexec/__detail/__tuple.hpp" -#include "../stdexec/__detail/__typeinfo.hpp" #include "../stdexec/__detail/__utility.hpp" #include "../stdexec/functional.hpp" @@ -631,37 +629,23 @@ namespace experimental::execution {} }; - template class _Template, std::size_t... _Is> - consteval auto __canonicalize_splice(__indices<_Is...>) noexcept - { - return _Template<__msplice<_Types[_Is]>...>(); - } - - template - consteval auto __canonicalize_impl(__static_vector<__type_index, _Size> __types) noexcept - { - std::ranges::sort(__types); - auto const __rest = std::ranges::unique(__types); - __types.erase(__rest.begin(), __types.end()); - return __types; - } + template + struct __canonical; template