From 94bcdac0e1cce9c0a8cf2e103d547029567d5e5c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 13 May 2026 12:55:24 -0700 Subject: [PATCH 01/13] Split get_frame_allocator out of function.hpp I'm about to write a new algorithm that injects a frame allocator into its child's environment, so I'm going to need to be able to refer to `get_frame_allocator_t` but I don't need to refer to `function` so I'm splitting the declarations. --- include/exec/function.hpp | 28 +-------------- include/exec/get_frame_allocator.hpp | 54 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 include/exec/get_frame_allocator.hpp diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 806e7ca2e..53879acfd 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -29,6 +29,7 @@ // TODO: split this header into pieces #include "any_sender_of.hpp" +#include "get_frame_allocator.hpp" #include #include @@ -52,33 +53,6 @@ // 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 - { - using STDEXEC::__query::operator(); - - constexpr auto operator()() const noexcept - { - return STDEXEC::read_env(get_frame_allocator_t{}); - } - - template - static constexpr void __validate() noexcept - { - static_assert(STDEXEC::__nothrow_callable); - using __alloc_t = STDEXEC::__call_result_t; - static_assert(STDEXEC::__simple_allocator>); - } - - static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool - { - return true; - } - }; - - inline constexpr get_frame_allocator_t get_frame_allocator{}; - namespace _func { using namespace STDEXEC; diff --git a/include/exec/get_frame_allocator.hpp b/include/exec/get_frame_allocator.hpp new file mode 100644 index 000000000..d3f98667e --- /dev/null +++ b/include/exec/get_frame_allocator.hpp @@ -0,0 +1,54 @@ +/* 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 "../stdexec/__detail/__query.hpp" +#include "../stdexec/__detail/__read_env.hpp" +#include "../stdexec/__detail/__type_traits.hpp" +#include "../stdexec/__detail/__utility.hpp" + +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 + { + using STDEXEC::__query::operator(); + + constexpr auto operator()() const noexcept + { + return STDEXEC::read_env(get_frame_allocator_t{}); + } + + template + static constexpr void __validate() noexcept + { + static_assert(STDEXEC::__nothrow_callable); + using __alloc_t = STDEXEC::__call_result_t; + static_assert(STDEXEC::__simple_allocator>); + } + + static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool + { + return true; + } + }; + + inline constexpr get_frame_allocator_t get_frame_allocator{}; +} // namespace experimental::execution + +namespace exec = experimental::execution; From 4282975f0ce679ff6958d810f872b21ca18c2dd1 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 15 May 2026 19:58:32 -0700 Subject: [PATCH 02/13] Allow get_frame_allocator to return both allocators and memory_resources Also, add an allocator named `__frame_allocator` that's basically `polymorphic_allocator` but knows the concrete type of the `memory_resource*` it uses for allocations. --- include/exec/__frame_allocator.hpp | 95 ++++++++++++++++++++++++++++ include/exec/get_frame_allocator.hpp | 4 +- test/exec/CMakeLists.txt | 1 + test/exec/test_frame_allocator.cpp | 59 +++++++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 include/exec/__frame_allocator.hpp create mode 100644 test/exec/test_frame_allocator.cpp diff --git a/include/exec/__frame_allocator.hpp b/include/exec/__frame_allocator.hpp new file mode 100644 index 000000000..d17b18074 --- /dev/null +++ b/include/exec/__frame_allocator.hpp @@ -0,0 +1,95 @@ + +/* 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 + +namespace experimental::execution +{ + namespace __fa + { + using namespace STDEXEC; + + template + struct __frame_allocator; + + template + requires __simple_allocator<_Delegate> + struct __frame_allocator<_Delegate> + { + template + using type = std::allocator_traits<_Delegate>::template rebind_alloc<_Ty>; + }; + + template <> + struct __frame_allocator + { + template + using type = std::pmr::polymorphic_allocator<_Ty>; + }; + + 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 *; + + /*implicit*/ constexpr type(_Delegate *__resource) noexcept + : __resource_(__resource) + {} + + constexpr type(type const &) noexcept = default; + + template + constexpr type(type<_Uy> const &other) noexcept + : __resource_(other.__resource_) + {} + + constexpr type &operator=(type &) noexcept = default; + + constexpr ~type() = default; + + constexpr pointer allocator(std::size_t __n) + { + return static_cast( + __resource_->allocate(__n * sizeof(value_type), alignof(value_type))); + } + + constexpr void delegate(void *__p, std::size_t __n) noexcept + { + __resource_->deallocate(__p, __n * sizeof(value_type), alignof(value_type)); + } + + private: + _Delegate *__resource_; + }; + }; + + template + using __frame_allocator_t = + __frame_allocator>::template type; + } // namespace __fa +} // namespace experimental::execution + +namespace exec = experimental::execution; diff --git a/include/exec/get_frame_allocator.hpp b/include/exec/get_frame_allocator.hpp index d3f98667e..e266808bc 100644 --- a/include/exec/get_frame_allocator.hpp +++ b/include/exec/get_frame_allocator.hpp @@ -39,7 +39,9 @@ namespace experimental::execution { static_assert(STDEXEC::__nothrow_callable); using __alloc_t = STDEXEC::__call_result_t; - static_assert(STDEXEC::__simple_allocator>); + static_assert( + STDEXEC::__std::constructible_from, __alloc_t> + || STDEXEC::__simple_allocator>); } static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 388143fad..d3f4b6b3e 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -62,6 +62,7 @@ set(exec_test_sources $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp test_function.cpp + test_frame_allocator.cpp ) set_source_files_properties(test_any_sender.cpp diff --git a/test/exec/test_frame_allocator.cpp b/test/exec/test_frame_allocator.cpp new file mode 100644 index 000000000..93226550d --- /dev/null +++ b/test/exec/test_frame_allocator.cpp @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#include + +#include + +#include + +#include +#include + +namespace ex = STDEXEC; + +namespace +{ + TEST_CASE("exec::__frame_allocator is constructible", "[types][frame_allocator]") + { + using namespace exec::__fa; + + SECTION("with a memory_resource*") + { + std::pmr::memory_resource* rsc = std::pmr::new_delete_resource(); + __frame_allocator_t fa(rsc); + + STATIC_REQUIRE(std::same_as>); + } + + SECTION("with a specific resource") + { + std::pmr::unsynchronized_pool_resource rsc; + __frame_allocator_t fa(&rsc); + + STATIC_REQUIRE(!std::same_as>); + } + + SECTION("with an allocator") + { + std::allocator rsc; + __frame_allocator_t fa(rsc); + + STATIC_REQUIRE(std::same_as>); + } + } +} // namespace From 4d17bf7e214754141e14cf507a5b3e5cc29c5297 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 16 May 2026 08:10:12 -0700 Subject: [PATCH 03/13] Wire __frame_allocator_t into function Use the new `exec::__fa::__frame_allocator_t` alias template in `exec::function`'s allocator-selection logic, in anticipation of supporting a wider variety of allocators. --- include/exec/function.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 53879acfd..d0ae56382 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -27,6 +27,7 @@ #include "../stdexec/__detail/__utility.hpp" #include "../stdexec/functional.hpp" +#include "__frame_allocator.hpp" // TODO: split this header into pieces #include "any_sender_of.hpp" #include "get_frame_allocator.hpp" @@ -122,7 +123,8 @@ namespace experimental::execution -> _any::_any_opstate_base { auto &make_sender = *__std::start_lifetime_as(storage); - auto alloc = choose_frame_allocator(get_env(rcvr)); + using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); + auto alloc = __fa::__frame_allocator_t(choose_frame_allocator(get_env(rcvr))); return _any::_any_opstate_base(__in_place_from, std::allocator_arg, alloc, From 47701793355d3975e14a1117b215a3e36b1622be Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 16 May 2026 09:27:08 -0700 Subject: [PATCH 04/13] Guarantee that function advertises a frame allocator to children With this change, you don't need to add `some_allocator(get_frame_allocator_t)` to the list of type-erased queries for the erased operation to see that its receiver's environment contains a frame allocator. If the list of type-erased queries *does* contain that type, it's preferred, allowing the user to override the return type of `get_frame_allocator` but we otherwise provide a `std::pmr::polymorphic_allocator` that is backed by whichever allocator was actually used to allocate the operation state. --- include/exec/function.hpp | 149 +++++++++++++++++++++++++++++++++--- test/exec/test_function.cpp | 6 +- 2 files changed, 139 insertions(+), 16 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d0ae56382..ecfc35048 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -64,30 +64,157 @@ namespace experimental::execution inline constexpr auto choose_frame_allocator = __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; + template + struct _choose_receiver + { + struct type : Receiver + { + template + constexpr explicit type(Opstate *opstate) + : Receiver(opstate->rcvr_) + {} + }; + }; + + template + requires(!__queryable_with, get_frame_allocator_t>) + struct _choose_receiver + { + class type : public Receiver + { + using _prop_t = prop>; + _prop_t *env_; + + public: + template + constexpr explicit type(Opstate *opstate) + : Receiver(opstate->rcvr_) + , env_(&opstate->env_) + {} + + constexpr auto get_env() const noexcept -> __join_env_t<_prop_t, env_of_t> + { + return __env::__join(*env_, STDEXEC::get_env(*static_cast(this))); + } + }; + }; + + template + using _any_receiver_ref = ::exec::_any::_any_receiver_ref; + + template + using _choose_receiver_t = _choose_receiver::type; + + template + struct _opstate_base + { + using _receiver_t = _choose_receiver_t<_any_receiver_ref>; + using _stop_token_t = stop_token_of_t>; + + _any::_state rcvr_; + }; + + template + struct _memory_resource_adaptor; + + template + requires __simple_allocator + && __same_as::value_type> + struct _memory_resource_adaptor + { + class type : public std::pmr::memory_resource + { + using traits = std::allocator_traits; + static_assert(__same_as); + typename traits::allocator_type alloc_; + + public: + template + requires __same_as< + traits, + typename std::allocator_traits::template rebind_traits> + explicit type(Alloc const &alloc) noexcept + : alloc_(alloc) + {} + + constexpr void *do_allocate(std::size_t __bytes, std::size_t __align) override + { + return traits::allocate(alloc_, __bytes); + } + + constexpr void do_deallocate(void *__p, std::size_t __bytes, std::size_t __align) override + { + traits::deallocate(alloc_, new (__p) std::byte[__bytes], __bytes); + } + + 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; + } + }; + }; + + template + requires __simple_allocator + struct _memory_resource_adaptor + : _memory_resource_adaptor< + typename std::allocator_traits::template rebind_alloc> + {}; + + template + requires __std::constructible_from, Adaptee> + struct _memory_resource_adaptor + { + using type = Adaptee; + }; + + template + using _memory_resource_adaptor_t = _memory_resource_adaptor>::type; + + template + requires(!__queryable_with>, get_frame_allocator_t>) + struct _opstate_base + { + using _receiver_t = _choose_receiver_t<_any_receiver_ref>; + using _stop_token_t = stop_token_of_t>; + using _adaptee_t = decltype(choose_frame_allocator(__declval())); + + _memory_resource_adaptor_t<_adaptee_t> resource_; + _any::_state rcvr_; + prop> env_; + + constexpr explicit _opstate_base(Receiver &&rcvr) + : resource_(choose_frame_allocator(rcvr)) + , rcvr_(static_cast(rcvr)) + , env_{get_frame_allocator, &resource_} + {} + }; + //! 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 + class _opstate : public _opstate_base { - using _receiver_t = ::exec::_any::_any_receiver_ref; - using _stop_token_t = stop_token_of_t>; + using _base = _opstate_base; + 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_))) + : _base(static_cast(rcvr)) + , op_(factory(_receiver_t(this))) {} constexpr void start() & noexcept @@ -113,7 +240,7 @@ namespace experimental::execution template class _function, Args...> { - using _receiver_t = ::exec::_any::_any_receiver_ref>; + using _receiver_t = _choose_receiver_t<_any_receiver_ref>>; template using _opstate_t = _opstate>; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index b80c2d2d3..6283df70e 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -139,11 +139,7 @@ namespace TEST_CASE("exec::function forwards get_frame_allocator", "[types][function]") { - // TODO: you probably shouldn't have to specify the frame allocator query like this - using Queries = exec::queries( - exec::get_frame_allocator_t) noexcept>; - - exec::function sndr( + exec::function sndr( []() noexcept { return ex::read_env(exec::get_frame_allocator) From dfeac40e9611add5bf983d57665bf2d51fe5ed4d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 16 May 2026 13:49:47 -0700 Subject: [PATCH 05/13] Tidy up exec/function.hpp a little --- include/exec/function.hpp | 100 +++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index ecfc35048..a7ccfc2ff 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -65,55 +65,39 @@ namespace experimental::execution __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; template - struct _choose_receiver + struct _receiver_wrapper : public Receiver { - struct type : Receiver - { - template - constexpr explicit type(Opstate *opstate) - : Receiver(opstate->rcvr_) - {} - }; + template + constexpr explicit _receiver_wrapper(Opstate *opstate) + : Receiver(opstate->rcvr_) + {} }; template requires(!__queryable_with, get_frame_allocator_t>) - struct _choose_receiver + struct _receiver_wrapper : public Receiver { - class type : public Receiver - { - using _prop_t = prop>; - _prop_t *env_; + using _prop_t = prop>; - public: - template - constexpr explicit type(Opstate *opstate) - : Receiver(opstate->rcvr_) - , env_(&opstate->env_) - {} + template + constexpr explicit _receiver_wrapper(Opstate *opstate) + : Receiver(opstate->rcvr_) + , env_(&opstate->env_) + {} - constexpr auto get_env() const noexcept -> __join_env_t<_prop_t, env_of_t> - { - return __env::__join(*env_, STDEXEC::get_env(*static_cast(this))); - } - }; + constexpr auto get_env() const noexcept // + -> __join_env_t<_prop_t, env_of_t> + { + return __env::__join(*env_, STDEXEC::get_env(*static_cast(this))); + } + + private: + _prop_t *env_; }; template using _any_receiver_ref = ::exec::_any::_any_receiver_ref; - template - using _choose_receiver_t = _choose_receiver::type; - - template - struct _opstate_base - { - using _receiver_t = _choose_receiver_t<_any_receiver_ref>; - using _stop_token_t = stop_token_of_t>; - - _any::_state rcvr_; - }; - template struct _memory_resource_adaptor; @@ -122,6 +106,7 @@ namespace experimental::execution && __same_as::value_type> struct _memory_resource_adaptor { + //! Implement memory_resource in terms of an allocator class type : public std::pmr::memory_resource { using traits = std::allocator_traits; @@ -130,12 +115,13 @@ namespace experimental::execution public: template - requires __same_as< - traits, - typename std::allocator_traits::template rebind_traits> - explicit type(Alloc const &alloc) noexcept + requires(!__same_as) + constexpr explicit type(Alloc const &alloc) noexcept : alloc_(alloc) - {} + { + using rebound_traits = std::allocator_traits::template rebind_traits; + static_assert(__same_as); + } constexpr void *do_allocate(std::size_t __bytes, std::size_t __align) override { @@ -162,6 +148,9 @@ namespace experimental::execution template requires __simple_allocator struct _memory_resource_adaptor + //! for an arbitrary allocator, inherit from the adaptor that's implemented + //! in terms of that allocator rebound to std::byte to minimize template + //! instantiations, and to make the dynamic_cast in do_is_equal work : _memory_resource_adaptor< typename std::allocator_traits::template rebind_alloc> {}; @@ -170,28 +159,39 @@ namespace experimental::execution requires __std::constructible_from, Adaptee> struct _memory_resource_adaptor { + //! no need to "adapt" a memory_resource using type = Adaptee; }; template using _memory_resource_adaptor_t = _memory_resource_adaptor>::type; + template + struct _opstate_base + { + using _receiver_t = _receiver_wrapper<_any_receiver_ref>; + using _stop_token_t = stop_token_of_t>; + + _any::_state rcvr_; + }; + template requires(!__queryable_with>, get_frame_allocator_t>) struct _opstate_base { - using _receiver_t = _choose_receiver_t<_any_receiver_ref>; + using _receiver_t = _receiver_wrapper<_any_receiver_ref>; + using _prop_t = _receiver_t::_prop_t; using _stop_token_t = stop_token_of_t>; using _adaptee_t = decltype(choose_frame_allocator(__declval())); - _memory_resource_adaptor_t<_adaptee_t> resource_; - _any::_state rcvr_; - prop> env_; + _memory_resource_adaptor_t<_adaptee_t> resource_; + _prop_t env_; + _any::_state rcvr_; constexpr explicit _opstate_base(Receiver &&rcvr) : resource_(choose_frame_allocator(rcvr)) - , rcvr_(static_cast(rcvr)) , env_{get_frame_allocator, &resource_} + , rcvr_(static_cast(rcvr)) {} }; @@ -240,7 +240,7 @@ namespace experimental::execution template class _function, Args...> { - using _receiver_t = _choose_receiver_t<_any_receiver_ref>>; + using _receiver_t = _receiver_wrapper<_any_receiver_ref>>; template using _opstate_t = _opstate>; @@ -308,7 +308,8 @@ namespace experimental::execution } template - constexpr auto connect(Receiver rcvr) && -> _opstate_t + constexpr auto connect(Receiver rcvr) && // + -> _opstate_t { auto factory = [this](RcvrRef rcvr) { @@ -322,7 +323,8 @@ namespace experimental::execution template requires __std::copy_constructible<_function> - constexpr auto connect(Receiver rcvr) const & -> _opstate_t + constexpr auto connect(Receiver rcvr) const & // + -> _opstate_t { return _function(*this).connect(static_cast(rcvr)); } From b3fc3f6fc8c965598f989ecc003ea5909f2f4ada Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 18 May 2026 14:00:51 -0700 Subject: [PATCH 06/13] Tidy up __frame_allocator.hpp More documentation, a TODO, and IWYU. --- include/exec/__frame_allocator.hpp | 65 +++++++++++++++++++++++++----- include/exec/function.hpp | 2 +- test/exec/test_frame_allocator.cpp | 2 +- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/include/exec/__frame_allocator.hpp b/include/exec/__frame_allocator.hpp index d17b18074..a36a80fd5 100644 --- a/include/exec/__frame_allocator.hpp +++ b/include/exec/__frame_allocator.hpp @@ -1,4 +1,3 @@ - /* Copyright (c) 2026 Ian Petersen * Copyright (c) 2026 NVIDIA Corporation * @@ -18,9 +17,33 @@ #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 @@ -30,6 +53,10 @@ namespace experimental::execution 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> @@ -38,6 +65,10 @@ namespace experimental::execution 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 { @@ -45,6 +76,13 @@ namespace experimental::execution 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 *> @@ -55,6 +93,9 @@ namespace experimental::execution 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) {} @@ -62,34 +103,38 @@ namespace experimental::execution constexpr type(type const &) noexcept = default; template - constexpr type(type<_Uy> const &other) noexcept + /*implicit*/ constexpr type(type<_Uy> const &other) noexcept : __resource_(other.__resource_) {} - constexpr type &operator=(type &) noexcept = default; - constexpr ~type() = default; + constexpr type &operator=(type const &) noexcept = default; + constexpr pointer allocator(std::size_t __n) { return static_cast( __resource_->allocate(__n * sizeof(value_type), alignof(value_type))); } - constexpr void delegate(void *__p, std::size_t __n) noexcept + constexpr void deallocate(void *__p, std::size_t __n) noexcept { __resource_->deallocate(__p, __n * sizeof(value_type), alignof(value_type)); } + // TODO: perform uses-allocator construction in construct + private: _Delegate *__resource_; }; }; - - template - using __frame_allocator_t = - __frame_allocator>::template type; } // 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/function.hpp b/include/exec/function.hpp index a7ccfc2ff..e2cb58edf 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -251,7 +251,7 @@ namespace experimental::execution { auto &make_sender = *__std::start_lifetime_as(storage); using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); - auto alloc = __fa::__frame_allocator_t(choose_frame_allocator(get_env(rcvr))); + auto alloc = __frame_allocator_t(choose_frame_allocator(get_env(rcvr))); return _any::_any_opstate_base(__in_place_from, std::allocator_arg, alloc, diff --git a/test/exec/test_frame_allocator.cpp b/test/exec/test_frame_allocator.cpp index 93226550d..4c243a027 100644 --- a/test/exec/test_frame_allocator.cpp +++ b/test/exec/test_frame_allocator.cpp @@ -30,7 +30,7 @@ namespace { TEST_CASE("exec::__frame_allocator is constructible", "[types][frame_allocator]") { - using namespace exec::__fa; + using namespace exec; SECTION("with a memory_resource*") { From 0a64e19c34a5b7d5aa651bce32c163f59aca8983 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 18 May 2026 14:41:52 -0700 Subject: [PATCH 07/13] _Uglify __function This diff brings `function.hpp` in line with the other files that implement `exec::function`-related types by using `_Ugly` and `__ugly` identifiers. --- include/exec/function.hpp | 357 +++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 176 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index e2cb58edf..0f7b0c4c8 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -54,90 +54,90 @@ // queries to pick the frame allocator from the environment without relying on TLS. namespace experimental::execution { - namespace _func + namespace __func { using namespace STDEXEC; //! 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 = + inline constexpr auto __choose_frame_allocator = __first_callable{get_frame_allocator, get_allocator, __always{std::allocator()}}; - template - struct _receiver_wrapper : public Receiver + template + struct __receiver_wrapper : public _Receiver { - template - constexpr explicit _receiver_wrapper(Opstate *opstate) - : Receiver(opstate->rcvr_) + template + constexpr explicit __receiver_wrapper(_Opstate *__opstate) + : _Receiver(__opstate->__rcvr_) {} }; - template - requires(!__queryable_with, get_frame_allocator_t>) - struct _receiver_wrapper : public Receiver + template + requires(!__queryable_with, get_frame_allocator_t>) + struct __receiver_wrapper<_Receiver> : public _Receiver { - using _prop_t = prop>; + using __prop_t = prop>; - template - constexpr explicit _receiver_wrapper(Opstate *opstate) - : Receiver(opstate->rcvr_) - , env_(&opstate->env_) + template + constexpr explicit __receiver_wrapper(_Opstate *__opstate) + : _Receiver(__opstate->__rcvr_) + , __env_(&__opstate->__env_) {} constexpr auto get_env() const noexcept // - -> __join_env_t<_prop_t, env_of_t> + -> __join_env_t<__prop_t, env_of_t<_Receiver>> { - return __env::__join(*env_, STDEXEC::get_env(*static_cast(this))); + return __env::__join(*__env_, STDEXEC::get_env(*static_cast<_Receiver const *>(this))); } private: - _prop_t *env_; + __prop_t *__env_; }; - template - using _any_receiver_ref = ::exec::_any::_any_receiver_ref; + template + using __any_receiver_ref = ::exec::_any::_any_receiver_ref<_Sigs, _Queries>; - template - struct _memory_resource_adaptor; + template + struct __memory_resource_adaptor; - template - requires __simple_allocator - && __same_as::value_type> - struct _memory_resource_adaptor + template + requires __simple_allocator<_Adaptee> + && __same_as::value_type> + struct __memory_resource_adaptor<_Adaptee> { //! Implement memory_resource in terms of an allocator class type : public std::pmr::memory_resource { - using traits = std::allocator_traits; - static_assert(__same_as); - typename traits::allocator_type alloc_; + using __traits = std::allocator_traits<_Adaptee>; + static_assert(__same_as); + typename __traits::allocator_type __alloc_; public: - template - requires(!__same_as) - constexpr explicit type(Alloc const &alloc) noexcept - : alloc_(alloc) + template + requires(!__same_as<_Alloc, type>) + constexpr explicit type(_Alloc const &__alloc) noexcept + : __alloc_(__alloc) { - using rebound_traits = std::allocator_traits::template rebind_traits; - static_assert(__same_as); + using __rebound_traits = std::allocator_traits<_Alloc>::template rebind_traits; + static_assert(__same_as<__traits, __rebound_traits>); } constexpr void *do_allocate(std::size_t __bytes, std::size_t __align) override { - return traits::allocate(alloc_, __bytes); + return __traits::allocate(__alloc_, __bytes); } constexpr void do_deallocate(void *__p, std::size_t __bytes, std::size_t __align) override { - traits::deallocate(alloc_, new (__p) std::byte[__bytes], __bytes); + __traits::deallocate(__alloc_, new (__p) std::byte[__bytes], __bytes); } constexpr bool do_is_equal(std::pmr::memory_resource const &__other) const noexcept override { - if (auto *ptr = dynamic_cast(&__other)) + if (auto *__ptr = dynamic_cast(&__other)) { - return alloc_ == ptr->alloc_; + return __alloc_ == __ptr->__alloc_; } return false; @@ -145,53 +145,55 @@ namespace experimental::execution }; }; - template - requires __simple_allocator - struct _memory_resource_adaptor + template + requires __simple_allocator<_Adaptee> + struct __memory_resource_adaptor<_Adaptee> //! for an arbitrary allocator, inherit from the adaptor that's implemented //! in terms of that allocator rebound to std::byte to minimize template //! instantiations, and to make the dynamic_cast in do_is_equal work - : _memory_resource_adaptor< - typename std::allocator_traits::template rebind_alloc> + : __memory_resource_adaptor< + typename std::allocator_traits<_Adaptee>::template rebind_alloc> {}; - template - requires __std::constructible_from, Adaptee> - struct _memory_resource_adaptor + template + requires __std::constructible_from, _Adaptee> + struct __memory_resource_adaptor<_Adaptee> { //! no need to "adapt" a memory_resource - using type = Adaptee; + using type = _Adaptee; }; - template - using _memory_resource_adaptor_t = _memory_resource_adaptor>::type; + template + using __memory_resource_adaptor_t = + __memory_resource_adaptor>::type; - template - struct _opstate_base + template + struct __opstate_base { - using _receiver_t = _receiver_wrapper<_any_receiver_ref>; - using _stop_token_t = stop_token_of_t>; + using __receiver_t = __receiver_wrapper<__any_receiver_ref<_Sigs, _Queries>>; + using __stop_token_t = stop_token_of_t>; - _any::_state rcvr_; + _any::_state<_Receiver, __stop_token_t> __rcvr_; }; - template - requires(!__queryable_with>, get_frame_allocator_t>) - struct _opstate_base + template + requires( + !__queryable_with>, get_frame_allocator_t>) + struct __opstate_base<_Receiver, _Sigs, _Queries> { - using _receiver_t = _receiver_wrapper<_any_receiver_ref>; - using _prop_t = _receiver_t::_prop_t; - using _stop_token_t = stop_token_of_t>; - using _adaptee_t = decltype(choose_frame_allocator(__declval())); - - _memory_resource_adaptor_t<_adaptee_t> resource_; - _prop_t env_; - _any::_state rcvr_; - - constexpr explicit _opstate_base(Receiver &&rcvr) - : resource_(choose_frame_allocator(rcvr)) - , env_{get_frame_allocator, &resource_} - , rcvr_(static_cast(rcvr)) + 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)) {} }; @@ -200,214 +202,217 @@ namespace experimental::execution //! 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 : public _opstate_base + template + class __opstate : public __opstate_base<_Receiver, _Sigs, _Queries> { - using _base = _opstate_base; - using typename _base::_receiver_t; + using __base = __opstate_base<_Receiver, _Sigs, _Queries>; + using typename __base::__receiver_t; - _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) - : _base(static_cast(rcvr)) - , op_(factory(_receiver_t(this))) + 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 = _receiver_wrapper<_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); - using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); - auto alloc = __frame_allocator_t(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_. 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 //! 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 &&...); + _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 *)]{}; + 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); + 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...>; - if constexpr (__merror<_check_queries_t>) - return __throw_compile_time_error(_check_queries_t()); + 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