diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 7dd717aae..806e7ca2e 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -34,27 +34,26 @@ #include #include -// This file defines function, which is a -// type-erased sender that can complete with +// This file defines function, which is a type-erased sender +// that can complete with: +// // - set_value(ReturnType) // - set_error(std::exception_ptr) // - set_stopped() // -// 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 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, assuming that a -// std::tuple qualifies as "ABI-stable". 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 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. 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. + //! 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 { using STDEXEC::__query::operator(); @@ -84,23 +83,26 @@ namespace experimental::execution { using namespace STDEXEC; - template - class _func_op; - - // 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 _func_op, Queries...> + //! 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()}}; + + //! 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 { - using _receiver_t = - ::exec::_any::_any_receiver_ref, queries>; - + using _receiver_t = ::exec::_any::_any_receiver_ref; using _stop_token_t = stop_token_of_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 + //! 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_; @@ -108,422 +110,223 @@ namespace experimental::execution using operation_state_concept = operation_state_tag; template - explicit constexpr _func_op(Receiver rcvr, Factory factory) + explicit constexpr _opstate(Receiver rcvr, Factory factory) : rcvr_(static_cast(rcvr)) , op_(factory(_receiver_t(rcvr_))) {} - _func_op(_func_op &&) = delete; - - constexpr ~_func_op() = default; - constexpr void start() & noexcept { op_.start(); } }; - // 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 = - STDEXEC::__first_callable{get_frame_allocator, - get_allocator, - STDEXEC::__always{std::allocator()}}; - - template - class _func_impl; + template + class _function; - // the main implementation of the type-erasing sender function<...> + //! the main implementation of the type-erasing sender function<...> // - // SndrCncpt should be std::execution::sender_concept - // Args... is the argument types used to construct the erased sender - // Sigs... is the supported completion signatures - // Queries... is 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 - template - class _func_impl, queries> + //! \tparam Sigs The supported completion signatures + //! + //! \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...> { - using _receiver_t = - ::exec::_any::_any_receiver_ref, queries>; + using _receiver_t = ::exec::_any::_any_receiver_ref>; template - using _func_op_t = _func_op, Queries...>; - - // 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 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 the sender. - _any::_any_opstate_base (*make_op_)(void *, _receiver_t, Args &&...); - // Storage for the sender factory passed to our constructor template; make_op_ 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_op_. + using _opstate_t = _opstate>; + + 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)); + return _any::_any_opstate_base(__in_place_from, + std::allocator_arg, + alloc, + STDEXEC::connect, + __invoke(make_sender, static_cast(args)...), + static_cast<_receiver_t &&>(rcvr)); + } + + //! The curried arguments that will be passed to make_sender_ from inside make_opstate_. STDEXEC_ATTRIBUTE(no_unique_address) - STDEXEC::__tuple args_; + __tuple 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 + //! 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 + //! 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 *)]{}; public: - using sender_concept = SndrCncpt; - - template Factory> - requires STDEXEC::__not_decays_to // - && (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) // - && (sizeof(Factory) <= sizeof(make_sender_)) // - && STDEXEC::sender_to, _receiver_t> - constexpr explicit _func_impl(Args &&...args, Factory factory) - noexcept(STDEXEC::__nothrow_move_constructible) + 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) { - using sender_t = __invoke_result_t; - std::memcpy(make_sender_, std::addressof(factory), sizeof(Factory)); - - make_op_ = [](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)); - - return _any::_any_opstate_base(__in_place_from, - std::allocator_arg, - alloc, - STDEXEC::connect, - STDEXEC::__invoke(make_sender, - static_cast(args)...), - static_cast<_receiver_t &&>(rcvr)); - }; } - template <__std::derived_from<_func_impl> Func> - requires __not_decays_to - constexpr _func_impl(Func &&other) noexcept(__nothrow_move_constructible<__tuple>) - : _func_impl(static_cast<_func_impl &&>(other)) - {} - - template <__std::derived_from<_func_impl> Func> - requires __not_decays_to && __std::copy_constructible<__tuple> - constexpr _func_impl(Func const &other) - noexcept(__nothrow_copy_constructible<__tuple>) - : _func_impl(static_cast<_func_impl const &>(other)) - {} - - // this implementation of get_completion_signatures is taken directly from - // the equivalent function on any_sender_of + //! this implementation of get_completion_signatures is taken directly from the + //! equivalent function on any_sender_of template static consteval auto get_completion_signatures() { - static_assert(__std::derived_from, _func_impl>); - - // throw if Env does not contain the queries needed to type-erase the receiver: + 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 STDEXEC::__throw_compile_time_error(_check_queries_t{}); - } + return __throw_compile_time_error(_check_queries_t()); else - { - return completion_signatures{}; - } + return Sigs(); } template - constexpr _func_op_t connect(Receiver rcvr) && + constexpr auto connect(Receiver rcvr) && -> _opstate_t { - return _func_op_t{static_cast(rcvr), - [this](RcvrRef rcvr) - { - return STDEXEC::__apply(make_op_, - static_cast<__tuple &&>( - args_), - make_sender_, - static_cast(rcvr)); - }}; + auto factory = [this](RcvrRef rcvr) + { + return __apply(make_opstate_, + static_cast<__tuple &&>(args_), + make_sender_, + static_cast(rcvr)); + }; + return _opstate_t{static_cast(rcvr), factory}; } template - requires STDEXEC::__std::copy_constructible<_func_impl> - constexpr _func_op_t connect(Receiver rcvr) const & + requires __std::copy_constructible<_function> + constexpr auto connect(Receiver rcvr) const & -> _opstate_t { - return _func_impl(*this).connect(static_cast(rcvr)); + return _function(*this).connect(static_cast(rcvr)); } }; - template - struct _canonical_fn; - - template - struct _canonical_fn> + template class Template, std::size_t... Is> + consteval auto _canonicalize_splice(__indices) noexcept { - consteval auto operator()() const noexcept - { - constexpr auto make_sigs = []() noexcept - { - return __cmplsigs::__to_array(completion_signatures{}); - }; - - return __cmplsigs::__completion_sigs_from(make_sigs); - } - }; + return Template<__msplice...>(); + } - template - struct _canonical_fn> + template + consteval auto _canonicalize_impl(__static_vector<__type_index, Size> types) noexcept { - private: - // sort and unique the function types in Queries... into an array of __mtypeids - static consteval auto get_sigs() noexcept - { - using sig_array_t = __static_vector<__type_index, sizeof...(Queries)>; - auto sigs = sig_array_t{__mtypeid...}; - - std::ranges::sort(sigs); + std::ranges::sort(types); + auto const rest = std::ranges::unique(types); + types.erase(rest.begin(), types.end()); + return types; + } - auto const end = std::ranges::unique(sigs).begin(); - sigs.erase(end, sigs.end()); + template