diff --git a/include/exec/__frame_allocator.hpp b/include/exec/__frame_allocator.hpp new file mode 100644 index 000000000..6a5f0e671 --- /dev/null +++ b/include/exec/__frame_allocator.hpp @@ -0,0 +1,157 @@ +/* Copyright (c) 2026 Ian Petersen + * Copyright (c) 2026 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "../stdexec/__detail/__concepts.hpp" + +#include +#include +#include +#include + +#include "../stdexec/__detail/__prologue.hpp" + +//! Defines template exec::__frame_allocator_t +//! +//! The intended use for __frame_allocator_t is the dynamic allocation of operation +//! states and/or coroutine frames. __frame_allocator_t models the Allocator named +//! requirement in terms of its type parameter, _Delegate. +//! +//! _Delegate may be one of: +//! - Another allocator, in which case __frame_allocator_t<_Delegate> is just _Delegate +//! rebound to std::byte; +//! - std::pmr::memory_resource*, in which case __frame_allocator_t<_Delegate> is just +//! std::pmr::polymorphic_allocator; or +//! - T* for some T that inherits from std::pmr::memor_resource, in which case +//! __frame_allocator_t is an allocator that behaves like +//! std::pmr::polymorphic_allocator, but knows the concrete type of its +//! memory_resource and so therefore may be able to avoid virtual dispatch. +//! +//! Given that __frame_allocator_t<_Delegate> is just an alias to _Delegate when _Delegate +//! is an allocator type, it's up to that type whether its construct member function does +//! "uses-allocator construction". When _Delegate is a pointer to a memory_resource, +//! __frame_allocator_t<_Delegate>::construct does "uses-allocator construction". +namespace experimental::execution +{ + namespace __fa + { + using namespace STDEXEC; + + template + struct __frame_allocator; + + //! Handle the case that _Delegate is an allocator already + //! + //! In this case, __frame_allocator_t is just an alias to _Delegate but + //! rebound to std::byte. + template + requires __simple_allocator<_Delegate> + struct __frame_allocator<_Delegate> + { + template + using type = std::allocator_traits<_Delegate>::template rebind_alloc<_Ty>; + }; + + //! Handle the case that _Delegate is exactly std::pmr::memory_resource * + //! + //! In this case, __frame_allocator_t<_Delegate> is exactly + //! std::pmr::polymorphic_allocator. + template <> + struct __frame_allocator + { + template + using type = std::pmr::polymorphic_allocator<_Ty>; + }; + + //! Handle the case that _Delegate is a pointer to a type that derives from + //! std::pmr::memory_resource + //! + //! In this case, __frame_allocator_t<_Delegate> is an allocator that behaves like + //! std::pmr::polymorphic_allocator except that it knows the concrete type + //! of its memory resource. In other words, allocation and deallocation are delegated + //! to the given resource, construct does uses-allocator construction, etc. + template + requires __std::derived_from<_Delegate, std::pmr::memory_resource> + struct __frame_allocator<_Delegate *> + { + template + struct type + { + using value_type = _Ty; + using pointer = value_type *; + + // polymorphic_allocator's default constructor grabs the default memory resource, + // which we can't do because there's no default for _Delegate + + /*implicit*/ constexpr type(_Delegate *__resource) noexcept + : __resource_(__resource) + {} + + constexpr type(type const &) noexcept = default; + + template + /*implicit*/ constexpr type(type<_Uy> const &other) noexcept + : __resource_(other.resource()) + {} + + constexpr ~type() = default; + + constexpr type &operator=(type const &) noexcept = default; + + constexpr pointer allocate(std::size_t __n) + { + return static_cast( + __resource_->allocate(__n * sizeof(value_type), alignof(value_type))); + } + + constexpr void deallocate(void *__p, std::size_t __n) noexcept + { + __resource_->deallocate(__p, __n * sizeof(value_type), alignof(value_type)); + } + + template + constexpr void construct(_Uy *__p, _Args &&...__args) + { + (void) std::uninitialized_construct_using_allocator(__p, + *this, + static_cast<_Args &&>(__args)...); + } + + template + constexpr void destroy(_Uy *__p) noexcept + { + __p->~_Uy(); + } + + constexpr _Delegate *resource() const noexcept + { + return __resource_; + } + + private: + _Delegate *__resource_; + }; + }; + } // namespace __fa + + template + using __frame_allocator_t = + __fa::__frame_allocator>::template type; +} // namespace experimental::execution + +namespace exec = experimental::execution; + +#include "../stdexec/__detail/__epilogue.hpp" diff --git a/include/exec/__memory_resource_adaptor.hpp b/include/exec/__memory_resource_adaptor.hpp new file mode 100644 index 000000000..50279cde9 --- /dev/null +++ b/include/exec/__memory_resource_adaptor.hpp @@ -0,0 +1,272 @@ +/* Copyright (c) 2026 Ian Petersen + * Copyright (c) 2026 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "../stdexec/__detail/__concepts.hpp" + +#include +#include +#include + +#include "../stdexec/__detail/__prologue.hpp" + +//! Defines template exec::__memory_resource_adaptor_t +//! +//! A "memory resource adaptor" adapts a "thing that can allocate memory" to the +//! std::pmr::memory_resource interface. It's used in exec::function when the function's +//! type parameters do not require that its eventual receiver have an environment that's +//! queryable with exec::get_frame_allocator. In those circumstances, function will ensure +//! that the receiver to which its erased sender is connected *does* have an environment +//! that responds to get_frame_allocator with a type-erased frame allocator. The +//! type-erased frame allocator is a std::pmr::polymorphic_allocator<>, and the +//! memory_resource given to it is a __memory_resoure_adaptor<_Adaptee>, where _Adaptee is +//! the type of "allocator" used to allocate the function's operation state. _Adaptee may +//! be one of: +//! - a type that models Allocator; +//! - std::pmr::memory_resource*; or +//! - T*, where T derives from std::pmr::memory_resource. +//! +//! Given an appropriate type, _Adaptee, __memory_resource_adaptor_t<_Adaptee> is a type +//! T, such that: +//! - T is constructible from an lvalue reference to an object of type _Adaptee; and +//! - given an object rsrc of type T, std::pmr::polymorphic_allocator<>(&rsrc) is a valid +//! expression. +namespace experimental::execution +{ + namespace __mem_rsc_adpt + { + using namespace STDEXEC; + + template + struct __memory_resource_adaptor; + + //! Handle the case that _Adaptee is exactly std::allocator + //! + //! Implement do_allocate and do_deallocate in terms of ::operator new and + //! ::operator delete rather than conservatively reimplementing aligned allocation + //! on top of an arbitrary allocator. + template <> + struct __memory_resource_adaptor> + { + struct type : std::pmr::memory_resource + { + template + constexpr explicit type(std::allocator<_Ty> const &) noexcept + {} + + constexpr void * + allocate(std::size_t __bytes, std::size_t __align = alignof(std::max_align_t)) const + { + return ::operator new(__bytes, static_cast(__align)); + } + + constexpr void deallocate(void *__p, + std::size_t __bytes, + std::size_t __align = alignof(std::max_align_t)) const noexcept + { + ::operator delete(__p, __bytes, static_cast(__align)); + } + + private: + constexpr void *do_allocate(std::size_t __bytes, std::size_t __align) final + { + return allocate(__bytes, __align); + } + + constexpr void + do_deallocate(void *__p, std::size_t __bytes, std::size_t __align) noexcept final + { + deallocate(__p, __bytes, __align); + } + + constexpr bool do_is_equal(std::pmr::memory_resource const &__other) const noexcept final + { + return !!dynamic_cast(&__other); + } + }; + }; + + //! Handle the case that _Adaptee is an allocator of std::bytes + //! + //! Implement do_allocate and do_deallocate in terms of _Adaptee's allocate and + //! deallocate, respectively. Implement do_is_equal in terms of _Adaptee's + //! operator==. + template + requires __simple_allocator<_Adaptee> + && __same_as::value_type> + struct __memory_resource_adaptor<_Adaptee> + { + //! Implement memory_resource in terms of an allocator + struct type : std::pmr::memory_resource + { + template + requires(!__same_as<_Alloc, type>) + constexpr explicit type(_Alloc const &__alloc) noexcept + : __alloc_(__alloc) + { + using __rebound_traits = std::allocator_traits<_Alloc>::template rebind_traits; + static_assert(__same_as<__traits, __rebound_traits>); + } + + constexpr void * + allocate(std::size_t __bytes, std::size_t __align = alignof(std::max_align_t)) + { + // When asking __alloc_ for __bytes number of bytes, the worst case is that + // the resulting address is byte-aligned, and we need to adjust right by + // (__align - 1) bytes to get a properly aligned buffer; since we might have to + // make that shift, we need to allocate too many bytes, possibly make the shift, + // and return the resulting address. Since we're going to return an address that + // might be offset from what we got back from __alloc_, we need a way to + // retrieve from the offset address what the original address was so can pass to + // deallocate a pointer that actually originally came from allocate. To do that, + // we store a copy of the source address at the end of the buffer. To make room + // for the possible rightward shift and the copy of a pointer, we need to + // allocate extra space, and __bytes + __align - 1 + sizeof(void *) is the most + // we might need. + std::size_t __upstreamSize = __bytes + __align - 1 + sizeof(void *); + void *const __buffer = __traits::allocate(__alloc_, __upstreamSize); + + void *__ptr = __buffer; + + void *__ret = std::align(__align, __bytes, __ptr, __upstreamSize); + + // by asking for as much extra storage as we did, std::align ought to succeed + STDEXEC_ASSERT(__ret != nullptr); + // this is a postcondition of a successful call to std::align + STDEXEC_ASSERT(__ret == __ptr); + // we're going to store the value of __buffer in the first sizeof(void*) bytes + // after the end of the returned buffer so there had better be room for that + STDEXEC_ASSERT(__upstreamSize >= (__bytes + sizeof(void *))); + + // put the address we got from __alloc_ at the end of the buffer + // we're going to return + auto *__as_bytes = new (__ret) std::byte[__upstreamSize]; + std::memcpy(__as_bytes + __bytes, &__buffer, sizeof(__buffer)); + + return __ret; + } + + constexpr void deallocate(void *__p, + std::size_t __bytes, + std::size_t __align = alignof(std::max_align_t)) noexcept + { + // we have to undo the bit-banging we did in allocate + + void *__address_to_free; + std::memcpy(&__address_to_free, static_cast(__p) + __bytes, sizeof(void *)); + + std::size_t __size_to_free = __bytes + __align - 1 + sizeof(void *); + + [=]() mutable noexcept + { + // std::align mutates its final two by-reference arguments so run this + // assertion inside an immediately-invoked lambda that captures the inputs by + // value + STDEXEC_ASSERT(std::align(__align, __bytes, __address_to_free, __size_to_free) == __p); + }(); + + __traits::deallocate(__alloc_, + new (__address_to_free) std::byte[__size_to_free], + __size_to_free); + } + + private: + using __traits = std::allocator_traits<_Adaptee>; + static_assert(__same_as); + typename __traits::allocator_type __alloc_; + + constexpr void *do_allocate(std::size_t __bytes, std::size_t __align) final + { + return allocate(__bytes, __align); + } + + constexpr void + do_deallocate(void *__p, std::size_t __bytes, std::size_t __align) noexcept final + { + deallocate(__p, __bytes, __align); + } + + constexpr bool do_is_equal(std::pmr::memory_resource const &__other) const noexcept override + { + if (auto *__ptr = dynamic_cast(&__other)) + { + return __alloc_ == __ptr->__alloc_; + } + + return false; + } + }; + }; + + //! Handle the case that _Adaptee is an allocator of some type other than std::byte + //! + //! We just rebind _Adaptee to be an allocator of std::bytes and inherit our nested + //! alias from the adaptor for that type. This strategy ensures that there's only one + //! adaptor for an entire family of adapted allocator types, reducing template bloat + //! and making the do_is_equals implementation sensible. + template + requires __simple_allocator<_Adaptee> + struct __memory_resource_adaptor<_Adaptee> + : __memory_resource_adaptor< + typename std::allocator_traits<_Adaptee>::template rebind_alloc> + { + // This class is the reason we have a nested type alias named type inside a + // constrained class template rather than just a constrained class template. We + // are not deriving a resource adaptor from another adaptor; we're deriving one + // meta-function from another so that we collapse the number of actual adaptor types + // to the minimum. + }; + + //! Handle the case that _Adaptee is a pointer to a type that derives from + //! std::pmr::memory_resource + //! + //! In this case, there's nothing to "adapt" but we need a type constructible + //! from _Adaptee*, and whose operator& returns _Adaptee*. + template + requires __std::constructible_from, _Adaptee *> + struct __memory_resource_adaptor<_Adaptee *> + { + struct type + { + explicit constexpr type(_Adaptee *__resource) noexcept + : __resource_(__resource) + {} + + constexpr _Adaptee *operator&() const noexcept + { + return __resource_; + } + + private: + _Adaptee *__resource_; + }; + }; + } // namespace __mem_rsc_adpt + + //! Adapt _Adaptee to be a std::pmr::memory_resource + //! + //! This alias is the identity when _Adaptee is a pointer to a type that derives from + //! std::pmr::memory_resource. When _Adaptee is an allocator type, it is a type that + //! derives from std::pmr::memory_resource and implements its pure-virtual member + //! functions in terms of that allocator type rebound to std::byte. + template + using __memory_resource_adaptor_t = + __mem_rsc_adpt::__memory_resource_adaptor>::type; +} // namespace experimental::execution + +namespace exec = experimental::execution; + +#include "../stdexec/__detail/__epilogue.hpp" diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 806e7ca2e..cd2787070 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -27,8 +27,11 @@ #include "../stdexec/__detail/__utility.hpp" #include "../stdexec/functional.hpp" +#include "__frame_allocator.hpp" +#include "__memory_resource_adaptor.hpp" // TODO: split this header into pieces #include "any_sender_of.hpp" +#include "get_frame_allocator.hpp" #include #include @@ -52,257 +55,341 @@ // queries to pick the frame allocator from the environment without relying on TLS. namespace experimental::execution { - //! A forwarding query for a "frame allocator", to be used for dynamically allocating - //! the operation states of senders type-erased by exec::function. - struct get_frame_allocator_t : STDEXEC::__query + namespace __func { - using STDEXEC::__query::operator(); + using namespace STDEXEC; - constexpr auto operator()() const noexcept - { - return STDEXEC::read_env(get_frame_allocator_t{}); - } + //! given the concrete receiver's environment, choose the frame allocator; first + //! choice is the result of get_frame_allocator(env), second choice is + //! get_allocator(env), and the default is std::allocator + inline constexpr auto __choose_frame_allocator = + __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; - template - static constexpr void __validate() 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. + //! + //! This wrapper exists primarily as a hook for injecting a defaulted frame allocator + //! when _Receiver *doesn't* have get_frame_allocator in its environment. That + //! injection happens in the partial specialization, below. + template + struct __receiver_wrapper : public _Receiver { - static_assert(STDEXEC::__nothrow_callable); - using __alloc_t = STDEXEC::__call_result_t; - static_assert(STDEXEC::__simple_allocator>); - } + template + constexpr explicit __receiver_wrapper(_Opstate *__opstate) + : _Receiver(__opstate->__rcvr_) + {} + }; - static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool + //! 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, and + //! inject an environment that contains a defaulted frame allocator. + //! + //! This partial specialization handles the case that _Receiver doesn't have a frame + //! allocator in its environment, in which case we need to provide a type-erasing + //! frame allocator in the injected environment because we won't know the concrete + //! type of the allocator that's actually used as our frame allocator until we're + //! connected to a concrete receiver. We could provide either + //! std::pmr::memory_resource*, or std::pmr::polymorphic_allocator<> with basically + //! the same tradeoffs so we provide an allocator rather than a memory resource to + //! better match the name of the injected query. + template + requires(!__queryable_with, get_frame_allocator_t>) + struct __receiver_wrapper<_Receiver> : public _Receiver { - return true; - } - }; + //! the injected query response for the frame allocator + using __prop_t = prop>; - inline constexpr get_frame_allocator_t get_frame_allocator{}; + template + constexpr explicit __receiver_wrapper(_Opstate *__opstate) + : _Receiver(__opstate->__rcvr_) + , __env_(&__opstate->__env_) + {} - namespace _func - { - using namespace STDEXEC; + constexpr auto get_env() const noexcept // + -> __join_env_t<__prop_t, env_of_t<_Receiver>> + { + return __env::__join(*__env_, STDEXEC::get_env(*static_cast<_Receiver const *>(this))); + } - //! given the concrete receiver's environment, choose the frame allocator; first - //! choice is the result of get_frame_allocator(env), second choice is - //! get_allocator(env), and the default is std::allocator - inline constexpr auto choose_frame_allocator = - __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; + private: + __prop_t *__env_; + }; + + template + using __any_receiver_ref = ::exec::_any::_any_receiver_ref<_Sigs, _Queries>; + + template + struct __opstate_base + { + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; + using __stop_token_t = stop_token_of_t>; + + _any::_state<_Receiver, __stop_token_t> __rcvr_; + }; + + template + requires( + !__queryable_with>, get_frame_allocator_t>) + struct __opstate_base<_Receiver, _Sigs, _Queries> + { + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; + using __prop_t = __receiver_t::__prop_t; + using __stop_token_t = stop_token_of_t>; + using __adaptee_t = decltype(__choose_frame_allocator(__declval<_Receiver const &>())); + + __memory_resource_adaptor_t<__adaptee_t> __resource_; + __prop_t __env_; + _any::_state<_Receiver, __stop_token_t> __rcvr_; + + constexpr explicit __opstate_base(_Receiver &&__rcvr) + : __resource_(__choose_frame_allocator(__rcvr)) + , __env_{get_frame_allocator, &__resource_} + , __rcvr_(static_cast<_Receiver &&>(__rcvr)) + {} + }; //! The concrete operation state resulting from connecting a function<...> to a //! concrete receiver of type Receiver. This type manages an _any::_any_opstate_base //! instance, which is the type-erased operation state resulting from connecting the //! type-erased sender to an _any::_any_receiver_ref with the given completion //! signatures and queries. - template - class _opstate + template + class __opstate : public __opstate_base<_Receiver, _Sigs, _Queries> { - using _receiver_t = ::exec::_any::_any_receiver_ref; - using _stop_token_t = stop_token_of_t>; + using __base = __opstate_base<_Receiver, _Sigs, _Queries>; + using typename __base::__receiver_t; - //! rcvr_ has to be initialized before op_ because our implementation of get_env is - //! empirically accessed during our constructor and depends on rcvr_ being - //! initialized - _any::_state rcvr_; - _any::_any_opstate_base op_; + _any::_any_opstate_base __op_; public: using operation_state_concept = operation_state_tag; - template - explicit constexpr _opstate(Receiver rcvr, Factory factory) - : rcvr_(static_cast(rcvr)) - , op_(factory(_receiver_t(rcvr_))) + template + explicit constexpr __opstate(_Receiver __rcvr, _Factory __factory) + : __base(static_cast<_Receiver &&>(__rcvr)) + , __op_(__factory(__receiver_t(this))) {} constexpr void start() & noexcept { - op_.start(); + __op_.start(); } }; - template - class _function; + template + class __function; //! the main implementation of the type-erasing sender function<...> // - //! \tparam Sigs The supported completion signatures + //! \tparam _Sigs The supported completion signatures //! - //! \tparam Queries The list of environment queries that must be supported by + //! \tparam _Queries The list of environment queries that must be supported by //! the eventual receiver; it's a pack of function type like Return(Query, Args...) or //! Return(Query, Args...) noexcept. The named query, when given the specified //! arguments, must return a value convertible to Return, and it must be noexcept, or //! not, as appropriate //! - //! \tparam Args The argument types used to construct the erased sender - template - class _function, Args...> + //! \tparam _Args The argument types used to construct the erased sender + template + class __function<_Sigs, queries<_Queries...>, _Args...> { - using _receiver_t = ::exec::_any::_any_receiver_ref>; + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, queries<_Queries...>>>; - template - using _opstate_t = _opstate>; + template + using __opstate_t = __opstate<_Receiver, _Sigs, queries<_Queries...>>; - template - static constexpr auto _mk_opstate(void *storage, _receiver_t rcvr, Args &&...args) // + template + static constexpr auto + __mk_opstate(void *__storage, __receiver_t __rcvr, _Args &&...__args) // -> _any::_any_opstate_base { - auto &make_sender = *__std::start_lifetime_as(storage); - auto alloc = choose_frame_allocator(get_env(rcvr)); + 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))); return _any::_any_opstate_base(__in_place_from, std::allocator_arg, - alloc, + __alloc, STDEXEC::connect, - __invoke(make_sender, static_cast(args)...), - static_cast<_receiver_t &&>(rcvr)); + __invoke(__make_sender, static_cast<_Args &&>(__args)...), + static_cast<__receiver_t &&>(__rcvr)); } - //! The curried arguments that will be passed to make_sender_ from inside make_opstate_. + //! The curried arguments that will be passed to __make_sender_ from inside + //! __make_opstate_. STDEXEC_ATTRIBUTE(no_unique_address) - __tuple args_; + __tuple<_Args...> __args_; //! 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 + //! concrete type of the sender factory stored in __make_sender_ so that it can //! construct the desired sender on demand and connect it to the given receiver. The - //! expected arguments are the address of make_sender_, the _any_receiver_ref to - //! connect the sender to, and the arguments to pass to make_sender_ to construct + //! expected arguments are the address of __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 *)]{}; + _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 *)]{}; public: using sender_concept = sender_tag; - template <__invocable Factory> - requires __not_decays_to // - && (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) // - && (sizeof(Factory) <= sizeof(make_sender_)) // - && sender_to<__invoke_result_t, _receiver_t> - constexpr explicit _function(Args &&...args, Factory factory) - noexcept(__nothrow_move_constructible) - : args_(static_cast(args)...) - , make_opstate_(&_mk_opstate) + 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> + constexpr explicit __function(_Args &&...__args, _Factory __factory) + noexcept(__nothrow_move_constructible<_Args...>) + : __args_(static_cast<_Args &&>(__args)...) + , __make_opstate_(&__mk_opstate<_Factory>) { - std::memcpy(make_sender_, std::addressof(factory), sizeof(Factory)); + std::memcpy(__make_sender_, std::addressof(__factory), sizeof(_Factory)); } //! this implementation of get_completion_signatures is taken directly from the //! equivalent function on any_sender_of - template + template static consteval auto get_completion_signatures() { - static_assert(__decays_to_derived_from); - //! throw if Env does not contain the queries needed to type-erase the receiver: - using _check_queries_t = __mfind_error<_any::_check_query_t...>; - if constexpr (__merror<_check_queries_t>) - return __throw_compile_time_error(_check_queries_t()); + 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()); else - return Sigs(); + return _Sigs(); } - template - constexpr auto connect(Receiver rcvr) && -> _opstate_t + template + constexpr auto connect(_Receiver __rcvr) && // + -> __opstate_t<_Receiver> { - auto factory = [this](RcvrRef rcvr) + auto __factory = [this](_RcvrRef __rcvr) { - return __apply(make_opstate_, - static_cast<__tuple &&>(args_), - make_sender_, - static_cast(rcvr)); + return __apply(__make_opstate_, + static_cast<__tuple<_Args...> &&>(__args_), + __make_sender_, + static_cast<_RcvrRef &&>(__rcvr)); }; - return _opstate_t{static_cast(rcvr), factory}; + return __opstate_t<_Receiver>{static_cast<_Receiver &&>(__rcvr), __factory}; } - template - requires __std::copy_constructible<_function> - constexpr auto connect(Receiver rcvr) const & -> _opstate_t + template + requires __std::copy_constructible<__function> + constexpr auto connect(_Receiver __rcvr) const & // + -> __opstate_t<_Receiver> { - return _function(*this).connect(static_cast(rcvr)); + return __function(*this).connect(static_cast<_Receiver &&>(__rcvr)); } }; - template class Template, std::size_t... Is> - consteval auto _canonicalize_splice(__indices) noexcept + template class _Template, std::size_t... _Is> + consteval auto __canonicalize_splice(__indices<_Is...>) noexcept { - return Template<__msplice...>(); + return _Template<__msplice<_Types[_Is]>...>(); } - template - consteval auto _canonicalize_impl(__static_vector<__type_index, Size> types) noexcept + 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; + std::ranges::sort(__types); + auto const __rest = std::ranges::unique(__types); + __types.erase(__rest.begin(), __types.end()); + return __types; } - template