diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 4b6f8d0b1..7c50a3f8a 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" @@ -34,27 +32,29 @@ #include "any_sender_of.hpp" #include "get_frame_allocator.hpp" +#include #include #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 @@ -72,6 +72,18 @@ namespace experimental::execution inline constexpr auto __choose_frame_allocator = __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; + //! Satisfied when choosing the frame allocator from an environment of type + //! _Env and allocating from it can't throw. The check is made on the + //! __frame_allocator_t adaptation of the chosen allocator, which is what + //! actually allocates. + template + concept __has_nothrow_frame_allocator = + __nothrow_callable + && requires(__frame_allocator_t<__result_of<__choose_frame_allocator, _Env const &>> &__alloc, + std::size_t __n) { + { __alloc.allocate(__n) } noexcept; + }; + //! Wrap _Receiver, which is a type-erased receiver, in a type that can //! extract the concrete, to-be-erased receiver from the operation state //! that contains it. @@ -135,6 +147,11 @@ namespace experimental::execution using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; using __stop_token_t = stop_token_of_t>; + //! The declared get_frame_allocator query's result is what allocates, so + //! check it as the type-erased receiver's environment reports it. + static constexpr bool __nothrow_frame_allocation = + __has_nothrow_frame_allocator>; + _any::_state<_Receiver, __stop_token_t> __rcvr_; }; @@ -148,6 +165,11 @@ namespace experimental::execution using __stop_token_t = stop_token_of_t>; using __adaptee_t = __result_of<__choose_frame_allocator, env_of_t<_Receiver>>; + //! The injected allocator forwards to the one chosen from the concrete + //! receiver's environment, so check that one. + static constexpr bool __nothrow_frame_allocation = + __has_nothrow_frame_allocator>; + __memory_resource_adaptor_t<__adaptee_t> __resource_; __prop_t __env_; _any::_state<_Receiver, __stop_token_t> __rcvr_; @@ -192,6 +214,12 @@ namespace experimental::execution public: using operation_state_concept = operation_state_tag; + // The type-erased receiver holds a pointer to this object, and + // __opstate_base may hold a polymorphic_allocator pointing at its own + // __resource_, so moving an __opstate would leave dangling pointers. Say + // so explicitly rather than relying on _any_opstate_base being immovable. + STDEXEC_IMMOVABLE(__opstate); + template explicit constexpr __opstate(_Receiver __rcvr, _Factory __factory) : __base(static_cast<_Receiver &&>(__rcvr)) @@ -286,6 +314,41 @@ namespace experimental::execution template using __check_queries_t = __check_queries<_Queries, _Env...>::type; + //! get_completion_domain_t<> (i.e. get_completion_domain_t) asks for + //! the set_value domain, so attrs)> means the + //! same as attrs)>. Normalize to the + //! latter before canonicalizing so the rest of the implementation only + //! sees explicit completion tags. + template + struct __normalize_attr + { + using type = _Attr; + }; + + template + struct __normalize_attr<_Domain(get_completion_domain_t<>)> + { + using type = _Domain(get_completion_domain_t); + }; + + template + struct __normalize_attr<_Domain(get_completion_domain_t<>) noexcept> + { + using type = _Domain(get_completion_domain_t) noexcept; + }; + + template + struct __normalize_attrs; + + template + struct __normalize_attrs> + { + using type = attrs::type...>; + }; + + template + using __normalized_attrs_t = __normalize_attrs<_Attrs>::type; + template struct __get_completion_domain_tag; @@ -326,8 +389,265 @@ 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 + //! 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. + + //! Never defined. A pointer to a member of an incomplete class gets the + //! ABI's most general member-pointer representation, so these types bound + //! the size and alignment of any pointer factory a user can provide. + struct __incomplete; + using __general_pmf_t = void (__incomplete::*)(); + using __general_pmd_t = int __incomplete::*; + using __fn_ptr_t = void (*)(); + + //! The size required to store any factory __function accepts: an empty + //! callable, a pointer to function, or a pointer to member. Under the + //! Itanium C++ ABI a pointer to member function is two pointers, but under + //! the Microsoft ABI its size depends on the class's inheritance model, and + //! the general representation is larger. + inline constexpr std::size_t __factory_storage_size = std::max( + {sizeof(__fn_ptr_t), sizeof(__general_pmf_t), sizeof(__general_pmd_t)}); + + //! The alignment required to store any factory __function accepts: an + //! empty callable, a pointer to function, or a pointer to member. + inline constexpr std::size_t __factory_storage_align = std::max( + {alignof(__fn_ptr_t), alignof(__general_pmf_t), alignof(__general_pmd_t)}); + + //! 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>); + + //! A frame allocator whose allocate is declared noexcept. It exists only to + //! be named in unevaluated operands, so its members are never defined. + template + struct __nothrow_frame_allocator_stub + { + using value_type = _Ty; + + __nothrow_frame_allocator_stub() = default; + + template + constexpr __nothrow_frame_allocator_stub(__nothrow_frame_allocator_stub<_Uy> const &) noexcept + {} + + auto allocate(std::size_t) noexcept -> _Ty *; + void deallocate(_Ty *, std::size_t) noexcept; + + friend constexpr auto operator==(__nothrow_frame_allocator_stub, + __nothrow_frame_allocator_stub) noexcept -> bool = default; + }; + + //! A receiver used only to check, at construction, whether connecting the + //! factory's sender can throw. It is the type-erased receiver _Receiver + //! except that its environment's frame allocator is declared not to throw. + //! Frame-allocation failure is the caller's concern (it appears in the + //! conditional noexcept of function's connect), so a noexcept function type + //! promises only that the type-erased path doesn't throw *other than* by + //! failing to allocate a frame. In particular, a noexcept function whose + //! factory returns another noexcept function is accepted, even though the + //! inner function's connect allocates its frame through the outer one's + //! type-erased frame allocator. + //! + //! This is sound as long as a sender's connect depends on the frame + //! allocator's exception behaviour only through calls to allocate: the check + //! sees this stub's type, while at run time the sender sees the type-erased + //! receiver's frame allocator. + template + struct __nothrow_frame_allocation_receiver : _Receiver + { + using __prop_t = prop>; + + auto get_env() const noexcept -> __join_env_t<__prop_t, env_of_t<_Receiver>>; + }; + + //! Satisfied when _Factory can be invoked as a const lvalue with arguments + //! of types _Args without throwing. + template + concept __factory_is_nothrow_invocable = __nothrow_invocable<_Factory const &, _Args...>; + + //! Satisfied when transform_sender, given a _Sender and an _Env, returns a + //! sender of a different type, i.e. a domain has transformed it. + template + concept __is_transformed_for = + !__same_as<__decay_t>, __decay_t<_Sender>>; + + //! Satisfied when connecting the sender _Factory returns to the type-erased + //! receiver _Receiver can't throw, short of failing to allocate a frame (see + //! __nothrow_frame_allocation_receiver). If a domain transforms that sender, + //! the transformation and the transformed sender's connect are trusted + //! rather than checked. The domains that can transform it depend only on + //! the sender and on the queries the function type declares: _Receiver's + //! environment provides nothing else but a frame allocator, so the + //! caller's domain can't reach the sender. Those domains are chosen only + //! implicitly (declaring a get_scheduler_t query, for example, brings that + //! scheduler's domain into play) and nothing checks the choice, so + //! declaring the function type noexcept vouches for whatever they do. + template + concept __factory_sender_connects_without_throwing = + __is_transformed_for<__invoke_result_t<_Factory const &, _Args...>, + env_of_t<__nothrow_frame_allocation_receiver<_Receiver>>> + || __nothrow_connectable<__invoke_result_t<_Factory const &, _Args...>, + __nothrow_frame_allocation_receiver<_Receiver>>; + + //! Defines the additional constraints on the sender factory of a noexcept + //! function type, whose noexcept promises that the type-erased path + //! (invoking the factory and connecting the sender it returns) doesn't + //! throw. Satisfied when the function type isn't noexcept, or when _Factory: + //! - is nothrow-invocable with the curried arguments as connect() && passes + //! them (rvalues, unless the parameter type is a reference), and + //! - returns a sender that connects to _Receiver without throwing as + //! defined by __factory_sender_connects_without_throwing. + //! These are the only checks: each is exact, and when one fails, the + //! function type's author can fix it. Anything a domain transformation + //! does is trusted instead. + template + concept __meets_noexcept_contract = + (!_Nothrow) + || (__factory_is_nothrow_invocable<_Factory, _Args...> + && __factory_sender_connects_without_throwing<_Factory, _Receiver, _Args...>); + + //! 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(STDEXEC_IS_CONST(__value) == STDEXEC_IS_CONST(_Void)); + + return __invoke(__factory, + static_cast<_Self>(*static_cast<__pointer>(__self)), + static_cast<_Args &&>(__args)...); + } + }; + + //! The type with which function's constructor takes a curried argument + //! declared as _Ty: `_Ty const &` when _Ty is a non-reference type that is + //! both trivially copy-constructible and trivially move-constructible, + //! otherwise `_Ty &&`. An lvalue is therefore accepted (and copied) + //! exactly when writing std::move would change nothing; otherwise the + //! caller must move or copy explicitly (e.g. with auto(x)), so a costly + //! copy is never made silently. Both traits are required: with a + //! `const &` parameter even std::move(x) copies, so a type whose move is + //! non-trivial must not take this path. Reference types are unchanged. + template + using __curried_param_t = + __if_c && std::is_trivially_copy_constructible_v<_Ty> + && std::is_trivially_move_constructible_v<_Ty>, + _Ty const &, + _Ty &&>; + + //! The types with which function's constructor takes curried arguments + //! declared as _Args; see __function's _Params. + template + using __curried_params_t = __mlist<__curried_param_t<_Args>...>; + + template + class __function; + //! the main implementation of the type-erasing sender function<...> // + //! @tparam _Nothrow Whether the function type is declared noexcept, which + //! promises that the type-erased path (invoking the factory and connecting + //! the sender it returns) doesn't throw; the completion signatures describe + //! only the asynchronous contract + //! //! @tparam _Sigs The supported completion signatures //! //! @tparam _Queries The list of environment queries that must be supported @@ -336,9 +656,26 @@ 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 _Params __curried_params_t<_Args...>, the types with which the + //! constructors take the curried arguments. Naming them as a separate pack, + //! rather than expanding __curried_param_t<_Args>... in the constructors' + //! parameter lists, avoids an internal error in the name mangler of nvc++ + //! (seen with 26.1 through 26.9), which fails on a constructor template + //! whose parameter list begins with an empty expansion of a dependent type + //! and continues with a deduced one. + //! //! @tparam _Args The argument types used to construct the erased sender - template - class __function + template + class __function<_Nothrow, _Sigs, _Queries, _Attrs, __mlist<_Params...>, _Args...> { // check these with asserts rather than requires because the only way to // violate them is to circumvent the exec::function alias template so any @@ -348,6 +685,11 @@ namespace experimental::execution static_assert(__is_instance_of<_Attrs, attrs>); static_assert(__completion_signatures_and_domains_are_compatible<_Sigs, _Attrs>); + // the member-function specialization wraps a void-pointer specialization + template + friend class __function; + + protected: using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; template @@ -358,10 +700,28 @@ 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))); + // Ideally, function would allocate raw storage for the operation state + // from __alloc and construct it with construct_at. Instead, an + // operation state too big for the inline buffer is wrapped by the + // __any machinery that function shares with any_sender_of, and the + // wrapper is constructed and destroyed with + // allocator_traits<...>::construct and destroy. + // Uses-allocator construction never reaches the operation state: the + // wrapper doesn't declare allocator_type, and the operation state is + // initialized inside it directly from connect's result. What does + // leak is that a frame allocator's own construct and destroy members, + // if it has them, are called with the wrapper's type. That happens + // only when the function declares the get_frame_allocator query; + // otherwise __alloc is the injected polymorphic_allocator, which + // forwards only allocate and deallocate to the caller's allocator. + // Tolerated because frame allocators are unlikely to customize + // construct or destroy. A fix would give function its own path through + // __any (raw storage plus construct_at and destroy_at), leaving + // any_sender_of unchanged. return _any::_any_opstate_base(__in_place_from, std::allocator_arg, __alloc, @@ -370,10 +730,19 @@ namespace experimental::execution static_cast<__receiver_t &&>(__rcvr)); } - //! The curried arguments that will be passed to __make_sender_ from - //! inside __make_opstate_. - STDEXEC_ATTRIBUTE(no_unique_address) - __tuple<_Args...> __args_; + // The members are declared in order of decreasing alignment, with the + // curried arguments, whose alignment varies, last. That puts any padding + // at the end of the object, where it can be reused by a following + // member of an enclosing class when a function is a [[no_unique_address]] + // member or a base class. + + //! Storage for the sender factory passed to our constructor template; + //! __make_opstate_ will reconstitute the actual factory from this + //! bag-of-bytes with start_lifetime_as because it internally knows the + //! concrete type of the user-provided sender factory. The storage is + //! sized and aligned for the largest pointer to member the ABI can + //! produce (see __factory_storage_size). + alignas(__factory_storage_align) std::byte __make_sender_[__factory_storage_size]{}; //! The type-erased operation state factory; it points to a function that //! knows the concrete type of the sender factory stored in __make_sender_ //! so that it can construct the desired sender on demand and connect it @@ -381,33 +750,41 @@ namespace experimental::execution //! __make_sender_, the __any_receiver_ref to connect the sender to, and //! the arguments to pass to __make_sender_ to construct the sender. _any::_any_opstate_base (*__make_opstate_)(void *, __receiver_t, _Args &&...); - //! Storage for the sender factory passed to our constructor template; - //! __make_opstate_ will reconstitute the actual factory from this - //! bag-of-bytes with start_lifetime_as because it internally knows the - //! concrete type of the user-provided sender factory. We're reserving 2 * - //! sizeof(void *) bytes to permit the factory to be a pointer to member - //! function, which usually requires two pointers. - std::byte __make_sender_[2 * sizeof(void *)]{}; + //! The curried arguments that will be passed to __make_sender_ from + //! inside __make_opstate_. + STDEXEC_ATTRIBUTE(no_unique_address) + __tuple<_Args...> __args_; - public: - using sender_concept = sender_tag; + struct __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>> - constexpr explicit __function(_Args &&...__args, _Factory __factory) - noexcept(__nothrow_move_constructible<_Args...>) - : __args_(static_cast<_Args &&>(__args)...) - , __make_opstate_(&__mk_opstate<_Factory>) + template + constexpr explicit __function(_Params... __args, _Factory __factory, __tag) + noexcept((__nothrow_constructible_from<_Args, _Params> && ...)) + : __make_opstate_(&__mk_opstate<_Factory>) + , __args_(static_cast<_Params>(__args)...) { + static_assert(sizeof(_Factory) <= sizeof(__make_sender_)); + static_assert(alignof(_Factory) <= __factory_storage_align); + 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...> + && __meets_noexcept_contract<_Nothrow, _Factory, __receiver_t, _Args...> + constexpr explicit __function(_Params... __args, _Factory __factory) + noexcept((__nothrow_constructible_from<_Args, _Params> && ...)) + : __function(static_cast<_Params>(__args)..., __factory, __tag{}) + {} + //! this implementation of get_completion_signatures is taken directly //! from the equivalent function on any_sender_of template @@ -427,8 +804,17 @@ namespace experimental::execution return {}; } + //! connect is noexcept exactly when nothing on the path to the operation + //! state can throw: the function type is noexcept and the frame allocator + //! chosen from the receiver's environment allocates without throwing. If + //! a trusted domain transformation or a transformed sender's connect + //! throws anyway, std::terminate is called. template - constexpr auto connect(_Receiver __rcvr) && // + static constexpr bool __nothrow_connect = _Nothrow + && __opstate_t<_Receiver>::__nothrow_frame_allocation; + + template + constexpr auto connect(_Receiver __rcvr) && noexcept(__nothrow_connect<_Receiver>) // -> __opstate_t<_Receiver> { auto __factory = [this](_RcvrRef __rcvr) @@ -441,56 +827,268 @@ namespace experimental::execution return __opstate_t<_Receiver>{static_cast<_Receiver &&>(__rcvr), __factory}; } - template + //! as for connect() &&, plus copying the curried arguments mustn't throw + template requires __std::copy_constructible<__function> constexpr auto connect(_Receiver __rcvr) const & // - -> __opstate_t<_Receiver> + noexcept(__nothrow_connect<_Receiver> && __nothrow_copy_constructible<_Args...>) + -> __opstate_t<_Receiver> { return __function(*this).connect(static_cast<_Receiver &&>(__rcvr)); } }; - template class _Template, std::size_t... _Is> - consteval auto __canonicalize_splice(__indices<_Is...>) noexcept + //! 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 _Nothrow whether the function type is declared noexcept + //! \tparam _Sigs the function's 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 _Params __curried_params_t<_Args...>, as for the void-pointer form + //! \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<_Nothrow, _Sigs, _Queries, _Attrs, __mlist<_Params...>, _SelfBox, _Args...> { - return _Template<__msplice<_Types[_Is]>...>(); - } + using __void_pointer = _SelfBox::__void_pointer; + using __impl_t = __function<_Nothrow, + _Sigs, + _Queries, + _Attrs, + __curried_params_t<__void_pointer, _Args...>, + __void_pointer, + _Args...>; + + using __receiver_t = __impl_t::__receiver_t; + using __tag = __impl_t::__tag; - 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 + using __opstate_t = __impl_t::template __opstate_t<_Receiver>; + + // The void-pointer form does all the work. It's a member rather than a + // base so that a member-function function doesn't convert to it. + __impl_t __impl_; + + public: + using sender_concept = sender_tag; + + // 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...> + && __meets_noexcept_contract<_Nothrow, + _Factory, + __receiver_t, + __declared_self_t<_Self, _SelfBox>, + _Args...> + constexpr explicit __function(_Self &&__self, _Params... __args, _Factory __fact) + // like the base's constructor: storing the self pointer and the + // (pointer or empty) factory can't throw, so only initializing the + // curried arguments matters + noexcept((__nothrow_constructible_from<_Args, _Params> && ...)) + : __impl_(static_cast<__void_pointer>(std::addressof(__self)), + static_cast<_Params>(__args)..., + __self_adapting_factory<_Factory, __declared_self_t<_Self, _SelfBox>>{__fact}, + __tag{}) + {} + + template + static consteval auto get_completion_signatures() + { + static_assert(__decays_to_derived_from<_Self, __function>); + return __impl_t::template get_completion_signatures<__copy_cvref_t<_Self, __impl_t>, + _Env...>(); + } + + constexpr auto get_env() const noexcept + { + return __impl_.get_env(); + } + + template + constexpr auto connect(_Receiver __rcvr) && // + noexcept(__impl_t::template __nothrow_connect<_Receiver>) -> __opstate_t<_Receiver> + { + return static_cast<__impl_t &&>(__impl_).connect(static_cast<_Receiver &&>(__rcvr)); + } + + template + requires __std::copy_constructible<__impl_t> + constexpr auto connect(_Receiver __rcvr) const & // + noexcept(__impl_t::template __nothrow_connect<_Receiver> + && __nothrow_copy_constructible<__impl_t>) // + -> __opstate_t<_Receiver> + { + return __impl_.connect(static_cast<_Receiver &&>(__rcvr)); + } + }; + + template + struct __canonical; template