From 70480a0f28422c24eebf5870453e2efaa66f19ea Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 16 Apr 2026 20:36:56 -0700 Subject: [PATCH 01/31] 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 6b91628e3ab02fb9a40781d8532bbbd693acdf2d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 16:46:52 -0700 Subject: [PATCH 02/31] 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 8d77234ebd5242216e933b0b67fd724af5a30406 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 20:06:48 -0700 Subject: [PATCH 03/31] 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 33c445b9981b67002599d53626aa5dddec5cb34c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 09:31:23 -0700 Subject: [PATCH 04/31] 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 f8ae7a8743f8e0fe48ead7176b61c7bf7698a88f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 20:44:18 -0700 Subject: [PATCH 05/31] 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 3d3acea3bd5ca0d42e6c5900c29e2149b4e65cc4 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:03:14 -0700 Subject: [PATCH 06/31] 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 a47516672ce654645b4332d935fd5fbaf19471f9 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:19:20 -0700 Subject: [PATCH 07/31] 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 dbc417212730582fe76877c67dbb0cd8e9589d7d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:23:03 -0700 Subject: [PATCH 08/31] 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 b45d97abf507fee6416848e06cff53a9c736dcf3 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 12:50:23 -0700 Subject: [PATCH 09/31] 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 60b1408daea2e8056926c11de1ff43d6075f11b1 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 14:02:31 -0700 Subject: [PATCH 10/31] 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 83bbcb568b0b3b2b6112ac9a865da0bfbb8b1732 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 17:26:17 -0700 Subject: [PATCH 11/31] 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 9d8da86538991f91fe9c2c2a7098c56b190a925d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 18:35:26 -0700 Subject: [PATCH 12/31] 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 608cfa42d0e7dedf0af2a136daff3c3ae0e3bdc4 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 20 Apr 2026 07:40:51 -0700 Subject: [PATCH 13/31] 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 7b775513fa6d4a211abe15eb06c50fe1f4348871 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 15:33:55 -0700 Subject: [PATCH 14/31] 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 4065190cc3ea05b22fe1372b95a979cc259a008d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 16:40:22 -0700 Subject: [PATCH 15/31] 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 e891273b74f2be7ad3fe73ad61dd8bce084f8a8d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:25:36 -0700 Subject: [PATCH 16/31] 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 60096fd642bb3e65dcd49dbda00e491d34de573f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:28:22 -0700 Subject: [PATCH 17/31] 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 2a98495e2dfcc859d1a6cde923c23a37c88e0892 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 20:32:12 -0700 Subject: [PATCH 18/31] 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 3d9e6537252317a1d525a3e9ca237d4fd92bff8b Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:26:19 -0700 Subject: [PATCH 19/31] 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 67d3950ebd2630c0c3a44daa4c7ac4e02e58ba76 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:31:21 -0700 Subject: [PATCH 20/31] 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; From 0851467731aaa7d2690e6b4109572c4b22d00d13 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 07:52:09 -0700 Subject: [PATCH 21/31] Simplify with __any<_iopstate> Replace the `unique_ptr` to custom type-erased operation state with an `STDEXEC::__any::__any`; I might be able to go further and replace `_func_op` with `exec::_any::_any_opstate`, but I need to think about the stop token adaption it does before committing to that. --- include/exec/function.hpp | 100 +++++--------------------------------- 1 file changed, 12 insertions(+), 88 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index e71630638..0192fb6d9 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -97,58 +97,6 @@ namespace experimental::execution { using namespace STDEXEC; - // the type-erased operation state type that supports starting and destruction - struct _base_op - { - constexpr _base_op() = default; - - _base_op(_base_op &&) = delete; - - constexpr virtual ~_base_op() = default; - - constexpr virtual void start() & noexcept = 0; - }; - - // the operation state resulting from connecting a sender being erased by a function<...> - // 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 - { - 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; - - constexpr ~_derived_op() final = default; - - constexpr void start() & noexcept final - { - ::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>; - - typename traits::allocator_type alloc = std::move(p->alloc_); - traits::destroy(alloc, p); - traits::deallocate(alloc, p, 1); - } - - private: - connect_result_t op_; - STDEXEC_ATTRIBUTE(no_unique_address) - Allocator alloc_; - }; - template class _func_op; @@ -162,11 +110,8 @@ 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 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 - // a user-provided frame allocator - std::unique_ptr<_base_op> op_; + Receiver rcvr_; + __any::__any<_any::_iopstate> op_; using _receiver_t = ::exec::_any::_any_receiver_ref, queries>; @@ -186,7 +131,7 @@ namespace experimental::execution constexpr void start() & noexcept { - op_->start(); + op_.start(); } }; @@ -231,7 +176,7 @@ namespace experimental::execution // 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_)(_receiver_t, Args &&...); + __any::__any<_any::_iopstate> (*factory_)(_receiver_t, Args &&...); STDEXEC_ATTRIBUTE(no_unique_address) std::tuple args_; @@ -251,42 +196,21 @@ namespace experimental::execution { using sender_t = std::invoke_result_t; - factory_ = [](_receiver_t rcvr, Args &&...args) -> std::unique_ptr<_base_op> + factory_ = [](_receiver_t rcvr, Args &&...args) -> __any::__any<_any::_iopstate> { - // 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; - - // 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); }}; - // 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, - op, - factory(std::forward(args)...), - std::move(rcvr), - alloc); - - guard.__dismiss(); + auto alloc = choose_frame_allocator(get_env(rcvr)); - return std::unique_ptr<_base_op>(op); + return __any::__any<_any::_iopstate>(__in_place_from, + std::allocator_arg, + alloc, + STDEXEC::connect, + factory(std::forward(args)...), + std::move(rcvr)); }; } From c54bcee820e3fa59212f8c0e84532ad2fabe5d7a Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 09:32:36 -0700 Subject: [PATCH 22/31] Use _any_opstate_base and _state in _func_op This change moves `_func::_func_op` to store its receiver as an `_any::_state`, and its child op as an `_any::_any_opstate_base`, similar to how `_any::_any_opstate` works. This means there's now support for adapting stop tokens, and it slightly shortens some declarations because `_any_opstate_base` is shorter than `__any::__any<_any::_iopstate>`. --- include/exec/function.hpp | 49 +++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 0192fb6d9..4f9bcc1ed 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -109,18 +109,19 @@ 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 - STDEXEC_ATTRIBUTE(no_unique_address) - Receiver rcvr_; - __any::__any<_any::_iopstate> op_; - using _receiver_t = ::exec::_any::_any_receiver_ref, queries>; + using _stop_token_t = stop_token_of_t>; + + _any::_state rcvr_; + _any::_any_opstate_base op_; + public: using operation_state_concept = operation_state_tag; template - constexpr _func_op(Receiver rcvr, Factory factory) + explicit constexpr _func_op(Receiver rcvr, Factory factory) : rcvr_(std::move(rcvr)) , op_(factory(_receiver_t(rcvr_))) {} @@ -174,9 +175,12 @@ namespace experimental::execution using _receiver_t = ::exec::_any::_any_receiver_ref, queries>; + template + using _func_op_t = _func_op, Queries...>; + // the type-erased sender factory that, when called, constructs the erased sender from // args_ and connects the resulting sender to the provided receiver - __any::__any<_any::_iopstate> (*factory_)(_receiver_t, Args &&...); + _any::_any_opstate_base (*factory_)(_receiver_t, Args &&...); STDEXEC_ATTRIBUTE(no_unique_address) std::tuple args_; @@ -196,7 +200,7 @@ namespace experimental::execution { using sender_t = std::invoke_result_t; - factory_ = [](_receiver_t rcvr, Args &&...args) -> __any::__any<_any::_iopstate> + factory_ = [](_receiver_t rcvr, Args &&...args) -> _any::_any_opstate_base { // TODO: as mentioned above, Factory must be a stateless lambda, which makes it // default-constructible like this; this obviously doesn't work if Factory @@ -205,12 +209,12 @@ namespace experimental::execution auto alloc = choose_frame_allocator(get_env(rcvr)); - return __any::__any<_any::_iopstate>(__in_place_from, - std::allocator_arg, - alloc, - STDEXEC::connect, - factory(std::forward(args)...), - std::move(rcvr)); + return _any::_any_opstate_base(__in_place_from, + std::allocator_arg, + alloc, + STDEXEC::connect, + factory(std::forward(args)...), + std::move(rcvr)); }; } @@ -234,17 +238,16 @@ namespace experimental::execution // TODO: this assumes rvalue connection; lvalue connection requires thought and tests template - constexpr _func_op, Queries...> - connect(Receiver rcvr) + constexpr _func_op_t connect(Receiver rcvr) { - return {std::move(rcvr), - [this](auto rcvr) - { - return std::apply( - [&rcvr, this](Args &&...args) - { return factory_(std::move(rcvr), std::forward(args)...); }, - std::move(args_)); - }}; + return _func_op_t{ + std::move(rcvr), + [this](auto rcvr) + { + return std::apply([&rcvr, this](Args &&...args) + { return factory_(std::move(rcvr), std::forward(args)...); }, + std::move(args_)); + }}; } }; From 24d0a3f4e3451f259dbc2b118eaeb6a34216c03d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 15:14:18 -0700 Subject: [PATCH 23/31] Simplify _sigs_from_t Take @ericniebler's suggestion and simplify the `_sigs_from_t` alias template. --- include/exec/function.hpp | 70 ++++++++++----------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 4f9bcc1ed..24b480014 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -251,44 +251,15 @@ 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; - - template - struct _sigs_from - { - using type = STDEXEC::completion_signatures; - }; - - template - struct _sigs_from - { - using type = STDEXEC::completion_signatures; - }; - - 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; + // Given a return type and a bool indicating whether the functino is noexcept, + // 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 + using _sigs_from_t = STDEXEC::__concat_completion_signatures_t< + STDEXEC::completion_signatures, + STDEXEC::set_stopped_t()>, + STDEXEC::__eptr_completion_unless_t>>; } // namespace _func // the user-facing interface to exec::function that supports several different declaration @@ -323,12 +294,10 @@ namespace experimental::execution // the same question applies to all the specializations below that take explicit // completion signatures struct function - : _func::_func_impl, - queries<>> + : _func::_func_impl, queries<>> { using base = _func::_func_impl, + _func::_sigs_from_t, queries<>>; using base::base; @@ -336,13 +305,10 @@ namespace experimental::execution template struct function - : _func::_func_impl, - queries<>> + : _func::_func_impl, queries<>> { - using base = _func::_func_impl, - queries<>>; + using base = + _func::_func_impl, queries<>>; using base::base; }; @@ -360,11 +326,11 @@ namespace experimental::execution template struct function> : _func::_func_impl, + _func::_sigs_from_t, queries> { using base = _func::_func_impl, + _func::_sigs_from_t, queries>; using base::base; @@ -373,11 +339,11 @@ namespace experimental::execution template struct function> : _func::_func_impl, + _func::_sigs_from_t, queries> { using base = _func::_func_impl, + _func::_sigs_from_t, queries>; using base::base; From 1e9771aed8d20e85e0524b50ae55c65f7c56a6c5 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 15:22:01 -0700 Subject: [PATCH 24/31] CR feedback and lvalue connectability Clean up the implementation of `connect`: * switch from `std::tuple` to `STDEXEC::__tuple` * rvalue ref-qualify the existing `connect` * add a const lvalue ref-qualified `connect` that copies the source sender and rvalue connects the temporary * add a test of lvalue connect --- include/exec/function.hpp | 20 +++++++++++--------- test/exec/test_function.cpp | 11 +++++++++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 24b480014..d61f56aff 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/__detail/__tuple.hpp" // TODO: split this header into pieces #include "any_sender_of.hpp" @@ -29,7 +30,6 @@ #include #include #include -#include #include #include @@ -182,7 +182,7 @@ namespace experimental::execution // args_ and connects the resulting sender to the provided receiver _any::_any_opstate_base (*factory_)(_receiver_t, Args &&...); STDEXEC_ATTRIBUTE(no_unique_address) - std::tuple args_; + STDEXEC::__tuple args_; public: using sender_concept = SndrCncpt; @@ -236,18 +236,20 @@ namespace experimental::execution //} } - // TODO: this assumes rvalue connection; lvalue connection requires thought and tests template - constexpr _func_op_t connect(Receiver rcvr) + constexpr _func_op_t connect(Receiver rcvr) && { return _func_op_t{ std::move(rcvr), [this](auto rcvr) - { - return std::apply([&rcvr, this](Args &&...args) - { return factory_(std::move(rcvr), std::forward(args)...); }, - std::move(args_)); - }}; + { return STDEXEC::__apply(factory_, std::move(args_), std::move(rcvr)); }}; + } + + template + requires STDEXEC::__std::copy_constructible<_func_impl> + constexpr _func_op_t connect(Receiver rcvr) const & + { + return auto(*this).connect(std::move(rcvr)); } }; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 3fedbd7fe..684db1a8e 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -107,8 +107,6 @@ 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 Queries = exec::queries( exec::get_frame_allocator_t) noexcept>; @@ -131,4 +129,13 @@ namespace REQUIRE(ret); } + + TEST_CASE("exec::function is conditionally lvalue connectable", "[types][function]") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + + auto [ret] = ex::sync_wait(sndr).value(); + + REQUIRE(ret == 42); + } } // namespace From 97e3ed797da770083c067963ec65229c30871cad Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 17:15:20 -0700 Subject: [PATCH 25/31] Tidy up test cases Add some descriptive `SECTION("blah")` declarations to the basic tests. --- test/exec/test_function.cpp | 99 ++++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 684db1a8e..feb385fe9 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -29,43 +29,72 @@ namespace { TEST_CASE("exec::function is constructible", "[types][function]") { - exec::function voidSndr([]() noexcept { return ex::just(); }); - - exec::function intSndr([]() noexcept { return ex::just(42); }); - - double d = 4.; - exec::function binarySndr(5, - d, - [](int, double&) noexcept { return ex::just(); }); - - 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(); }); - - exec::function> trivialCustomEnv([]() noexcept { return ex::just(); }); - - exec::function, - exec::queries<>> - totalControl(5, [](int) noexcept { return ex::just(); }); - - 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); - STATIC_REQUIRE(STDEXEC::sender); - STATIC_REQUIRE(STDEXEC::sender); + SECTION("void()") + { + exec::function sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("int()") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void(int, double&)") + { + double d = 4.; + exec::function sndr(5, + d, + [](int, double&) noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void() noexcept") + { + exec::function sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("int() noexcept") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag() with only set_value_t(int)") + { + exec::function> sndr( + []() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag() with only set_stopped_t()") + { + exec::function> sndr( + []() noexcept { return ex::just_stopped(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void() with trivial custom environment") + { + exec::function> sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag(int) with only set_value_t() and trivial environment") + { + exec::function, + exec::queries<>> + sndr(5, [](int) noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } } TEST_CASE("exec::function is connectable", "[types][function]") { + SECTION("int() noexcept from just(42)") { exec::function sndr([]() noexcept { return ex::just(42); }); @@ -74,12 +103,14 @@ namespace REQUIRE(fortytwo == 42); } + SECTION("void() from throwing factory") { exec::function sndr([]() -> decltype(ex::just()) { throw "oops"; }); REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); } + SECTION("void() from throwing then") { exec::function sndr([]() noexcept { return ex::just() | ex::then([] { throw "oops"; }); }); @@ -87,6 +118,7 @@ namespace REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); } + SECTION("void() from just_stopped()") { exec::function sndr([]() noexcept { return ex::just_stopped(); }); @@ -95,6 +127,7 @@ namespace REQUIRE_FALSE(ret.has_value()); } + SECTION("custom completions from just_error(42)") { exec::function> From e764b1d508adce70782ba165278294bf9f12e549 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 17:20:00 -0700 Subject: [PATCH 26/31] Do not deduce factories as references Add a test proving that @ericniebler's suggestion to deduce `function`'s factory argument as a value is necessary to accept lvalue factories, and then take the suggestion to make the test pass. --- include/exec/function.hpp | 2 +- test/exec/test_function.cpp | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d61f56aff..a59464b7f 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -194,7 +194,7 @@ namespace experimental::execution && STDEXEC::__std::constructible_from // && STDEXEC::__callable && STDEXEC::sender_to, _receiver_t> - constexpr explicit _func_impl(Args &&...args, Factory &&factory) + constexpr explicit _func_impl(Args &&...args, Factory factory) noexcept(STDEXEC::__nothrow_move_constructible) : args_(std::forward(args)...) { diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index feb385fe9..07e117cc6 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -171,4 +171,13 @@ namespace REQUIRE(ret == 42); } + + TEST_CASE("exec::function accepts lvalue callables", "[types][function]") + { + exec::function sndr(42, ex::just); + + auto [ret] = ex::sync_wait(sndr).value(); + + REQUIRE(ret == 42); + } } // namespace From 3f1166fd13f55b68965112418d25f3037ceecd38 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:24:28 -0700 Subject: [PATCH 27/31] Fix build Replace `auto(*this)` with `_func_impl(*this)`. --- include/exec/function.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index a59464b7f..31ea681a2 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -249,7 +249,7 @@ namespace experimental::execution requires STDEXEC::__std::copy_constructible<_func_impl> constexpr _func_op_t connect(Receiver rcvr) const & { - return auto(*this).connect(std::move(rcvr)); + return _func_impl(*this).connect(std::move(rcvr)); } }; From 09047d1400b492377baea553a6b5bbd5f3e985fd Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:25:08 -0700 Subject: [PATCH 28/31] Add signature validation to exec::queries This ports a constraint I put on my implementation of `exec::queries<...>` to the existing one; it requires that a type passed to `exec::queries` be a possibly-`noexcept` callable that can be invoked on an archetypal environmnet type with a member `query`. --- include/exec/any_sender_of.hpp | 27 +++++++++++++++++++++++++++ include/exec/function.hpp | 14 -------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/include/exec/any_sender_of.hpp b/include/exec/any_sender_of.hpp index 93dbe7805..8e538d7b6 100644 --- a/include/exec/any_sender_of.hpp +++ b/include/exec/any_sender_of.hpp @@ -16,6 +16,7 @@ #pragma once #include "../stdexec/__detail/__any.hpp" +#include "../stdexec/__detail/__concepts.hpp" #include "../stdexec/__detail/__receiver_ref.hpp" #include "../stdexec/__detail/__receivers.hpp" @@ -28,7 +29,33 @@ STDEXEC_PRAGMA_IGNORE_GNU("-Woverloaded-virtual") namespace experimental::execution { + namespace _qry_detail + { + template + struct _env_archetype; + + template + struct _env_archetype + { + Return query(Query, Args &&...) const noexcept(Nothrow); + }; + + using namespace STDEXEC; + + template + inline constexpr bool is_query_function_v = false; + + template + inline constexpr bool is_query_function_v = + __callable const &, Args...>; + + template + inline constexpr bool is_query_function_v = + __nothrow_callable const &, Args...>; + } // namespace _qry_detail + template + requires(_qry_detail::is_query_function_v && ...) struct queries; template > diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 31ea681a2..ec761a460 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -79,20 +79,6 @@ namespace experimental::execution inline constexpr get_frame_allocator_t get_frame_allocator{}; -#if 0 - namespace _qry_detail - { - 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; - } // namespace _qry_detail -#endif - namespace _func { using namespace STDEXEC; From cfdb2e247fa490d097c8aac5b2c4d21338a16b5c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:46:09 -0700 Subject: [PATCH 29/31] Clean up includes * Delete unused includes * Replace `std::invoke_result_t` with `STDEXEC::__invoke_result_t` and update the includes * Replace `std::move` and `std::forward` with the appropriate `static_cast` --- include/exec/function.hpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index ec761a460..d98b5b069 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -17,21 +17,17 @@ #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" #include "../stdexec/__detail/__tuple.hpp" +#include "../stdexec/functional.hpp" // TODO: split this header into pieces #include "any_sender_of.hpp" -#include +#include #include -#include -#include -#include // This file defines function, which is a // type-erased sender that can complete with @@ -108,7 +104,7 @@ namespace experimental::execution template explicit constexpr _func_op(Receiver rcvr, Factory factory) - : rcvr_(std::move(rcvr)) + : rcvr_(static_cast(rcvr)) , op_(factory(_receiver_t(rcvr_))) {} @@ -182,9 +178,9 @@ namespace experimental::execution && STDEXEC::sender_to, _receiver_t> constexpr explicit _func_impl(Args &&...args, Factory factory) noexcept(STDEXEC::__nothrow_move_constructible) - : args_(std::forward(args)...) + : args_(static_cast(args)...) { - using sender_t = std::invoke_result_t; + using sender_t = __invoke_result_t; factory_ = [](_receiver_t rcvr, Args &&...args) -> _any::_any_opstate_base { @@ -199,8 +195,8 @@ namespace experimental::execution std::allocator_arg, alloc, STDEXEC::connect, - factory(std::forward(args)...), - std::move(rcvr)); + factory(static_cast(args)...), + static_cast<_receiver_t &&>(rcvr)); }; } @@ -225,17 +221,21 @@ namespace experimental::execution template constexpr _func_op_t connect(Receiver rcvr) && { - return _func_op_t{ - std::move(rcvr), - [this](auto rcvr) - { return STDEXEC::__apply(factory_, std::move(args_), std::move(rcvr)); }}; + return _func_op_t{static_cast(rcvr), + [this](RcvrRef rcvr) + { + return STDEXEC::__apply(factory_, + static_cast<__tuple &&>( + args_), + static_cast(rcvr)); + }}; } template requires STDEXEC::__std::copy_constructible<_func_impl> constexpr _func_op_t connect(Receiver rcvr) const & { - return _func_impl(*this).connect(std::move(rcvr)); + return _func_impl(*this).connect(static_cast(rcvr)); } }; From 0a03532b773fbdb4386af72dfd40fb9f98c7f6c4 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 11:52:35 -0700 Subject: [PATCH 30/31] Accept non-empty callables This diff adds two pointers' worth of storage space to `function` to add support for capturing callables other than empty lambdas, such as pointers to functions and pointers to member functions. As a nice side effect, trivially-copyable, non-empty lambdas are now also supported, which means member functions can return instances of `function` that contain a lambda that captures `this`, like so: ```c++ struct impl { function get_int() const { return function([this] { return just(i_); }); } int i_; ; ``` --- include/exec/function.hpp | 40 ++++++++----- test/exec/test_function.cpp | 110 ++++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 19 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index d98b5b069..c1186c562 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -21,12 +21,14 @@ #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" #include "../stdexec/__detail/__tuple.hpp" +#include "../stdexec/__detail/__utility.hpp" #include "../stdexec/functional.hpp" // TODO: split this header into pieces #include "any_sender_of.hpp" #include +#include #include // This file defines function, which is a @@ -160,21 +162,29 @@ namespace experimental::execution template using _func_op_t = _func_op, Queries...>; - // the type-erased sender factory that, when called, constructs the erased sender from - // args_ and connects the resulting sender to the provided receiver - _any::_any_opstate_base (*factory_)(_receiver_t, Args &&...); + // The type-erased operation state factory; it points to a function that knows the concrete + // type of the sender factory stored in make_sender_ so that it can construct the desired + // sender on demand and connect it to the given receiver. The expected arguments are the + // address of make_sender_, the _any_receiver_ref to connect the sender to, and the arguments + // to pass to make_sender_ to construct the sender. + _any::_any_opstate_base (*make_op_)(void *, _receiver_t, Args &&...); + // Storage for the sender factory passed to our constructor template; make_op_ will + // reconstitute the actual factory from this bag-of-bytes with start_lifetime_as + // because it internally knows the concrete type of the user-provided sender factory. + // We're reserving 2 * sizeof(void *) bytes to permit the factory to be a pointer to + // member function, which usually requires two pointers. + std::byte make_sender_[2 * sizeof(void *)]; + // The curried arguments that will be passed to make_sender_ from inside make_op_. STDEXEC_ATTRIBUTE(no_unique_address) STDEXEC::__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> + template Factory> requires STDEXEC::__not_decays_to // - && STDEXEC::__std::constructible_from // - && STDEXEC::__callable + && (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) // + && (sizeof(Factory) <= sizeof(make_sender_)) // && STDEXEC::sender_to, _receiver_t> constexpr explicit _func_impl(Args &&...args, Factory factory) noexcept(STDEXEC::__nothrow_move_constructible) @@ -182,12 +192,11 @@ namespace experimental::execution { using sender_t = __invoke_result_t; - factory_ = [](_receiver_t rcvr, Args &&...args) -> _any::_any_opstate_base + std::memcpy(make_sender_, std::addressof(factory), sizeof(Factory)); + + make_op_ = [](void *storage, _receiver_t rcvr, Args &&...args) -> _any::_any_opstate_base { - // 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; + auto &make_sender = *__std::start_lifetime_as(storage); auto alloc = choose_frame_allocator(get_env(rcvr)); @@ -195,7 +204,7 @@ namespace experimental::execution std::allocator_arg, alloc, STDEXEC::connect, - factory(static_cast(args)...), + std::invoke(make_sender, static_cast(args)...), static_cast<_receiver_t &&>(rcvr)); }; } @@ -224,9 +233,10 @@ namespace experimental::execution return _func_op_t{static_cast(rcvr), [this](RcvrRef rcvr) { - return STDEXEC::__apply(factory_, + return STDEXEC::__apply(make_op_, static_cast<__tuple &&>( args_), + make_sender_, static_cast(rcvr)); }}; } diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index 07e117cc6..a2b5a5c5e 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -43,10 +43,10 @@ namespace SECTION("void(int, double&)") { - double d = 4.; - exec::function sndr(5, - d, - [](int, double&) noexcept { return ex::just(); }); + double d = 4.; + exec::function sndr(5, + d, + [](int, double &) noexcept { return ex::just(); }); STATIC_REQUIRE(STDEXEC::sender); } @@ -180,4 +180,106 @@ namespace REQUIRE(ret == 42); } + + TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + { + struct iface + { + virtual exec::function get_i_virtually() const noexcept = 0; + }; + + struct iface2 + { + exec::function get_i_from_base() const noexcept + { + return exec::function(this, &iface2::get_i_virtually); + } + + virtual exec::function get_i_virtually() const noexcept = 0; + }; + + struct impl + : iface + , iface2 + { + explicit impl(int i) noexcept + : i_(i) + {} + + auto just_i() const noexcept + { + return ex::just(i_); + } + + static auto static_just_i(impl const *self) noexcept + { + return self->just_i(); + } + + exec::function get_i_with_capture() const noexcept + { + return exec::function([this]() noexcept { return just_i(); }); + } + + exec::function get_i_with_pmfn() const noexcept + { + return exec::function(this, &impl::just_i); + } + + exec::function get_i_virtually() const noexcept override + { + return get_i_with_capture(); + } + + private: + int i_; + }; + + SECTION("function accepts a lambda capturing this") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_with_capture()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-member function") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_with_pmfn()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-function") + { + impl imp{42}; + auto [ret] = ex::sync_wait( + exec::function(&imp, &impl::static_just_i)) + .value(); + + REQUIRE(ret == 42); + } + + SECTION("function can be the return type of a virtual member function") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_virtually()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-member function") + { + impl imp{42}; + auto [ret] = + ex::sync_wait(exec::function(&imp, &iface::get_i_virtually)).value(); + + REQUIRE(ret == 42); + } + + SECTION("function works on the base class") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_from_base()).value(); + + REQUIRE(ret == 42); + } + } } // namespace From 051b215210a89aec6455645c0ae99137acb2ee01 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 12:23:16 -0700 Subject: [PATCH 31/31] Fix up _func_impl::get_completion_signatures This diff steals the `get_completion_signatures` implementation from `any_sender_of`; the rules for the two type-erasing containers are the same. It'd be nice to share an impl somehow, but this is good enough for now. --- include/exec/function.hpp | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index c1186c562..3f867b992 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -91,13 +91,13 @@ namespace experimental::execution template class _func_op, Queries...> { - // 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 using _receiver_t = ::exec::_any::_any_receiver_ref, queries>; using _stop_token_t = stop_token_of_t>; + // rcvr_ has to be initialized before op_ because our implementation of get_env + // is empirically accessed during our constructor and depends on rcvr_ being initialized _any::_state rcvr_; _any::_any_opstate_base op_; @@ -209,22 +209,23 @@ namespace experimental::execution }; } - template - static consteval auto get_completion_signatures() noexcept + // this implementation of get_completion_signatures is taken directly from + // the equivalent function on any_sender_of + template + static consteval auto get_completion_signatures() { - static_assert(STDEXEC_IS_BASE_OF(_func_impl, __decay_t)); + static_assert(__std::derived_from, _func_impl>); - // TODO: validate that Env supports all the required queries - // - //if constexpr (std::constructible_from) + // throw if Env does not contain the queries needed to type-erase the receiver: + using _check_queries_t = __mfind_error<_any::_check_query_t...>; + if constexpr (__merror<_check_queries_t>) + { + return STDEXEC::__throw_compile_time_error(_check_queries_t{}); + } + else { return completion_signatures{}; } - //else - //{ - // TODO: make this error accurate - //return __throw_compile_time_error(__unrecognized_sender_error_t()); - //} } template @@ -249,7 +250,7 @@ namespace experimental::execution } }; - // Given a return type and a bool indicating whether the functino is noexcept, + // Given a return type and a bool indicating whether the function is noexcept, // 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)