From 86a8979425a767ace21345fe63edf838eb5c381f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 16 Apr 2026 20:36:56 -0700 Subject: [PATCH 01/20] Start work on an io_sender This diff starts the work to add a type-erased sender named `io_sender`. The intent is for such a sender to represent "an async function from `Args...` to `Return`", a bit like a task coroutine, but with different trade offs. The sender itself stores a `std::tuple` and a `sender auto(Args&&...)` factory that can construct the intended erased sender from the stored arguments on demand. This representation allows us to defer allocation of the type-erased operation state until `connect` time, giving us coroutine-like behaviour but allowing us to choose the frame allocator by querying the eventual receiver's environment. The completion signatures for an `io_sender` are: - `set_value_t(R&&)` - `set_error_t(std::exception_ptr)` - `set_stopped_t()` We may be able to eliminate the error channel for `io_sender` but that direction requires more thought. This first diff proves that we can store a tuple of arguments and a factory and, at `connect` time, use those values to allocate a type-erased operation state. The test cases cover only basic cases, and all allocations happen through `::operator new`. Future changes will expand the test cases and invent a `get_frame_allocator` environment query that can be used to control frame allocations. The expectation is that we can meet Capy's performance characteristics with a slightly different API in a sender-first way. --- include/exec/io/io_sender.hpp | 301 ++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/io/test_io_sender.cpp | 54 ++++++ 3 files changed, 356 insertions(+) create mode 100644 include/exec/io/io_sender.hpp create mode 100644 test/exec/io/test_io_sender.cpp diff --git a/include/exec/io/io_sender.hpp b/include/exec/io/io_sender.hpp new file mode 100644 index 000000000..c77b3ef68 --- /dev/null +++ b/include/exec/io/io_sender.hpp @@ -0,0 +1,301 @@ +/* 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/__completion_signatures.hpp" +#include "../../stdexec/__detail/__concepts.hpp" +#include "../../stdexec/__detail/__env.hpp" +#include "../../stdexec/__detail/__receivers.hpp" +#include "../../stdexec/__detail/__sender_concepts.hpp" + +#include +#include +#include +#include +#include + +// This file defines io_sender, 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 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. +namespace experimental::execution +{ + + // TODO: think about environment forwarding + template > + struct io_sender; + + template + struct completer + { + completer() = default; + + virtual void set_value(R&& value) noexcept = 0; + + virtual void set_error(std::exception_ptr err) noexcept = 0; + + virtual void set_stopped() noexcept = 0; + + protected: + ~completer() = default; + }; + + template <> + struct completer + { + completer() = default; + + virtual void set_value() noexcept = 0; + + virtual void set_error(std::exception_ptr err) noexcept = 0; + + virtual void set_stopped() noexcept = 0; + + protected: + ~completer() = default; + }; + + template + struct io_receiver + { + using receiver_concept = STDEXEC::receiver_tag; + + void set_value(R&& value) noexcept + { + completer_->set_value(std::forward(value)); + } + + void set_error(std::exception_ptr err) noexcept + { + completer_->set_error(std::move(err)); + } + + void set_stopped() noexcept + { + completer_->set_stopped(); + } + + completer* completer_; + }; + + template <> + struct io_receiver + { + using receiver_concept = STDEXEC::receiver_tag; + + void set_value() noexcept + { + completer_->set_value(); + } + + void set_error(std::exception_ptr err) noexcept + { + completer_->set_error(std::move(err)); + } + + void set_stopped() noexcept + { + completer_->set_stopped(); + } + + completer* completer_; + }; + + template + struct io_sender_completions + { + template + static consteval STDEXEC::completion_signatures + get_completion_signatures() + { + return {}; + } + }; + + template <> + struct io_sender_completions + { + template + static consteval STDEXEC::completion_signatures + get_completion_signatures() + { + return {}; + } + }; + + struct base_operation + { + base_operation() = default; + base_operation(base_operation&&) = delete; + virtual ~base_operation() = default; + + virtual void start() & noexcept = 0; + }; + + template + struct operation_storage : completer + { + explicit operation_storage(Receiver rcvr) noexcept + : receiver_(std::move(rcvr)) + {} + + void set_value(R&& value) noexcept final + { + STDEXEC::set_value(std::move(receiver_), std::forward(value)); + } + + Receiver receiver_; + }; + + template + struct operation_storage : completer + { + explicit operation_storage(Receiver rcvr) noexcept + : receiver_(std::move(rcvr)) + {} + + void set_value() noexcept final + { + STDEXEC::set_value(std::move(receiver_)); + } + + Receiver receiver_; + }; + + template + struct operation : operation_storage + { + using operation_state_concept = STDEXEC::operation_state_tag; + + template + operation(Receiver rcvr, Factory factory) + : operation_storage{std::move(rcvr)} + , op_(factory(io_receiver(this))) + {} + + void start() & noexcept + { + op_->start(); + } + + private: + std::unique_ptr op_; + + void set_error(std::exception_ptr err) noexcept final + { + STDEXEC::set_error(std::move(this->receiver_), std::move(err)); + } + + void set_stopped() noexcept final + { + STDEXEC::set_stopped(std::move(this->receiver_)); + } + }; + + // consider: + // + // template + // struct io_sender {}; + // + // to declare no error channel + // + // we allocate in connect, which could throw, but that just means connect + // can't be noexcept; it doesn't mean we have to have an error channel after + // we successfully connect... + template + requires((std::movable || std::is_reference_v) && ...) + struct io_sender : io_sender_completions + { + using sender_concept = STDEXEC::sender_tag; + + template Factory> + requires STDEXEC::__not_decays_to // + && std::constructible_from // + && STDEXEC::__callable + && STDEXEC::sender_to, io_receiver> + explicit(sizeof...(Args) == 0) io_sender(Args&&... args, Factory&& factory) + noexcept((std::is_nothrow_constructible_v && ...)) + : args_(std::forward(args)...) + { + using sender_t = std::invoke_result_t; + + struct derived_operation : base_operation + { + explicit derived_operation(sender_t&& sndr, io_receiver rcvr) // TODO noexcept + : op_(STDEXEC::connect(std::forward(sndr), std::move(rcvr))) + {} + + ~derived_operation() override = default; + + void start() & noexcept override + { + STDEXEC::start(op_); + } + + private: + STDEXEC::connect_result_t> op_; + }; + + factory_ = [](io_receiver rcvr, Args&&... args) -> base_operation* + { + Factory factory; + // TODO: query rcvr for a frame allocator and use it + return new derived_operation(factory(std::forward(args)...), std::move(rcvr)); + }; + } + + template + auto connect(this Self&& sender, Receiver receiver) -> operation + { + return operation(std::move(receiver), + [&](io_receiver rcvr) + { + return std::apply( + [&](Args&&... args) + { + return sender.factory_(std::move(rcvr), + std::forward(args)...); + }, + std::forward(sender).args_); + }); + } + + private: + base_operation* (*factory_)(io_receiver, Args&&...); + [[no_unique_address]] + std::tuple args_; + }; + +} // namespace experimental::execution + +namespace exec = experimental::execution; diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 93a30070a..4cdbdfa25 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -61,6 +61,7 @@ set(exec_test_sources $<$>:sequence/test_merge_each_threaded.cpp> $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp + io/test_io_sender.cpp ) set_source_files_properties(test_any_sender.cpp diff --git a/test/exec/io/test_io_sender.cpp b/test/exec/io/test_io_sender.cpp new file mode 100644 index 000000000..6aadc6340 --- /dev/null +++ b/test/exec/io/test_io_sender.cpp @@ -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. + */ + +#include + +#include + +#include + +namespace ex = STDEXEC; + +namespace +{ + + TEST_CASE("exec::io_sender is constructible", "[types][io_sender]") + { + exec::io_sender voidSndr([]() noexcept { return ex::just(); }); + + exec::io_sender intSndr([]() noexcept { return ex::just(42); }); + + double d = 4.; + exec::io_sender binarySndr(5, + d, + [](int, double&) noexcept + { return ex::just(); }); + + STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); + } + + TEST_CASE("exec::io_sender is connectable", "[types][io_sender]") + { + exec::io_sender sndr([]() noexcept { return ex::just(42); }); + + auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + + REQUIRE(fortytwo == 42); + } +} // namespace From 3b5911bcd0dfca410a11fc5374a605a59cacce48 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 16:46:52 -0700 Subject: [PATCH 02/20] Rename io_sender to function This diff changes the name of `io_sender` to `function` after some discussion with other folks working on `std::execution`. `exec::function<...>` is a type-erased wrapper around an async function with the given signature (elided here as `...`). More features are coming in future diffs. --- .../exec/{io/io_sender.hpp => function.hpp} | 47 ++++++++++--------- test/exec/CMakeLists.txt | 2 +- .../test_io_sender.cpp => test_function.cpp} | 21 ++++----- 3 files changed, 35 insertions(+), 35 deletions(-) rename include/exec/{io/io_sender.hpp => function.hpp} (85%) rename test/exec/{io/test_io_sender.cpp => test_function.cpp} (60%) diff --git a/include/exec/io/io_sender.hpp b/include/exec/function.hpp similarity index 85% rename from include/exec/io/io_sender.hpp rename to include/exec/function.hpp index c77b3ef68..a75314366 100644 --- a/include/exec/io/io_sender.hpp +++ b/include/exec/function.hpp @@ -15,11 +15,11 @@ */ #pragma once -#include "../../stdexec/__detail/__completion_signatures.hpp" -#include "../../stdexec/__detail/__concepts.hpp" -#include "../../stdexec/__detail/__env.hpp" -#include "../../stdexec/__detail/__receivers.hpp" -#include "../../stdexec/__detail/__sender_concepts.hpp" +#include "../stdexec/__detail/__completion_signatures.hpp" +#include "../stdexec/__detail/__concepts.hpp" +#include "../stdexec/__detail/__env.hpp" +#include "../stdexec/__detail/__receivers.hpp" +#include "../stdexec/__detail/__sender_concepts.hpp" #include #include @@ -27,7 +27,7 @@ #include #include -// This file defines io_sender, which is a +// This file defines function, which is a // type-erased sender that can complete with // - set_value(ReturnType&&) // - set_error(std::exception_ptr) @@ -49,7 +49,7 @@ namespace experimental::execution // TODO: think about environment forwarding template > - struct io_sender; + struct function; template struct completer @@ -82,7 +82,7 @@ namespace experimental::execution }; template - struct io_receiver + struct function_receiver { using receiver_concept = STDEXEC::receiver_tag; @@ -105,7 +105,7 @@ namespace experimental::execution }; template <> - struct io_receiver + struct function_receiver { using receiver_concept = STDEXEC::receiver_tag; @@ -128,7 +128,7 @@ namespace experimental::execution }; template - struct io_sender_completions + struct function_completions { template static consteval STDEXEC::completion_signatures - struct io_sender_completions + struct function_completions { template static consteval STDEXEC::completion_signatures operation(Receiver rcvr, Factory factory) : operation_storage{std::move(rcvr)} - , op_(factory(io_receiver(this))) + , op_(factory(function_receiver(this))) {} void start() & noexcept @@ -225,7 +225,7 @@ namespace experimental::execution // consider: // // template - // struct io_sender {}; + // struct function {}; // // to declare no error channel // @@ -234,16 +234,17 @@ namespace experimental::execution // we successfully connect... template requires((std::movable || std::is_reference_v) && ...) - struct io_sender : io_sender_completions + struct function : function_completions { using sender_concept = STDEXEC::sender_tag; template Factory> - requires STDEXEC::__not_decays_to // - && std::constructible_from // + requires STDEXEC::__not_decays_to // + && std::constructible_from // && STDEXEC::__callable - && STDEXEC::sender_to, io_receiver> - explicit(sizeof...(Args) == 0) io_sender(Args&&... args, Factory&& factory) + && STDEXEC::sender_to, + function_receiver> + explicit(sizeof...(Args) == 0) function(Args&&... args, Factory&& factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) { @@ -251,7 +252,7 @@ namespace experimental::execution struct derived_operation : base_operation { - explicit derived_operation(sender_t&& sndr, io_receiver rcvr) // TODO noexcept + explicit derived_operation(sender_t&& sndr, function_receiver rcvr) // TODO noexcept : op_(STDEXEC::connect(std::forward(sndr), std::move(rcvr))) {} @@ -263,10 +264,10 @@ namespace experimental::execution } private: - STDEXEC::connect_result_t> op_; + STDEXEC::connect_result_t> op_; }; - factory_ = [](io_receiver rcvr, Args&&... args) -> base_operation* + factory_ = [](function_receiver rcvr, Args&&... args) -> base_operation* { Factory factory; // TODO: query rcvr for a frame allocator and use it @@ -278,7 +279,7 @@ namespace experimental::execution auto connect(this Self&& sender, Receiver receiver) -> operation { return operation(std::move(receiver), - [&](io_receiver rcvr) + [&](function_receiver rcvr) { return std::apply( [&](Args&&... args) @@ -291,7 +292,7 @@ namespace experimental::execution } private: - base_operation* (*factory_)(io_receiver, Args&&...); + base_operation* (*factory_)(function_receiver, Args&&...); [[no_unique_address]] std::tuple args_; }; diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 4cdbdfa25..388143fad 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -61,7 +61,7 @@ set(exec_test_sources $<$>:sequence/test_merge_each_threaded.cpp> $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp - io/test_io_sender.cpp + test_function.cpp ) set_source_files_properties(test_any_sender.cpp diff --git a/test/exec/io/test_io_sender.cpp b/test/exec/test_function.cpp similarity index 60% rename from test/exec/io/test_io_sender.cpp rename to test/exec/test_function.cpp index 6aadc6340..dff7c5d64 100644 --- a/test/exec/io/test_io_sender.cpp +++ b/test/exec/test_function.cpp @@ -15,7 +15,7 @@ * limitations under the License. */ -#include +#include #include @@ -26,26 +26,25 @@ namespace ex = STDEXEC; namespace { - TEST_CASE("exec::io_sender is constructible", "[types][io_sender]") + TEST_CASE("exec::function is constructible", "[types][function]") { - exec::io_sender voidSndr([]() noexcept { return ex::just(); }); + exec::function voidSndr([]() noexcept { return ex::just(); }); - exec::io_sender intSndr([]() noexcept { return ex::just(42); }); + exec::function intSndr([]() noexcept { return ex::just(42); }); - double d = 4.; - exec::io_sender binarySndr(5, - d, - [](int, double&) noexcept - { return ex::just(); }); + double d = 4.; + exec::function binarySndr(5, + d, + [](int, double&) noexcept { return ex::just(); }); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); } - TEST_CASE("exec::io_sender is connectable", "[types][io_sender]") + TEST_CASE("exec::function is connectable", "[types][function]") { - exec::io_sender sndr([]() noexcept { return ex::just(42); }); + exec::function sndr([]() noexcept { return ex::just(42); }); auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); From 1ea9036b759e5d716305a261f28d0ad931de8f59 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 20:06:48 -0700 Subject: [PATCH 03/20] Generalize implementation Move to an implementation that spreads `completion_signatures` throughout the internals so that we're not restricted to `R(A...)`-style constraints. The tests still only validate `R(A...)`-style constraints, with no validation of no-throw functions, or controlling the completion signature and environment; that'll come next. This implementation also relies on virtual inheritance of a pack of abstract base classes, which feels like a kludge. I should figure out how to reimplement the virtual dispatch in terms of a hand-rolled vtable. --- include/exec/function.hpp | 449 ++++++++++++++++++++++---------------- 1 file changed, 255 insertions(+), 194 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index a75314366..ffa0a6770 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -46,257 +46,318 @@ // the frame allocator from the environment without relying on TLS. namespace experimental::execution { + namespace _func + { + using namespace STDEXEC; - // TODO: think about environment forwarding - template > - struct function; + template + struct _virt_completion; - template - struct completer - { - completer() = default; + template + struct _virt_completion + { + _virt_completion() = default; - virtual void set_value(R&& value) noexcept = 0; + _virt_completion(_virt_completion&&) = delete; - virtual void set_error(std::exception_ptr err) noexcept = 0; + virtual void set_error(Error&& err) noexcept = 0; - virtual void set_stopped() noexcept = 0; + protected: + ~_virt_completion() = default; + }; - protected: - ~completer() = default; - }; + template <> + struct _virt_completion + { + _virt_completion() = default; - template <> - struct completer - { - completer() = default; + _virt_completion(_virt_completion&&) = delete; + + virtual void set_stopped() noexcept = 0; - virtual void set_value() noexcept = 0; + protected: + ~_virt_completion() = default; + }; - virtual void set_error(std::exception_ptr err) noexcept = 0; + template + struct _virt_completion + { + _virt_completion() = default; - virtual void set_stopped() noexcept = 0; + _virt_completion(_virt_completion&&) = delete; - protected: - ~completer() = default; - }; + virtual void set_value(Values&&... values) noexcept = 0; - template - struct function_receiver - { - using receiver_concept = STDEXEC::receiver_tag; + protected: + ~_virt_completion() = default; + }; - void set_value(R&& value) noexcept - { - completer_->set_value(std::forward(value)); - } + template + struct _virt_completions; - void set_error(std::exception_ptr err) noexcept + template + struct _virt_completions> : virtual _virt_completion... { - completer_->set_error(std::move(err)); - } + _virt_completions() = default; - void set_stopped() noexcept - { - completer_->set_stopped(); - } + _virt_completions(_virt_completions&&) = delete; - completer* completer_; - }; + protected: + ~_virt_completions() = default; + }; - template <> - struct function_receiver - { - using receiver_concept = STDEXEC::receiver_tag; + template + struct _func_rcvr_base; - void set_value() noexcept + template + struct _func_rcvr_base { - completer_->set_value(); - } + void set_error(Error&& err) && noexcept + { + static_cast(this)->completer_->set_error(std::forward(err)); + } + }; - void set_error(std::exception_ptr err) noexcept + template + struct _func_rcvr_base { - completer_->set_error(std::move(err)); - } + void set_stopped() && noexcept + { + static_cast(this)->completer_->set_stopped(); + } + }; - void set_stopped() noexcept + template + struct _func_rcvr_base { - completer_->set_stopped(); - } + void set_value(Value&&... value) && noexcept + { + static_cast(this)->completer_->set_value(std::forward(value)...); + } + }; - completer* completer_; - }; + template + class _func_rcvr; - template - struct function_completions - { - template - static consteval STDEXEC::completion_signatures - get_completion_signatures() + template + class _func_rcvr> + : public _func_rcvr_base>>... { - return {}; - } - }; + friend _func_rcvr_base...; - template <> - struct function_completions - { - template - static consteval STDEXEC::completion_signatures - get_completion_signatures() - { - return {}; - } - }; + using completer_t = _virt_completions>; - struct base_operation - { - base_operation() = default; - base_operation(base_operation&&) = delete; - virtual ~base_operation() = default; + completer_t* completer_; - virtual void start() & noexcept = 0; - }; + public: + using receiver_concept = receiver_tag; - template - struct operation_storage : completer - { - explicit operation_storage(Receiver rcvr) noexcept - : receiver_(std::move(rcvr)) - {} + explicit _func_rcvr(completer_t& completer) noexcept + : completer_(std::addressof(completer)) + {} + + // TODO: get_env + }; - void set_value(R&& value) noexcept final + struct _base_op { - STDEXEC::set_value(std::move(receiver_), std::forward(value)); - } + _base_op() = default; - Receiver receiver_; - }; + _base_op(_base_op&&) = delete; - template - struct operation_storage : completer - { - explicit operation_storage(Receiver rcvr) noexcept - : receiver_(std::move(rcvr)) - {} + virtual ~_base_op() = default; - void set_value() noexcept final + virtual void start() & noexcept = 0; + }; + + template + struct _derived_op : _base_op { - STDEXEC::set_value(std::move(receiver_)); - } + explicit _derived_op(Sender&& sndr, Receiver rcvr) + noexcept(std::is_nothrow_invocable_v) + : op_(connect(std::forward(sndr), std::move(rcvr))) + {} - Receiver receiver_; - }; + _derived_op(_derived_op&&) = delete; - template - struct operation : operation_storage - { - using operation_state_concept = STDEXEC::operation_state_tag; + ~_derived_op() override = default; - template - operation(Receiver rcvr, Factory factory) - : operation_storage{std::move(rcvr)} - , op_(factory(function_receiver(this))) - {} + void start() & noexcept override + { + ::STDEXEC::start(op_); + } - void start() & noexcept - { - op_->start(); - } + private: + connect_result_t op_; + }; - private: - std::unique_ptr op_; + template + struct _func_op_completion; - void set_error(std::exception_ptr err) noexcept final + template + struct _func_op_completion + : virtual _virt_completion { - STDEXEC::set_error(std::move(this->receiver_), std::move(err)); - } + void set_error(Error&& err) noexcept final + { + static_cast(this)->complete(set_error_t{}, std::forward(err)); + } + }; - void set_stopped() noexcept final + template + struct _func_op_completion : virtual _virt_completion { - STDEXEC::set_stopped(std::move(this->receiver_)); - } - }; + void set_stopped() noexcept final + { + static_cast(this)->complete(set_stopped_t{}); + } + }; - // consider: - // - // template - // struct function {}; - // - // to declare no error channel - // - // we allocate in connect, which could throw, but that just means connect - // can't be noexcept; it doesn't mean we have to have an error channel after - // we successfully connect... - template - requires((std::movable || std::is_reference_v) && ...) - struct function : function_completions - { - using sender_concept = STDEXEC::sender_tag; - - template Factory> - requires STDEXEC::__not_decays_to // - && std::constructible_from // - && STDEXEC::__callable - && STDEXEC::sender_to, - function_receiver> - explicit(sizeof...(Args) == 0) function(Args&&... args, Factory&& factory) - noexcept((std::is_nothrow_constructible_v && ...)) - : args_(std::forward(args)...) + template + struct _func_op_completion + : virtual _virt_completion + { + void set_value(Value&&... value) noexcept final + { + static_cast(this)->complete(set_value_t{}, std::forward(value)...); + } + }; + + template + class _func_op; + + template + class _func_op> + : private _virt_completions> + , private _func_op_completion>>... { - using sender_t = std::invoke_result_t; + // TODO: use get_frame_allocator(get_env(rcvr_)) to allocate and destroy this + std::unique_ptr<_base_op> op_; + [[no_unique_address]] + Receiver rcvr_; + + friend _func_op_completion...; - struct derived_operation : base_operation + template + void complete(CPO cpo, Arg&&... arg) noexcept { - explicit derived_operation(sender_t&& sndr, function_receiver rcvr) // TODO noexcept - : op_(STDEXEC::connect(std::forward(sndr), std::move(rcvr))) - {} + std::move(cpo)(std::move(rcvr_), std::forward(arg)...); + } - ~derived_operation() override = default; + public: + using operation_state_concept = operation_state_tag; - void start() & noexcept override - { - STDEXEC::start(op_); - } + template + _func_op(Receiver rcvr, Factory factory) + : rcvr_(std::move(rcvr)) + , op_(factory(_func_rcvr>(*this))){}; + + _func_op(_func_op&&) = delete; + + ~_func_op() = default; + + void start() & noexcept + { + op_->start(); + } + }; + + template + class _func_impl; + + template + class _func_impl, Env> + { + _base_op* (*factory_)(_func_rcvr>, Args&&...); + [[no_unique_address]] + std::tuple args_; + + public: + using sender_concept = SndrCncpt; + + template Factory> + requires STDEXEC::__not_decays_to // + && std::constructible_from // + && STDEXEC::__callable + //&& STDEXEC::sender_to, + //_func_rcvr>> + explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) + noexcept((std::is_nothrow_constructible_v && ...)) + : args_(std::forward(args)...) + { + using sender_t = std::invoke_result_t; + using receiver_t = _func_rcvr>; + + using op_t = _derived_op; - private: - STDEXEC::connect_result_t> op_; - }; + factory_ = [](receiver_t rcvr, Args&&... args) -> _base_op* + { + Factory factory; + // TODO: query rcvr for a frame allocator and use it + return new op_t(factory(std::forward(args)...), std::move(rcvr)); + }; + } + + template + static consteval completion_signatures get_completion_signatures() noexcept + { + // TODO: validate that the Env passed here is compatible with the class-level Env + return {}; + } - factory_ = [](function_receiver rcvr, Args&&... args) -> base_operation* + template + constexpr _func_op> connect(Receiver rcvr) { - Factory factory; - // TODO: query rcvr for a frame allocator and use it - return new derived_operation(factory(std::forward(args)...), std::move(rcvr)); - }; - } - - template - auto connect(this Self&& sender, Receiver receiver) -> operation + return {std::move(rcvr), + [&, this](auto rcvr) + { + return std::apply( + [&](Args&&... args) + { return factory_(std::move(rcvr), std::forward(args)...); }, + std::move(args_)); + }}; + } + }; + + template + struct _sigs_from; + + template + struct _sigs_from { - return operation(std::move(receiver), - [&](function_receiver rcvr) - { - return std::apply( - [&](Args&&... args) - { - return sender.factory_(std::move(rcvr), - std::forward(args)...); - }, - std::forward(sender).args_); - }); - } - - private: - base_operation* (*factory_)(function_receiver, Args&&...); - [[no_unique_address]] - std::tuple args_; - }; + using type = STDEXEC::completion_signatures; + }; + + template + struct _sigs_from + { + using type = STDEXEC::completion_signatures; + }; + + template + using _sigs_from_t = _sigs_from::type; + } // namespace _func + // TODO: think about environment forwarding + template + class function; + + template + class function + : public _func::_func_impl, + STDEXEC::env<>> + { + using base = _func::_func_impl, + STDEXEC::env<>>; + + using base::base; + }; } // namespace experimental::execution namespace exec = experimental::execution; From fd531388c26767b21b9ff6363fbde4a643c88f1b Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 09:31:23 -0700 Subject: [PATCH 04/20] Support no-throw functions --- include/exec/function.hpp | 21 +++++++++++++++++---- test/exec/test_function.cpp | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index ffa0a6770..4c2a682e0 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -338,6 +338,19 @@ namespace experimental::execution STDEXEC::set_value_t()>; }; + template + struct _sigs_from + { + using type = + STDEXEC::completion_signatures; + }; + + template + struct _sigs_from + { + using type = STDEXEC::completion_signatures; + }; + template using _sigs_from_t = _sigs_from::type; } // namespace _func @@ -346,14 +359,14 @@ namespace experimental::execution template class function; - template - class function + template + class function : public _func::_func_impl, + _func::_sigs_from_t, STDEXEC::env<>> { using base = _func::_func_impl, + _func::_sigs_from_t, STDEXEC::env<>>; using base::base; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index dff7c5d64..b855b38c0 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -37,9 +37,14 @@ namespace d, [](int, double&) noexcept { return ex::just(); }); + exec::function nothrowSndr([]() noexcept { return ex::just(); }); + exec::function nothrowIntSndr([]() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); } TEST_CASE("exec::function is connectable", "[types][function]") From 665566a853a3a4e8efd122413b5aefc06c608e88 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 20:44:18 -0700 Subject: [PATCH 05/20] Get rid of virtual inheritance Thanks to a suggestion from @RobertLeahy, I've been able to rework the virtual function inheritance to not need virtual inheritance. --- include/exec/function.hpp | 140 ++++++++++++------------------------ test/exec/test_function.cpp | 17 ++++- 2 files changed, 60 insertions(+), 97 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 4c2a682e0..9a62f4dc2 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -53,40 +53,14 @@ namespace experimental::execution template struct _virt_completion; - template - struct _virt_completion + template + struct _virt_completion { _virt_completion() = default; _virt_completion(_virt_completion&&) = delete; - virtual void set_error(Error&& err) noexcept = 0; - - protected: - ~_virt_completion() = default; - }; - - template <> - struct _virt_completion - { - _virt_completion() = default; - - _virt_completion(_virt_completion&&) = delete; - - virtual void set_stopped() noexcept = 0; - - protected: - ~_virt_completion() = default; - }; - - template - struct _virt_completion - { - _virt_completion() = default; - - _virt_completion(_virt_completion&&) = delete; - - virtual void set_value(Values&&... values) noexcept = 0; + virtual void complete(CPO, Args&&...) noexcept = 0; protected: ~_virt_completion() = default; @@ -96,55 +70,24 @@ namespace experimental::execution struct _virt_completions; template - struct _virt_completions> : virtual _virt_completion... + struct _virt_completions> : _virt_completion... { _virt_completions() = default; _virt_completions(_virt_completions&&) = delete; + using _virt_completion::complete...; + protected: ~_virt_completions() = default; }; - template - struct _func_rcvr_base; - - template - struct _func_rcvr_base - { - void set_error(Error&& err) && noexcept - { - static_cast(this)->completer_->set_error(std::forward(err)); - } - }; - - template - struct _func_rcvr_base - { - void set_stopped() && noexcept - { - static_cast(this)->completer_->set_stopped(); - } - }; - - template - struct _func_rcvr_base - { - void set_value(Value&&... value) && noexcept - { - static_cast(this)->completer_->set_value(std::forward(value)...); - } - }; - template class _func_rcvr; template class _func_rcvr> - : public _func_rcvr_base>>... { - friend _func_rcvr_base...; - using completer_t = _virt_completions>; completer_t* completer_; @@ -156,6 +99,28 @@ namespace experimental::execution : completer_(std::addressof(completer)) {} + template + void set_error(Error&& err) && noexcept + requires requires { this->completer_->complete(set_error_t{}, std::forward(err)); } + { + this->completer_->complete(set_error_t{}, std::forward(err)); + } + + void set_stopped() && noexcept + requires requires { this->completer_->complete(set_stopped_t{}); } + { + this->completer_->complete(set_stopped_t{}); + } + + template + void set_value(Values&&... values) && noexcept + requires requires { + this->completer_->complete(set_value_t{}, std::forward(values)...); + } + { + this->completer_->complete(set_value_t{}, std::forward(values)...); + } + // TODO: get_env }; @@ -180,9 +145,9 @@ namespace experimental::execution _derived_op(_derived_op&&) = delete; - ~_derived_op() override = default; + ~_derived_op() final = default; - void start() & noexcept override + void start() & noexcept final { ::STDEXEC::start(op_); } @@ -191,35 +156,20 @@ namespace experimental::execution connect_result_t op_; }; - template + template struct _func_op_completion; - template - struct _func_op_completion - : virtual _virt_completion - { - void set_error(Error&& err) noexcept final - { - static_cast(this)->complete(set_error_t{}, std::forward(err)); - } - }; - - template - struct _func_op_completion : virtual _virt_completion - { - void set_stopped() noexcept final - { - static_cast(this)->complete(set_stopped_t{}); - } - }; + template + struct _func_op_completion : Base + {}; - template - struct _func_op_completion - : virtual _virt_completion + template + struct _func_op_completion + : _func_op_completion { - void set_value(Value&&... value) noexcept final + void complete(CPO, Args&&... args) noexcept final { - static_cast(this)->complete(set_value_t{}, std::forward(value)...); + static_cast(this)->complete(CPO{}, std::forward(args)...); } }; @@ -228,15 +178,17 @@ namespace experimental::execution template class _func_op> - : private _virt_completions> - , private _func_op_completion>>... + : private _func_op_completion<_virt_completions>, + _func_op>, + Sigs...> { // TODO: use get_frame_allocator(get_env(rcvr_)) to allocate and destroy this std::unique_ptr<_base_op> op_; [[no_unique_address]] Receiver rcvr_; - friend _func_op_completion...; + template + friend struct _func_op_completion; template void complete(CPO cpo, Arg&&... arg) noexcept @@ -279,8 +231,8 @@ namespace experimental::execution requires STDEXEC::__not_decays_to // && std::constructible_from // && STDEXEC::__callable - //&& STDEXEC::sender_to, - //_func_rcvr>> + && STDEXEC::sender_to, + _func_rcvr>> explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index b855b38c0..3051ca452 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -25,7 +25,6 @@ namespace ex = STDEXEC; namespace { - TEST_CASE("exec::function is constructible", "[types][function]") { exec::function voidSndr([]() noexcept { return ex::just(); }); @@ -49,10 +48,22 @@ namespace TEST_CASE("exec::function is connectable", "[types][function]") { - exec::function sndr([]() noexcept { return ex::just(42); }); + exec::function sndr([]() noexcept { return ex::just(42); }); - auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + struct rcvr + { + using receiver_concept = ex::receiver_tag; + + void set_value(int) && noexcept {} + void set_stopped() && noexcept {} + }; + STATIC_REQUIRE(ex::receiver); + + auto op = ex::connect(std::move(sndr), rcvr{}); + + auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + REQUIRE(fortytwo == 42); } } // namespace From de5ccbb456e1d03a77b7043ee4110d2d91b108f7 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:03:14 -0700 Subject: [PATCH 06/20] Support arbitrary completion signatures `function>` now declares an async function mapping `Args...` to the explicitly specified completion signatures. --- include/exec/function.hpp | 19 ++++++++++++++----- test/exec/test_function.cpp | 9 ++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 9a62f4dc2..901e589dc 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -309,13 +309,13 @@ namespace experimental::execution // TODO: think about environment forwarding template - class function; + struct function; template - class function - : public _func::_func_impl, - STDEXEC::env<>> + struct function + : _func::_func_impl, + STDEXEC::env<>> { using base = _func::_func_impl, @@ -323,6 +323,15 @@ namespace experimental::execution using base::base; }; + + template Sigs> + struct function + : _func::_func_impl> + { + using base = _func::_func_impl>; + + using base::base; + }; } // namespace experimental::execution namespace exec = experimental::execution; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 3051ca452..146705588 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -39,11 +39,18 @@ namespace exec::function nothrowSndr([]() noexcept { return ex::just(); }); exec::function nothrowIntSndr([]() noexcept { return ex::just(42); }); + exec::function> unstoppable( + []() noexcept { return ex::just(42); }); + exec::function> onlystopped( + []() noexcept { return ex::just_stopped(); }); + STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); } TEST_CASE("exec::function is connectable", "[types][function]") @@ -63,7 +70,7 @@ namespace auto op = ex::connect(std::move(sndr), rcvr{}); auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); - + REQUIRE(fortytwo == 42); } } // namespace From 18d10f10d93f98c1af8e8c9af2d3a5036c110a91 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:19:20 -0700 Subject: [PATCH 07/20] Round out the partial specializations of exec::function Support for explicit completion signatures, environment, or both in the declaration of an `exec:function`. --- include/exec/function.hpp | 35 ++++++++++++++++++++++++++++++++++- test/exec/test_function.cpp | 7 +++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 901e589dc..21e58ba02 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -312,6 +312,13 @@ namespace experimental::execution struct function; template + // should this require STDEXEC::__not_same_as? + // + // you *could* write STDEXEC::just(STDEXEC::sender_tag{}), but it seems more likely + // that invokign this specialization with Return set to sender_tag is a bug... + // + // the same question applies to all the specializations below that take explicit + // completion signatures struct function : _func::_func_impl, @@ -324,7 +331,8 @@ namespace experimental::execution using base::base; }; - template Sigs> + template + requires STDEXEC::__is_instance_of struct function : _func::_func_impl> { @@ -332,6 +340,31 @@ namespace experimental::execution using base::base; }; + + template + requires STDEXEC::__is_not_instance_of + struct function + : _func::_func_impl, + Env> + { + using base = _func::_func_impl, + Env>; + + using base::base; + }; + + template + requires STDEXEC::__is_not_instance_of + struct function, Env> + : _func::_func_impl, Env> + { + using base = + _func::_func_impl, Env>; + + using base::base; + }; } // namespace experimental::execution namespace exec = experimental::execution; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 146705588..bf3f5dc0a 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -44,6 +44,11 @@ namespace exec::function> onlystopped( []() noexcept { return ex::just_stopped(); }); + exec::function> trivialCustomEnv([]() noexcept { return ex::just(); }); + + exec::function, ex::env<>> + totalControl(5, [](int) noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); @@ -51,6 +56,8 @@ namespace STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); + STATIC_REQUIRE(STDEXEC::sender); } TEST_CASE("exec::function is connectable", "[types][function]") From 846b53e69ec973e3a53fa62389bd3691a0093511 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:23:03 -0700 Subject: [PATCH 08/20] Delete a layer of forwarding --- include/exec/function.hpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 21e58ba02..d23308baa 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -169,7 +169,8 @@ namespace experimental::execution { void complete(CPO, Args&&... args) noexcept final { - static_cast(this)->complete(CPO{}, std::forward(args)...); + auto& rcvr = static_cast(this)->rcvr_; + CPO{}(std::move(rcvr), std::forward(args)...); } }; @@ -190,12 +191,6 @@ namespace experimental::execution template friend struct _func_op_completion; - template - void complete(CPO cpo, Arg&&... arg) noexcept - { - std::move(cpo)(std::move(rcvr_), std::forward(arg)...); - } - public: using operation_state_concept = operation_state_tag; From 320a92369fce3320a6e773cf2fb16934476e6c69 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 12:50:23 -0700 Subject: [PATCH 09/20] Inch towards allocator support Rework the dynamically allocated operation state type to support allocators, but always use `std::allocator` for now. --- include/exec/function.hpp | 43 +++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d23308baa..f7c16194e 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -19,10 +19,12 @@ #include "../stdexec/__detail/__concepts.hpp" #include "../stdexec/__detail/__env.hpp" #include "../stdexec/__detail/__receivers.hpp" +#include "../stdexec/__detail/__scope.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" #include #include +#include #include #include #include @@ -135,12 +137,13 @@ namespace experimental::execution virtual void start() & noexcept = 0; }; - template + template struct _derived_op : _base_op { - explicit _derived_op(Sender&& sndr, Receiver rcvr) + explicit _derived_op(Sender&& sndr, Receiver rcvr, Allocator const & alloc) noexcept(std::is_nothrow_invocable_v) : op_(connect(std::forward(sndr), std::move(rcvr))) + , alloc_(alloc) {} _derived_op(_derived_op&&) = delete; @@ -152,8 +155,19 @@ namespace experimental::execution ::STDEXEC::start(op_); } + static constexpr void operator delete(_derived_op* p, std::destroying_delete_t) + { + using traits = std::allocator_traits::template rebind_traits<_derived_op>; + + typename traits::allocator_type alloc = std::move(p->alloc_); + traits::destroy(alloc, p); + traits::deallocate(alloc, p, 1); + } + private: connect_result_t op_; + [[no_unique_address]] + Allocator alloc_; }; template @@ -215,7 +229,7 @@ namespace experimental::execution template class _func_impl, Env> { - _base_op* (*factory_)(_func_rcvr>, Args&&...); + std::unique_ptr<_base_op> (*factory_)(_func_rcvr>, Args&&...); [[no_unique_address]] std::tuple args_; @@ -235,13 +249,30 @@ namespace experimental::execution using sender_t = std::invoke_result_t; using receiver_t = _func_rcvr>; - using op_t = _derived_op; + using op_t = _derived_op>; - factory_ = [](receiver_t rcvr, Args&&... args) -> _base_op* + factory_ = [](receiver_t rcvr, Args&&... args) -> std::unique_ptr<_base_op> { + using traits = std::allocator_traits>; + Factory factory; + // TODO: query rcvr for a frame allocator and use it - return new op_t(factory(std::forward(args)...), std::move(rcvr)); + typename traits::allocator_type alloc; + + auto* op = traits::allocate(alloc, 1); + + __scope_guard guard{[&]() noexcept { traits::deallocate(alloc, op, 1); }}; + + traits::construct(alloc, + op, + factory(std::forward(args)...), + std::move(rcvr), + alloc); + + guard.__dismiss(); + + return std::unique_ptr<_base_op>(op); }; } From 8b72f780e2ff88595bebc167cc736b5794c12e6c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 14:02:31 -0700 Subject: [PATCH 10/20] Add frame allocator support This diff needs tests, but the existing tests build and pass, which seems like a good signal. I've added a `get_frame_allocator` query, and a defaulting cascade from `get_frame_allocator` -> `get_allocator` -> `std::allocator` to the allocation of `_derived_op`. --- include/exec/function.hpp | 50 +++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index f7c16194e..16d078dfb 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -18,6 +18,7 @@ #include "../stdexec/__detail/__completion_signatures.hpp" #include "../stdexec/__detail/__concepts.hpp" #include "../stdexec/__detail/__env.hpp" +#include "../stdexec/__detail/__read_env.hpp" #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__scope.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" @@ -48,6 +49,31 @@ // the frame allocator from the environment without relying on TLS. namespace experimental::execution { + 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; @@ -197,7 +223,6 @@ namespace experimental::execution _func_op>, Sigs...> { - // TODO: use get_frame_allocator(get_env(rcvr_)) to allocate and destroy this std::unique_ptr<_base_op> op_; [[no_unique_address]] Receiver rcvr_; @@ -223,6 +248,23 @@ namespace experimental::execution } }; + template + constexpr auto choose_frame_allocator(Env const & env) noexcept + { + if constexpr (requires { get_frame_allocator(env); }) + { + return get_frame_allocator(env); + } + else if constexpr (requires { get_allocator(env); }) + { + return get_allocator(env); + } + else + { + return std::allocator(); + } + } + template class _func_impl; @@ -253,12 +295,12 @@ namespace experimental::execution factory_ = [](receiver_t rcvr, Args&&... args) -> std::unique_ptr<_base_op> { - using traits = std::allocator_traits>; + using traits = std::allocator_traits::template rebind_traits; Factory factory; - // TODO: query rcvr for a frame allocator and use it - typename traits::allocator_type alloc; + typename traits::allocator_type alloc(choose_frame_allocator(get_env(rcvr))); auto* op = traits::allocate(alloc, 1); From 93b5c5ec530a9e1eb6681706be8c9a707edaacd8 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 17:26:17 -0700 Subject: [PATCH 11/20] constexpr (almost) all the things This diff marks almost every function `constexpr`. It doesn't mark the imlementation of `complete` in the CRTP `_func_op_completion` class template because Clang rejects the down-cast to `Derived` as not a core constant expression; apparently, `Derived` is incomplete when it's being evaluated as a side effect of constraint satisfaction testing. This `constexpr` "hole" means `exec::function` can't be used at compile time, but maybe it can be worked around later. --- include/exec/function.hpp | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 16d078dfb..0a3503731 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -22,6 +22,7 @@ #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__scope.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" +#include "../stdexec/execution.hpp" #include #include @@ -84,14 +85,14 @@ namespace experimental::execution template struct _virt_completion { - _virt_completion() = default; + constexpr _virt_completion() = default; _virt_completion(_virt_completion&&) = delete; - virtual void complete(CPO, Args&&...) noexcept = 0; + constexpr virtual void complete(CPO, Args&&...) noexcept = 0; protected: - ~_virt_completion() = default; + constexpr ~_virt_completion() = default; }; template @@ -100,14 +101,14 @@ namespace experimental::execution template struct _virt_completions> : _virt_completion... { - _virt_completions() = default; + constexpr _virt_completions() = default; _virt_completions(_virt_completions&&) = delete; using _virt_completion::complete...; protected: - ~_virt_completions() = default; + constexpr ~_virt_completions() = default; }; template @@ -123,25 +124,25 @@ namespace experimental::execution public: using receiver_concept = receiver_tag; - explicit _func_rcvr(completer_t& completer) noexcept + constexpr explicit _func_rcvr(completer_t& completer) noexcept : completer_(std::addressof(completer)) {} template - void set_error(Error&& err) && noexcept + constexpr void set_error(Error&& err) && noexcept requires requires { this->completer_->complete(set_error_t{}, std::forward(err)); } { this->completer_->complete(set_error_t{}, std::forward(err)); } - void set_stopped() && noexcept + constexpr void set_stopped() && noexcept requires requires { this->completer_->complete(set_stopped_t{}); } { this->completer_->complete(set_stopped_t{}); } template - void set_value(Values&&... values) && noexcept + constexpr void set_value(Values&&... values) && noexcept requires requires { this->completer_->complete(set_value_t{}, std::forward(values)...); } @@ -154,19 +155,19 @@ namespace experimental::execution struct _base_op { - _base_op() = default; + constexpr _base_op() = default; _base_op(_base_op&&) = delete; - virtual ~_base_op() = default; + constexpr virtual ~_base_op() = default; - virtual void start() & noexcept = 0; + constexpr virtual void start() & noexcept = 0; }; template struct _derived_op : _base_op { - explicit _derived_op(Sender&& sndr, Receiver rcvr, Allocator const & alloc) + constexpr explicit _derived_op(Sender&& sndr, Receiver rcvr, Allocator const & alloc) noexcept(std::is_nothrow_invocable_v) : op_(connect(std::forward(sndr), std::move(rcvr))) , alloc_(alloc) @@ -174,9 +175,9 @@ namespace experimental::execution _derived_op(_derived_op&&) = delete; - ~_derived_op() final = default; + constexpr ~_derived_op() final = default; - void start() & noexcept final + constexpr void start() & noexcept final { ::STDEXEC::start(op_); } @@ -209,6 +210,12 @@ namespace experimental::execution { void complete(CPO, Args&&... args) noexcept final { + // This seems like it ought to be true, but it fails... + // + // Some testing shows it's being evaluated when Derive is incomplete + // during constraint satisfaction testing. + // + // static_assert(std::derived_from<_func_op_completion, Derived>); auto& rcvr = static_cast(this)->rcvr_; CPO{}(std::move(rcvr), std::forward(args)...); } @@ -234,15 +241,15 @@ namespace experimental::execution using operation_state_concept = operation_state_tag; template - _func_op(Receiver rcvr, Factory factory) + constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) , op_(factory(_func_rcvr>(*this))){}; _func_op(_func_op&&) = delete; - ~_func_op() = default; + constexpr ~_func_op() = default; - void start() & noexcept + constexpr void start() & noexcept { op_->start(); } @@ -284,7 +291,7 @@ namespace experimental::execution && STDEXEC::__callable && STDEXEC::sender_to, _func_rcvr>> - explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) + constexpr explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) { From 83e7ed785934786148156931c3495a6a9441a42b Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 18:35:26 -0700 Subject: [PATCH 12/20] More tests Validate that more kinds of senders can be erased and then connected and started. Also clean up the captures in some lambdas in `connect` and `clang-format`. --- include/exec/function.hpp | 6 +++--- test/exec/test_function.cpp | 39 +++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 0a3503731..531bc0960 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -213,7 +213,7 @@ namespace experimental::execution // This seems like it ought to be true, but it fails... // // Some testing shows it's being evaluated when Derive is incomplete - // during constraint satisfaction testing. + // during constraint satisfaction testing. // // static_assert(std::derived_from<_func_op_completion, Derived>); auto& rcvr = static_cast(this)->rcvr_; @@ -336,10 +336,10 @@ namespace experimental::execution constexpr _func_op> connect(Receiver rcvr) { return {std::move(rcvr), - [&, this](auto rcvr) + [this](auto rcvr) { return std::apply( - [&](Args&&... args) + [&rcvr, this](Args&&... args) { return factory_(std::move(rcvr), std::forward(args)...); }, std::move(args_)); }}; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index bf3f5dc0a..1a2d5aefe 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -62,22 +62,41 @@ namespace TEST_CASE("exec::function is connectable", "[types][function]") { - exec::function sndr([]() noexcept { return ex::just(42); }); + { + exec::function sndr([]() noexcept { return ex::just(42); }); + + auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + + REQUIRE(fortytwo == 42); + } - struct rcvr { - using receiver_concept = ex::receiver_tag; + exec::function sndr([]() -> decltype(ex::just()) { throw "oops"; }); - void set_value(int) && noexcept {} - void set_stopped() && noexcept {} - }; + REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); + } - STATIC_REQUIRE(ex::receiver); + { + exec::function sndr([]() noexcept + { return ex::just() | ex::then([] { throw "oops"; }); }); - auto op = ex::connect(std::move(sndr), rcvr{}); + REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); + } - auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + { + exec::function sndr([]() noexcept { return ex::just_stopped(); }); + + auto ret = ex::sync_wait(std::move(sndr)); + + REQUIRE_FALSE(ret.has_value()); + } + + { + exec::function> + sndr([]() noexcept { return ex::just_error(42); }); - REQUIRE(fortytwo == 42); + REQUIRE_THROWS_AS(ex::sync_wait(std::move(sndr)), int); + } } } // namespace From 65acee431f21ce381c23090f6cbcfad542fe6edd Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 20 Apr 2026 07:40:51 -0700 Subject: [PATCH 13/20] Get allocator selection working Still TODO is that the `get_frame_allocator` query shouldn't have to be specified in the `function`'s custom environment (and, come to think of it, neither should `get_allocator`), but, when specified, it works. --- include/exec/function.hpp | 75 +++++++++++++++++++++++++------------ test/exec/test_function.cpp | 28 ++++++++++++++ 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 531bc0960..c965423d1 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -95,11 +95,11 @@ namespace experimental::execution constexpr ~_virt_completion() = default; }; - template + template struct _virt_completions; - template - struct _virt_completions> : _virt_completion... + template + struct _virt_completions, Env> : _virt_completion... { constexpr _virt_completions() = default; @@ -107,17 +107,19 @@ namespace experimental::execution using _virt_completion::complete...; + virtual Env get_env() const noexcept = 0; + protected: constexpr ~_virt_completions() = default; }; - template + template class _func_rcvr; - template - class _func_rcvr> + template + class _func_rcvr, Env> { - using completer_t = _virt_completions>; + using completer_t = _virt_completions, Env>; completer_t* completer_; @@ -150,7 +152,10 @@ namespace experimental::execution this->completer_->complete(set_value_t{}, std::forward(values)...); } - // TODO: get_env + constexpr auto get_env() const noexcept -> env_of_t + { + return STDEXEC::get_env(*completer_); + } }; struct _base_op @@ -221,13 +226,13 @@ namespace experimental::execution } }; - template + template class _func_op; - template - class _func_op> - : private _func_op_completion<_virt_completions>, - _func_op>, + template + class _func_op, Env> + : private _func_op_completion<_virt_completions, Env>, + _func_op, Env>, Sigs...> { std::unique_ptr<_base_op> op_; @@ -237,13 +242,27 @@ namespace experimental::execution template friend struct _func_op_completion; + constexpr Env get_env() const noexcept final + { + using RcvrEnv = env_of_t; + + if constexpr (std::constructible_from) + { + return Env(::STDEXEC::get_env(rcvr_)); + } + else + { + return {}; + } + } + public: using operation_state_concept = operation_state_tag; template constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) - , op_(factory(_func_rcvr>(*this))){}; + , op_(factory(_func_rcvr, Env>(*this))){}; _func_op(_func_op&&) = delete; @@ -278,7 +297,8 @@ namespace experimental::execution template class _func_impl, Env> { - std::unique_ptr<_base_op> (*factory_)(_func_rcvr>, Args&&...); + std::unique_ptr<_base_op> (*factory_)(_func_rcvr, Env>, + Args&&...); [[no_unique_address]] std::tuple args_; @@ -290,29 +310,38 @@ namespace experimental::execution && std::constructible_from // && STDEXEC::__callable && STDEXEC::sender_to, - _func_rcvr>> + _func_rcvr, Env>> constexpr explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) { using sender_t = std::invoke_result_t; - using receiver_t = _func_rcvr>; - - using op_t = _derived_op>; + using receiver_t = _func_rcvr, Env>; factory_ = [](receiver_t rcvr, Args&&... args) -> std::unique_ptr<_base_op> { - using traits = std::allocator_traits::template rebind_traits; + // the type of the allocator provided by the receiver's environment + using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); + // the traits for that allocator, but normalized to std::byte to minimize + // template instantiations + using traits_t = std::allocator_traits::template rebind_traits; - Factory factory; + // the type of operation we'll ultimately allocate, which depends on the type of + // the allocator we're using + using op_t = _derived_op; + + // finally, the allocator traits for an allocator that can allocate an op_t + using traits = traits_t::template rebind_traits; + // ...and the allocator itself typename traits::allocator_type alloc(choose_frame_allocator(get_env(rcvr))); auto* op = traits::allocate(alloc, 1); __scope_guard guard{[&]() noexcept { traits::deallocate(alloc, op, 1); }}; + Factory factory; + traits::construct(alloc, op, factory(std::forward(args)...), @@ -333,7 +362,7 @@ namespace experimental::execution } template - constexpr _func_op> connect(Receiver rcvr) + constexpr _func_op, Env> connect(Receiver rcvr) { return {std::move(rcvr), [this](auto rcvr) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 1a2d5aefe..f927426b5 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -21,6 +21,8 @@ #include +#include + namespace ex = STDEXEC; namespace @@ -99,4 +101,30 @@ namespace REQUIRE_THROWS_AS(ex::sync_wait(std::move(sndr)), int); } } + + 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 Env = + ex::env>>; + + exec::function sndr( + []() noexcept + { + return ex::read_env(exec::get_frame_allocator) + | ex::then( + [](auto alloc) noexcept + { + return std::same_as, decltype(alloc)>; + }); + }); + + std::pmr::polymorphic_allocator alloc; + + auto [ret] = ex::sync_wait(std::move(sndr) + | ex::write_env(ex::prop(exec::get_frame_allocator, alloc))) + .value(); + + REQUIRE(ret); + } } // namespace From a738dd71d46a556a2fef422e6ff2dcd1827bd149 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 15:33:55 -0700 Subject: [PATCH 14/20] Environment forwarding works(ish) This needs cleaning up and a *lot* more tests, but the current tests build and pass with a synthesized polymorphic environment. --- include/exec/function.hpp | 336 +++++++++++++++++++++++++++--------- test/exec/test_function.cpp | 14 +- 2 files changed, 266 insertions(+), 84 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index c965423d1..28fda5db2 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -22,7 +22,6 @@ #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__scope.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" -#include "../stdexec/execution.hpp" #include #include @@ -75,6 +74,41 @@ namespace experimental::execution inline constexpr get_frame_allocator_t get_frame_allocator{}; + namespace _qry_detail + { + using namespace STDEXEC; + + template + concept _conditionally_nothrow_queryable_with = + (!NoThrow && __queryable_with) + || __nothrow_queryable_with; + + template + concept _query_result_convertible_to = + (!NoThrow && std::is_convertible_v<__query_result_t, Expected>) + || std::is_nothrow_convertible_v<__query_result_t, Expected>; + + template + struct query; + + template + struct query + { + protected: + template + requires _conditionally_nothrow_queryable_with + && _query_result_convertible_to + static Return query_delegate(Env const &env, Query query, Args &&...args) noexcept(NoThrow) + { + return __query()(env, std::forward(args)...); + } + }; + } // namespace _qry_detail + + template + struct queries : _qry_detail::query... + {}; + namespace _func { using namespace STDEXEC; @@ -87,51 +121,158 @@ namespace experimental::execution { constexpr _virt_completion() = default; - _virt_completion(_virt_completion&&) = delete; + _virt_completion(_virt_completion &&) = delete; - constexpr virtual void complete(CPO, Args&&...) noexcept = 0; + constexpr virtual void complete(CPO, Args &&...) noexcept = 0; protected: constexpr ~_virt_completion() = default; }; - template + template + struct _env_of_queries + {}; + + template + struct _env_of_queries + { + virtual Return query(Query query, Args &&...args) const noexcept(NoThrow) = 0; + }; + + template + struct _env_of_queries + : _env_of_queries + { + _env_of_queries() = default; + + _env_of_queries(_env_of_queries &&) = delete; + + using _env_of_queries::query; + + virtual Return query(Query query, Args &&...args) const noexcept(NoThrow) = 0; + + protected: + ~_env_of_queries() = default; + }; + + template + struct _delegate_env_base; + + template + struct _delegate_env_base : public Base + {}; + + template + struct _delegate_env_base + : _delegate_env_base + { + using query_base = _qry_detail::query; + + Return query(Query qry, Args &&...args) const noexcept(NoThrow) final + { + auto &delegate = **static_cast(this); + return __query()(delegate, std::forward(args)...); + } + }; + + template + struct _delegate_env; + + template <> + struct _delegate_env> + : _delegate_env_base<_env_of_queries<>, _delegate_env>> + { + using delegate_t = _env_of_queries<>; + + explicit _delegate_env(delegate_t const &delegate) noexcept + : delegate_(std::addressof(delegate)) + {} + + private: + delegate_t const *delegate_; + + template + friend class _delegte_env_base; + + delegate_t const &operator*() const noexcept + { + return *delegate_; + } + }; + + template + struct _delegate_env> + : _delegate_env_base<_env_of_queries, + _delegate_env>, + Queries...> + { + using delegate_t = _env_of_queries; + + explicit _delegate_env(delegate_t const &delegate) noexcept + : delegate_(std::addressof(delegate)) + {} + + //using _delegate_env_base<_env_of_queries + friend class _delegate_env_base; + + delegate_t const &operator*() const noexcept + { + return *delegate_; + } + }; + + template struct _virt_completions; - template - struct _virt_completions, Env> : _virt_completion... + template + struct _virt_completions, queries> + : _virt_completion... + , _env_of_queries { constexpr _virt_completions() = default; - _virt_completions(_virt_completions&&) = delete; + _virt_completions(_virt_completions &&) = delete; using _virt_completion::complete...; - virtual Env get_env() const noexcept = 0; + constexpr _delegate_env> get_env() const noexcept + { + return _delegate_env>(*this); + } protected: constexpr ~_virt_completions() = default; }; - template + template class _func_rcvr; - template - class _func_rcvr, Env> + template + class _func_rcvr, Queries> { - using completer_t = _virt_completions, Env>; + using completer_t = _virt_completions, Queries>; - completer_t* completer_; + completer_t *completer_; public: using receiver_concept = receiver_tag; - constexpr explicit _func_rcvr(completer_t& completer) noexcept + constexpr explicit _func_rcvr(completer_t &completer) noexcept : completer_(std::addressof(completer)) {} template - constexpr void set_error(Error&& err) && noexcept + constexpr void set_error(Error &&err) && noexcept requires requires { this->completer_->complete(set_error_t{}, std::forward(err)); } { this->completer_->complete(set_error_t{}, std::forward(err)); @@ -144,7 +285,7 @@ namespace experimental::execution } template - constexpr void set_value(Values&&... values) && noexcept + constexpr void set_value(Values &&...values) && noexcept requires requires { this->completer_->complete(set_value_t{}, std::forward(values)...); } @@ -152,7 +293,7 @@ namespace experimental::execution this->completer_->complete(set_value_t{}, std::forward(values)...); } - constexpr auto get_env() const noexcept -> env_of_t + constexpr auto get_env() const noexcept -> _delegate_env { return STDEXEC::get_env(*completer_); } @@ -162,7 +303,7 @@ namespace experimental::execution { constexpr _base_op() = default; - _base_op(_base_op&&) = delete; + _base_op(_base_op &&) = delete; constexpr virtual ~_base_op() = default; @@ -172,13 +313,13 @@ namespace experimental::execution template struct _derived_op : _base_op { - constexpr explicit _derived_op(Sender&& sndr, Receiver rcvr, Allocator const & alloc) + constexpr explicit _derived_op(Sender &&sndr, Receiver rcvr, Allocator const &alloc) noexcept(std::is_nothrow_invocable_v) : op_(connect(std::forward(sndr), std::move(rcvr))) , alloc_(alloc) {} - _derived_op(_derived_op&&) = delete; + _derived_op(_derived_op &&) = delete; constexpr ~_derived_op() final = default; @@ -187,7 +328,7 @@ namespace experimental::execution ::STDEXEC::start(op_); } - static constexpr void operator delete(_derived_op* p, std::destroying_delete_t) + static constexpr void operator delete(_derived_op *p, std::destroying_delete_t) { using traits = std::allocator_traits::template rebind_traits<_derived_op>; @@ -213,7 +354,7 @@ namespace experimental::execution struct _func_op_completion : _func_op_completion { - void complete(CPO, Args&&... args) noexcept final + void complete(CPO, Args &&...args) noexcept final { // This seems like it ought to be true, but it fails... // @@ -221,40 +362,60 @@ namespace experimental::execution // during constraint satisfaction testing. // // static_assert(std::derived_from<_func_op_completion, Derived>); - auto& rcvr = static_cast(this)->rcvr_; + auto &rcvr = static_cast(this)->rcvr_; CPO{}(std::move(rcvr), std::forward(args)...); } }; - template + template + struct _func_op_queries; + + template + struct _func_op_queries> : Base + {}; + + template + struct _func_op_queries> + : _func_op_queries> + { + Return query(Query, Args &&...args) const noexcept(NoThrow) final + { + using delegate_t = _qry_detail::query; + + auto const &rcvr = static_cast(this)->rcvr_; + return __query()(STDEXEC::get_env(rcvr), std::forward(args)...); + } + }; + + template class _func_op; - template - class _func_op, Env> - : private _func_op_completion<_virt_completions, Env>, - _func_op, Env>, - Sigs...> + template + class _func_op, Queries> + : _func_op_completion< + _func_op_queries<_virt_completions, Queries>, + _func_op, Queries>, + Queries>, + _func_op, Queries>, + Sigs...> { - std::unique_ptr<_base_op> op_; [[no_unique_address]] - Receiver rcvr_; + Receiver rcvr_; + std::unique_ptr<_base_op> op_; template friend struct _func_op_completion; - constexpr Env get_env() const noexcept final - { - using RcvrEnv = env_of_t; - - if constexpr (std::constructible_from) - { - return Env(::STDEXEC::get_env(rcvr_)); - } - else - { - return {}; - } - } + template + friend struct _func_op_queries; public: using operation_state_concept = operation_state_tag; @@ -262,9 +423,10 @@ namespace experimental::execution template constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) - , op_(factory(_func_rcvr, Env>(*this))){}; + , op_(factory(_func_rcvr, Queries>(*this))) + {} - _func_op(_func_op&&) = delete; + _func_op(_func_op &&) = delete; constexpr ~_func_op() = default; @@ -275,7 +437,7 @@ namespace experimental::execution }; template - constexpr auto choose_frame_allocator(Env const & env) noexcept + constexpr auto choose_frame_allocator(Env const &env) noexcept { if constexpr (requires { get_frame_allocator(env); }) { @@ -291,14 +453,14 @@ namespace experimental::execution } } - template + template class _func_impl; - template - class _func_impl, Env> + template + class _func_impl, queries> { - std::unique_ptr<_base_op> (*factory_)(_func_rcvr, Env>, - Args&&...); + std::unique_ptr<_base_op> ( + *factory_)(_func_rcvr, queries>, Args &&...); [[no_unique_address]] std::tuple args_; @@ -310,15 +472,15 @@ namespace experimental::execution && std::constructible_from // && STDEXEC::__callable && STDEXEC::sender_to, - _func_rcvr, Env>> - constexpr explicit(sizeof...(Args) == 0) _func_impl(Args&&... args, Factory&& factory) + _func_rcvr, queries>> + constexpr explicit(sizeof...(Args) == 0) _func_impl(Args &&...args, Factory &&factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) { using sender_t = std::invoke_result_t; - using receiver_t = _func_rcvr, Env>; + using receiver_t = _func_rcvr, queries>; - factory_ = [](receiver_t rcvr, Args&&... args) -> std::unique_ptr<_base_op> + factory_ = [](receiver_t rcvr, Args &&...args) -> std::unique_ptr<_base_op> { // the type of the allocator provided by the receiver's environment using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); @@ -333,10 +495,10 @@ namespace experimental::execution // finally, the allocator traits for an allocator that can allocate an op_t using traits = traits_t::template rebind_traits; - // ...and the allocator itself + // ...and the allocator itself typename traits::allocator_type alloc(choose_frame_allocator(get_env(rcvr))); - auto* op = traits::allocate(alloc, 1); + auto *op = traits::allocate(alloc, 1); __scope_guard guard{[&]() noexcept { traits::deallocate(alloc, op, 1); }}; @@ -354,21 +516,34 @@ namespace experimental::execution }; } - template - static consteval completion_signatures get_completion_signatures() noexcept + template + static consteval auto get_completion_signatures() noexcept { - // TODO: validate that the Env passed here is compatible with the class-level Env - return {}; + static_assert(STDEXEC_IS_BASE_OF(_func_impl, __decay_t)); + //static_assert(std::constructible_from); + + //Env env{RcvrEnv{}}; + + //if constexpr (std::constructible_from) + { + return completion_signatures{}; + } + //else + //{ + // TODO: make this error accurate + //return __throw_compile_time_error(__unrecognized_sender_error_t()); + //} } template - constexpr _func_op, Env> connect(Receiver rcvr) + constexpr _func_op, queries> + connect(Receiver rcvr) { return {std::move(rcvr), [this](auto rcvr) { return std::apply( - [&rcvr, this](Args&&... args) + [&rcvr, this](Args &&...args) { return factory_(std::move(rcvr), std::forward(args)...); }, std::move(args_)); }}; @@ -426,11 +601,11 @@ namespace experimental::execution struct function : _func::_func_impl, - STDEXEC::env<>> + queries<>> { using base = _func::_func_impl, - STDEXEC::env<>>; + queries<>>; using base::base; }; @@ -438,34 +613,37 @@ namespace experimental::execution template requires STDEXEC::__is_instance_of struct function - : _func::_func_impl> + : _func::_func_impl> { - using base = _func::_func_impl>; + using base = _func::_func_impl>; using base::base; }; - template - requires STDEXEC::__is_not_instance_of - struct function + template + struct function> : _func::_func_impl, - Env> + queries> { using base = _func::_func_impl, - Env>; + queries>; using base::base; }; - template - requires STDEXEC::__is_not_instance_of - struct function, Env> - : _func::_func_impl, Env> + template + struct function, + queries> + : _func::_func_impl, + queries> { - using base = - _func::_func_impl, Env>; + using base = _func::_func_impl, + queries>; using base::base; }; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index f927426b5..3fedbd7fe 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -46,9 +46,11 @@ namespace exec::function> onlystopped( []() noexcept { return ex::just_stopped(); }); - exec::function> trivialCustomEnv([]() noexcept { return ex::just(); }); + exec::function> trivialCustomEnv([]() noexcept { return ex::just(); }); - exec::function, ex::env<>> + exec::function, + exec::queries<>> totalControl(5, [](int) noexcept { return ex::just(); }); STATIC_REQUIRE(STDEXEC::sender); @@ -105,10 +107,12 @@ 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 Env = - ex::env>>; + //using Env = + //ex::env>>; + 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 1094cc5151afe5e6620669ce6e2046a2bb3b63ac Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 16:40:22 -0700 Subject: [PATCH 15/20] Tidy up and add comments This diff does some tidying and adds documentation. There are still some TODOs, but this is in good enough shape that I can start sharing it, I think. --- include/exec/function.hpp | 335 +++++++++++++++++++++----------------- 1 file changed, 188 insertions(+), 147 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 28fda5db2..757e7651c 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -49,6 +49,8 @@ // 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(); @@ -76,72 +78,51 @@ namespace experimental::execution namespace _qry_detail { - using namespace STDEXEC; - - template - concept _conditionally_nothrow_queryable_with = - (!NoThrow && __queryable_with) - || __nothrow_queryable_with; - - template - concept _query_result_convertible_to = - (!NoThrow && std::is_convertible_v<__query_result_t, Expected>) - || std::is_nothrow_convertible_v<__query_result_t, Expected>; - template - struct query; + inline constexpr bool is_query_function_v = false; template - struct query - { - protected: - template - requires _conditionally_nothrow_queryable_with - && _query_result_convertible_to - static Return query_delegate(Env const &env, Query query, Args &&...args) noexcept(NoThrow) - { - return __query()(env, std::forward(args)...); - } - }; + inline constexpr bool is_query_function_v = true; } // namespace _qry_detail + // a "type list" for bundling together function type representing queries to support in + // a type-erased environment. All of the types in Queries... must be (possibly noexcept) + // function types. For example: + // + // queries< + // std::execution::inline_stop_token(std::execution::get_stop_token_t) noexcept, + // std::pmr::polymorphic_allocator(std::execution::get_allocator_t) + // > template - struct queries : _qry_detail::query... + requires(_qry_detail::is_query_function_v && ...) + struct queries {}; namespace _func { using namespace STDEXEC; - template - struct _virt_completion; - - template - struct _virt_completion - { - constexpr _virt_completion() = default; - - _virt_completion(_virt_completion &&) = delete; - - constexpr virtual void complete(CPO, Args &&...) noexcept = 0; - - protected: - constexpr ~_virt_completion() = default; - }; - + // a recursively-defined type with a vtable containing one virtual function for + // each query in Queries... + // + // the base template is an empty class, representing the empty set of queries. template struct _env_of_queries {}; + // a special case in the recursion: when there is only one query in the pack, there's + // no base implementation of query to put in the using statement template struct _env_of_queries { virtual Return query(Query query, Args &&...args) const noexcept(NoThrow) = 0; }; + // the recursive case that declares the named query as a pure virtual member function + // and inherits the rest of the required queries through inheritance template struct _env_of_queries - : _env_of_queries + : private _env_of_queries { _env_of_queries() = default; @@ -155,87 +136,64 @@ namespace experimental::execution ~_env_of_queries() = default; }; - template - struct _delegate_env_base; - - template - struct _delegate_env_base : public Base - {}; - - template - struct _delegate_env_base - : _delegate_env_base - { - using query_base = _qry_detail::query; - - Return query(Query qry, Args &&...args) const noexcept(NoThrow) final - { - auto &delegate = **static_cast(this); - return __query()(delegate, std::forward(args)...); - } - }; - - template - struct _delegate_env; - - template <> - struct _delegate_env> - : _delegate_env_base<_env_of_queries<>, _delegate_env>> + // an environment type that delegates query to an _env_of_queries so that the + // environment type that we traffic in is cheaply copyable + template + struct _delegate_env { - using delegate_t = _env_of_queries<>; + using delegate_t = _env_of_queries; explicit _delegate_env(delegate_t const &delegate) noexcept : delegate_(std::addressof(delegate)) {} + template + requires __queryable_with + constexpr auto query(Query, Args &&...args) const + noexcept(__nothrow_queryable_with) + -> __query_result_t + { + return __query()(*delegate_, std::forward(args)...); + } + private: delegate_t const *delegate_; + }; - template - friend class _delegte_env_base; + // in the base case, there's no need to store a pointer + template <> + struct _delegate_env<> + { + using delegate_t = _env_of_queries<>; - delegate_t const &operator*() const noexcept - { - return *delegate_; - } + explicit _delegate_env(delegate_t const &) noexcept {} }; - template - struct _delegate_env> - : _delegate_env_base<_env_of_queries, - _delegate_env>, - Queries...> - { - using delegate_t = _env_of_queries; + template + struct _virt_completion; - explicit _delegate_env(delegate_t const &delegate) noexcept - : delegate_(std::addressof(delegate)) - {} + // a vtable entry representing a receiver completion function; CPO should be a completion + // function (e.g. set_Value_t), and Args... is the expected argument list. + template + struct _virt_completion + { + constexpr _virt_completion() = default; - //using _delegate_env_base<_env_of_queries - friend class _delegate_env_base; + constexpr virtual void complete(CPO, Args &&...) noexcept = 0; - delegate_t const &operator*() const noexcept - { - return *delegate_; - } + protected: + constexpr ~_virt_completion() = default; }; - template + template struct _virt_completions; + // a class template that bundles together a pure virtual completion function for each + // of the specified completion functions, and provides an implementation of get_env template - struct _virt_completions, queries> + struct _virt_completions, Queries...> : _virt_completion... , _env_of_queries { @@ -243,24 +201,33 @@ namespace experimental::execution _virt_completions(_virt_completions &&) = delete; + // this will complain if sizeof...(Sigs) == 0, but a sender with no completions + // isn't super useful... using _virt_completion::complete...; - constexpr _delegate_env> get_env() const noexcept + constexpr _delegate_env get_env() const noexcept { - return _delegate_env>(*this); + return _delegate_env(*this); } protected: constexpr ~_virt_completions() = default; }; - template + template class _func_rcvr; - template - class _func_rcvr, Queries> + // a type-erased receiver expecting to be completed by one of the completions specified + // in Sigs..., and providing an environment that supports the queries specified in + // Queries... + // + // this is the receiver type that is passed into the sender being type-erased by a + // function<...>, and it forwards completions to the concrete receiver through the + // internal completer_ pointer + template + class _func_rcvr, Queries...> { - using completer_t = _virt_completions, Queries>; + using completer_t = _virt_completions, Queries...>; completer_t *completer_; @@ -273,32 +240,31 @@ namespace experimental::execution template constexpr void set_error(Error &&err) && noexcept - requires requires { this->completer_->complete(set_error_t{}, std::forward(err)); } + requires requires { completer_->complete(set_error_t{}, std::forward(err)); } { - this->completer_->complete(set_error_t{}, std::forward(err)); + completer_->complete(set_error_t{}, std::forward(err)); } constexpr void set_stopped() && noexcept - requires requires { this->completer_->complete(set_stopped_t{}); } + requires requires { completer_->complete(set_stopped_t{}); } { - this->completer_->complete(set_stopped_t{}); + completer_->complete(set_stopped_t{}); } template constexpr void set_value(Values &&...values) && noexcept - requires requires { - this->completer_->complete(set_value_t{}, std::forward(values)...); - } + requires requires { completer_->complete(set_value_t{}, std::forward(values)...); } { - this->completer_->complete(set_value_t{}, std::forward(values)...); + completer_->complete(set_value_t{}, std::forward(values)...); } - constexpr auto get_env() const noexcept -> _delegate_env + constexpr auto get_env() const noexcept -> _delegate_env { return STDEXEC::get_env(*completer_); } }; + // the type-erased operation state type that supports starting and destruction struct _base_op { constexpr _base_op() = default; @@ -310,6 +276,9 @@ namespace experimental::execution constexpr virtual void start() & noexcept = 0; }; + // the operation state resulting from connecting a sender being erased by a function<...> + // with a _func_rcvr<...>; inherits from _base_op, and provides a class-specific override + // of operator delete that invokes the allocator deallocation protocol template struct _derived_op : _base_op { @@ -328,6 +297,10 @@ namespace experimental::execution ::STDEXEC::start(op_); } + // objects of this type are allocated with an allocator of type Allocator so they need + // to be deallocated using the same allocator; providing a class-specific overload of + // a destroying operator delete allows us to store the relevant allocator inside the + // to-be-destroyed object and retrieve it before running the destructor static constexpr void operator delete(_derived_op *p, std::destroying_delete_t) { using traits = std::allocator_traits::template rebind_traits<_derived_op>; @@ -343,13 +316,20 @@ namespace experimental::execution Allocator alloc_; }; + // a recursive implementation of Base, which is expected to inherit from + // _virt_completions template struct _func_op_completion; + // the base case of the recursive implementation; all subclasses of this type have, + // together, overridden all the virtual functions in Base so now we just need to + // inherit from Base to ensure those virtual functions exist to be overridden template struct _func_op_completion : Base {}; + // the recursive case, which implements a single overload of complete and delegates + // the implementation of all remaining overloads to the base class template struct _func_op_completion : _func_op_completion @@ -358,22 +338,31 @@ namespace experimental::execution { // This seems like it ought to be true, but it fails... // - // Some testing shows it's being evaluated when Derive is incomplete + // Some testing shows it's being evaluated when Derived is incomplete // during constraint satisfaction testing. // // static_assert(std::derived_from<_func_op_completion, Derived>); + // + // Consider: what if _func_op_completion (i.e. the base case of + // this recursive class hierarchy) owned the receiver? We could avoid + // CRTP and just use this->rcvr_, maybe. auto &rcvr = static_cast(this)->rcvr_; CPO{}(std::move(rcvr), std::forward(args)...); } }; - template + // a recursive implementation of all the queries in Queries... + template struct _func_op_queries; + // the base case of the recursive implementation; there are no more queries to + // implement so just inherit from Base template - struct _func_op_queries> : Base + struct _func_op_queries : Base {}; + // the recursive case, which implements a single query overload and delegates the + // implementation of the remaining overloads to the base class template - struct _func_op_queries> - : _func_op_queries> + struct _func_op_queries + : _func_op_queries { Return query(Query, Args &&...args) const noexcept(NoThrow) final { - using delegate_t = _qry_detail::query; - + // the idea of storing the receiver in the base class could help here, too, but + // we'd need to be careful about which class template is actually the base class auto const &rcvr = static_cast(this)->rcvr_; return __query()(STDEXEC::get_env(rcvr), std::forward(args)...); } }; - template + template class _func_op; - template - class _func_op, Queries> + // the concrete operation state resulting from connecting a function<...> to a concrete + // receiver of type Receiver. this type manages a dynamically-allocated _derived_op instance, + // which is the type-erased operation state resulting from connecting the type-erased sender + // to a _func_rcvr + template + class _func_op, Queries...> : _func_op_completion< - _func_op_queries<_virt_completions, Queries>, - _func_op, Queries>, - Queries>, - _func_op, Queries>, + _func_op_queries<_virt_completions, Queries...>, + _func_op, Queries...>, + Queries...>, + _func_op, Queries...>, Sigs...> { + // 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 [[no_unique_address]] - Receiver rcvr_; + Receiver rcvr_; + // the default deleter is OK because we've virtualized operator delete to invoke + // the allocator-based deallocation logic that's necessary to properly support + // a user-provided frame allocator std::unique_ptr<_base_op> op_; - template + // these friend declaratiosn allow our CRTP base classes to access rcvr_; they could + // disappear if we moved ownership of rcvr_ into the base class object + template friend struct _func_op_completion; - template + template friend struct _func_op_queries; public: @@ -423,7 +421,7 @@ namespace experimental::execution template constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) - , op_(factory(_func_rcvr, Queries>(*this))) + , op_(factory(_func_rcvr, Queries...>(*this))) {} _func_op(_func_op &&) = delete; @@ -436,6 +434,9 @@ namespace experimental::execution } }; + // 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 template constexpr auto choose_frame_allocator(Env const &env) noexcept { @@ -456,29 +457,43 @@ namespace experimental::execution template class _func_impl; + // 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> { - std::unique_ptr<_base_op> ( - *factory_)(_func_rcvr, queries>, Args &&...); + // the type-erased sender factory that, when called, constructs the erased sender from + // args_ and connects the resulting sender to the provided receiver + std::unique_ptr<_base_op> (*factory_)(_func_rcvr, Queries...>, + Args &&...); [[no_unique_address]] std::tuple args_; public: using sender_concept = SndrCncpt; + // TODO: I only know this works for empty lambdas; figure out whether function pointers + // and/or pointer-to-member functions can be made to work template Factory> requires STDEXEC::__not_decays_to // && std::constructible_from // && STDEXEC::__callable && STDEXEC::sender_to, - _func_rcvr, queries>> + _func_rcvr, Queries...>> constexpr explicit(sizeof...(Args) == 0) _func_impl(Args &&...args, Factory &&factory) noexcept((std::is_nothrow_constructible_v && ...)) : args_(std::forward(args)...) { using sender_t = std::invoke_result_t; - using receiver_t = _func_rcvr, queries>; + using receiver_t = _func_rcvr, Queries...>; factory_ = [](receiver_t rcvr, Args &&...args) -> std::unique_ptr<_base_op> { @@ -502,6 +517,9 @@ namespace experimental::execution __scope_guard guard{[&]() noexcept { traits::deallocate(alloc, op, 1); }}; + // TODO: as mentioned above, Factory must be a stateless lambda, which makes it + // default-constructible like this; this obviously doesn't work if Factory + // is a pointer type Factory factory; traits::construct(alloc, @@ -516,14 +534,13 @@ namespace experimental::execution }; } - template + template static consteval auto get_completion_signatures() noexcept { static_assert(STDEXEC_IS_BASE_OF(_func_impl, __decay_t)); - //static_assert(std::constructible_from); - - //Env env{RcvrEnv{}}; + // TODO: validate that Env supports all the required queries + // //if constexpr (std::constructible_from) { return completion_signatures{}; @@ -535,8 +552,9 @@ namespace experimental::execution //} } + // TODO: this assumes rvalue connection; lvalue connection requires thought and tests template - constexpr _func_op, queries> + constexpr _func_op, Queries...> connect(Receiver rcvr) { return {std::move(rcvr), @@ -550,6 +568,10 @@ namespace experimental::execution } }; + // given a possibly-noexcept function type like Return(Args...), compute the appropriate + // completion_signatures. The result is a set_value overload taking either Return&& or + // no args when Return is void, set_stopped, and, when the function type is not noexcept, + // set_error(std::exception_ptr) template struct _sigs_from; @@ -586,7 +608,26 @@ namespace experimental::execution using _sigs_from_t = _sigs_from::type; } // namespace _func - // TODO: think about environment forwarding + // the user-facing interface to exec::function that supports several different declaration + // styles, including: + // - function: a fallible function from (bar, baz) to int + // - function: an infallible function from (bar, baz) to int + // - function>: a function from (bar, baz) + // that completes in the ways specified by the given specialization of completion_signatures + // - function: a function from (bar, baz) + // to int that requires the final receiver to have an environment that supports the + // Query query, taking arguments Args..., and returning an object convertible to Return; queries + // may be required to be no-throw by delcaring the function type noexcept + // - function< + // sender_tag(bar, baz), + // completion_signatures<...>, + // queries>: a fully-specified async function that maps (bar, baz) + // to the specified completions, requiring the specified queries in the ultimate receiver's + // environment + // + // Future: support C-style ellipsis arguments in the function signature to permit type-erased + // arguments as well, like function (a fallible function from + // (bar, baz) plus unspecified, erased additional arguments to int) template struct function; From dc225b81bd1e6df94442145439142b53eb845136 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:25:36 -0700 Subject: [PATCH 16/20] Remove [[no_unique_address]] Per code review feedback, replace `[[no_unique_address]]` with `STDEXEC_ATTRIBUTE(no_unique_address)`. --- include/exec/function.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 757e7651c..d18cc46c0 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -312,7 +312,7 @@ namespace experimental::execution private: connect_result_t op_; - [[no_unique_address]] + STDEXEC_ATTRIBUTE(no_unique_address) Allocator alloc_; }; @@ -400,7 +400,7 @@ namespace experimental::execution { // 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 - [[no_unique_address]] + STDEXEC_ATTRIBUTE(no_unique_address) Receiver rcvr_; // the default deleter is OK because we've virtualized operator delete to invoke // the allocator-based deallocation logic that's necessary to properly support @@ -474,7 +474,7 @@ namespace experimental::execution // args_ and connects the resulting sender to the provided receiver std::unique_ptr<_base_op> (*factory_)(_func_rcvr, Queries...>, Args &&...); - [[no_unique_address]] + STDEXEC_ATTRIBUTE(no_unique_address) std::tuple args_; public: From a14b6bc432fbbf48c665930cca76e835102e58cb Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:28:22 -0700 Subject: [PATCH 17/20] Clean up the _func_impl constructor Take @ericniebler's code review feedback to clean up the declaration of `exec::_func::_func_impl`'s constructor. --- include/exec/function.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d18cc46c0..e9eb06f89 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -484,12 +484,12 @@ namespace experimental::execution // and/or pointer-to-member functions can be made to work template Factory> requires STDEXEC::__not_decays_to // - && std::constructible_from // + && STDEXEC::__std::constructible_from // && STDEXEC::__callable && STDEXEC::sender_to, _func_rcvr, Queries...>> - constexpr explicit(sizeof...(Args) == 0) _func_impl(Args &&...args, Factory &&factory) - noexcept((std::is_nothrow_constructible_v && ...)) + constexpr explicit _func_impl(Args &&...args, Factory &&factory) + noexcept(STDEXEC::__nothrow_move_constructible) : args_(std::forward(args)...) { using sender_t = std::invoke_result_t; From eb953d2f3394ee039117494e57b36644c4a65da9 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 20:32:12 -0700 Subject: [PATCH 18/20] Replace most of exec::function with _any_receiver_ref This commit replaces the vtable-building shenanigans in `exec::function` with the `exec::_any::_any_receiver_ref` class template in `any_sender_of.hpp`. The comments probably still need cleaning up, and there's a `TODO` to pull the stuff in `any_sender_of.hpp` that's shared between `exec::any_sender_of` and `exec::function` into a separate, shared header. --- include/exec/function.hpp | 279 +++----------------------------------- 1 file changed, 16 insertions(+), 263 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index e9eb06f89..a9a516d12 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -23,6 +23,9 @@ #include "../stdexec/__detail/__scope.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" +// TODO: split this header into pieces +#include "any_sender_of.hpp" + #include #include #include @@ -76,6 +79,7 @@ namespace experimental::execution inline constexpr get_frame_allocator_t get_frame_allocator{}; +#if 0 namespace _qry_detail { template @@ -84,186 +88,12 @@ namespace experimental::execution template inline constexpr bool is_query_function_v = true; } // namespace _qry_detail - - // a "type list" for bundling together function type representing queries to support in - // a type-erased environment. All of the types in Queries... must be (possibly noexcept) - // function types. For example: - // - // queries< - // std::execution::inline_stop_token(std::execution::get_stop_token_t) noexcept, - // std::pmr::polymorphic_allocator(std::execution::get_allocator_t) - // > - template - requires(_qry_detail::is_query_function_v && ...) - struct queries - {}; +#endif namespace _func { using namespace STDEXEC; - // a recursively-defined type with a vtable containing one virtual function for - // each query in Queries... - // - // the base template is an empty class, representing the empty set of queries. - template - struct _env_of_queries - {}; - - // a special case in the recursion: when there is only one query in the pack, there's - // no base implementation of query to put in the using statement - template - struct _env_of_queries - { - virtual Return query(Query query, Args &&...args) const noexcept(NoThrow) = 0; - }; - - // the recursive case that declares the named query as a pure virtual member function - // and inherits the rest of the required queries through inheritance - template - struct _env_of_queries - : private _env_of_queries - { - _env_of_queries() = default; - - _env_of_queries(_env_of_queries &&) = delete; - - using _env_of_queries::query; - - virtual Return query(Query query, Args &&...args) const noexcept(NoThrow) = 0; - - protected: - ~_env_of_queries() = default; - }; - - // an environment type that delegates query to an _env_of_queries so that the - // environment type that we traffic in is cheaply copyable - template - struct _delegate_env - { - using delegate_t = _env_of_queries; - - explicit _delegate_env(delegate_t const &delegate) noexcept - : delegate_(std::addressof(delegate)) - {} - - template - requires __queryable_with - constexpr auto query(Query, Args &&...args) const - noexcept(__nothrow_queryable_with) - -> __query_result_t - { - return __query()(*delegate_, std::forward(args)...); - } - - private: - delegate_t const *delegate_; - }; - - // in the base case, there's no need to store a pointer - template <> - struct _delegate_env<> - { - using delegate_t = _env_of_queries<>; - - explicit _delegate_env(delegate_t const &) noexcept {} - }; - - template - struct _virt_completion; - - // a vtable entry representing a receiver completion function; CPO should be a completion - // function (e.g. set_Value_t), and Args... is the expected argument list. - template - struct _virt_completion - { - constexpr _virt_completion() = default; - - _virt_completion(_virt_completion &&) = delete; - - constexpr virtual void complete(CPO, Args &&...) noexcept = 0; - - protected: - constexpr ~_virt_completion() = default; - }; - - template - struct _virt_completions; - - // a class template that bundles together a pure virtual completion function for each - // of the specified completion functions, and provides an implementation of get_env - template - struct _virt_completions, Queries...> - : _virt_completion... - , _env_of_queries - { - constexpr _virt_completions() = default; - - _virt_completions(_virt_completions &&) = delete; - - // this will complain if sizeof...(Sigs) == 0, but a sender with no completions - // isn't super useful... - using _virt_completion::complete...; - - constexpr _delegate_env get_env() const noexcept - { - return _delegate_env(*this); - } - - protected: - constexpr ~_virt_completions() = default; - }; - - template - class _func_rcvr; - - // a type-erased receiver expecting to be completed by one of the completions specified - // in Sigs..., and providing an environment that supports the queries specified in - // Queries... - // - // this is the receiver type that is passed into the sender being type-erased by a - // function<...>, and it forwards completions to the concrete receiver through the - // internal completer_ pointer - template - class _func_rcvr, Queries...> - { - using completer_t = _virt_completions, Queries...>; - - completer_t *completer_; - - public: - using receiver_concept = receiver_tag; - - constexpr explicit _func_rcvr(completer_t &completer) noexcept - : completer_(std::addressof(completer)) - {} - - template - constexpr void set_error(Error &&err) && noexcept - requires requires { completer_->complete(set_error_t{}, std::forward(err)); } - { - completer_->complete(set_error_t{}, std::forward(err)); - } - - constexpr void set_stopped() && noexcept - requires requires { completer_->complete(set_stopped_t{}); } - { - completer_->complete(set_stopped_t{}); - } - - template - constexpr void set_value(Values &&...values) && noexcept - requires requires { completer_->complete(set_value_t{}, std::forward(values)...); } - { - completer_->complete(set_value_t{}, std::forward(values)...); - } - - constexpr auto get_env() const noexcept -> _delegate_env - { - return STDEXEC::get_env(*completer_); - } - }; - // the type-erased operation state type that supports starting and destruction struct _base_op { @@ -316,72 +146,6 @@ namespace experimental::execution Allocator alloc_; }; - // a recursive implementation of Base, which is expected to inherit from - // _virt_completions - template - struct _func_op_completion; - - // the base case of the recursive implementation; all subclasses of this type have, - // together, overridden all the virtual functions in Base so now we just need to - // inherit from Base to ensure those virtual functions exist to be overridden - template - struct _func_op_completion : Base - {}; - - // the recursive case, which implements a single overload of complete and delegates - // the implementation of all remaining overloads to the base class - template - struct _func_op_completion - : _func_op_completion - { - void complete(CPO, Args &&...args) noexcept final - { - // This seems like it ought to be true, but it fails... - // - // Some testing shows it's being evaluated when Derived is incomplete - // during constraint satisfaction testing. - // - // static_assert(std::derived_from<_func_op_completion, Derived>); - // - // Consider: what if _func_op_completion (i.e. the base case of - // this recursive class hierarchy) owned the receiver? We could avoid - // CRTP and just use this->rcvr_, maybe. - auto &rcvr = static_cast(this)->rcvr_; - CPO{}(std::move(rcvr), std::forward(args)...); - } - }; - - // a recursive implementation of all the queries in Queries... - template - struct _func_op_queries; - - // the base case of the recursive implementation; there are no more queries to - // implement so just inherit from Base - template - struct _func_op_queries : Base - {}; - - // the recursive case, which implements a single query overload and delegates the - // implementation of the remaining overloads to the base class - template - struct _func_op_queries - : _func_op_queries - { - Return query(Query, Args &&...args) const noexcept(NoThrow) final - { - // the idea of storing the receiver in the base class could help here, too, but - // we'd need to be careful about which class template is actually the base class - auto const &rcvr = static_cast(this)->rcvr_; - return __query()(STDEXEC::get_env(rcvr), std::forward(args)...); - } - }; - template class _func_op; @@ -391,12 +155,6 @@ namespace experimental::execution // to a _func_rcvr template class _func_op, Queries...> - : _func_op_completion< - _func_op_queries<_virt_completions, Queries...>, - _func_op, Queries...>, - Queries...>, - _func_op, Queries...>, - Sigs...> { // 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 @@ -407,13 +165,8 @@ namespace experimental::execution // a user-provided frame allocator std::unique_ptr<_base_op> op_; - // these friend declaratiosn allow our CRTP base classes to access rcvr_; they could - // disappear if we moved ownership of rcvr_ into the base class object - template - friend struct _func_op_completion; - - template - friend struct _func_op_queries; + using _receiver_t = + ::exec::_any::_any_receiver_ref, queries>; public: using operation_state_concept = operation_state_tag; @@ -421,7 +174,7 @@ namespace experimental::execution template constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) - , op_(factory(_func_rcvr, Queries...>(*this))) + , op_(factory(_receiver_t(rcvr_))) {} _func_op(_func_op &&) = delete; @@ -470,10 +223,12 @@ namespace experimental::execution template class _func_impl, queries> { + using _receiver_t = + ::exec::_any::_any_receiver_ref, queries>; + // the type-erased sender factory that, when called, constructs the erased sender from // args_ and connects the resulting sender to the provided receiver - std::unique_ptr<_base_op> (*factory_)(_func_rcvr, Queries...>, - Args &&...); + std::unique_ptr<_base_op> (*factory_)(_receiver_t, Args &&...); STDEXEC_ATTRIBUTE(no_unique_address) std::tuple args_; @@ -486,16 +241,14 @@ namespace experimental::execution requires STDEXEC::__not_decays_to // && STDEXEC::__std::constructible_from // && STDEXEC::__callable - && STDEXEC::sender_to, - _func_rcvr, Queries...>> + && STDEXEC::sender_to, _receiver_t> constexpr explicit _func_impl(Args &&...args, Factory &&factory) noexcept(STDEXEC::__nothrow_move_constructible) : args_(std::forward(args)...) { - using sender_t = std::invoke_result_t; - using receiver_t = _func_rcvr, Queries...>; + using sender_t = std::invoke_result_t; - factory_ = [](receiver_t rcvr, Args &&...args) -> std::unique_ptr<_base_op> + factory_ = [](_receiver_t rcvr, Args &&...args) -> std::unique_ptr<_base_op> { // the type of the allocator provided by the receiver's environment using alloc_t = decltype(choose_frame_allocator(get_env(rcvr))); @@ -505,7 +258,7 @@ namespace experimental::execution // the type of operation we'll ultimately allocate, which depends on the type of // the allocator we're using - using op_t = _derived_op; + using op_t = _derived_op; // finally, the allocator traits for an allocator that can allocate an op_t using traits = traits_t::template rebind_traits; From e0818b32ab1ba38f3ecaf7dee13aaa376132a160 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:26:19 -0700 Subject: [PATCH 19/20] Clean up the comments Update comments to match the new implementation. --- include/exec/function.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index a9a516d12..679979198 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -107,8 +107,8 @@ namespace experimental::execution }; // the operation state resulting from connecting a sender being erased by a function<...> - // with a _func_rcvr<...>; inherits from _base_op, and provides a class-specific override - // of operator delete that invokes the allocator deallocation protocol + // with an _any::_any_receiver_ref<...>; inherits from _base_op, and provides a + // class-specific override of operator delete that invokes the allocator deallocation protocol template struct _derived_op : _base_op { @@ -149,10 +149,10 @@ namespace experimental::execution template class _func_op; - // the concrete operation state resulting from connecting a function<...> to a concrete - // receiver of type Receiver. this type manages a dynamically-allocated _derived_op instance, + // The concrete operation state resulting from connecting a function<...> to a concrete + // receiver of type Receiver. This type manages a dynamically-allocated _derived_op instance, // which is the type-erased operation state resulting from connecting the type-erased sender - // to a _func_rcvr + // to an _any::_any_receiver_ref with the given completion signatures and queries. template class _func_op, Queries...> { @@ -388,7 +388,7 @@ namespace experimental::execution // should this require STDEXEC::__not_same_as? // // you *could* write STDEXEC::just(STDEXEC::sender_tag{}), but it seems more likely - // that invokign this specialization with Return set to sender_tag is a bug... + // that invoking this specialization with Return set to sender_tag is a bug... // // the same question applies to all the specializations below that take explicit // completion signatures From b65e281158b96a7ceeeccbdd7ada36cfbd429f7a Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:31:21 -0700 Subject: [PATCH 20/20] Stop deducing noexcept Take code review feedback and replace attempts to deduce a function type's `noexcept` clause with explicit partial specializations for both the throwing and non-throwing cases. --- include/exec/function.hpp | 49 +++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 679979198..e71630638 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -85,8 +85,11 @@ namespace experimental::execution template inline constexpr bool is_query_function_v = false; - template - inline constexpr bool is_query_function_v = true; + template + inline constexpr bool is_query_function_v = true; + + template + inline constexpr bool is_query_function_v = true; } // namespace _qry_detail #endif @@ -384,7 +387,7 @@ namespace experimental::execution template struct function; - template + template // should this require STDEXEC::__not_same_as? // // you *could* write STDEXEC::just(STDEXEC::sender_tag{}), but it seems more likely @@ -392,13 +395,26 @@ namespace experimental::execution // // the same question applies to all the specializations below that take explicit // completion signatures - struct function + struct function + : _func::_func_impl, + queries<>> + { + using base = _func::_func_impl, + queries<>>; + + using base::base; + }; + + template + struct function : _func::_func_impl, + _func::_sigs_from_t, queries<>> { using base = _func::_func_impl, + _func::_sigs_from_t, queries<>>; using base::base; @@ -414,14 +430,27 @@ namespace experimental::execution using base::base; }; - template - struct function> + template + struct function> + : _func::_func_impl, + queries> + { + using base = _func::_func_impl, + queries>; + + using base::base; + }; + + template + struct function> : _func::_func_impl, + _func::_sigs_from_t, queries> { using base = _func::_func_impl, + _func::_sigs_from_t, queries>; using base::base;