From c0a3b233371911ed3aec50501ad74d11d1e72d59 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 16 Apr 2026 20:36:56 -0700 Subject: [PATCH 01/45] 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 3e8a7b9d113a78345c4125a0987a7e0a202837a7 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 16:46:52 -0700 Subject: [PATCH 02/45] 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 0e833fbd7160f0617653d6f9a563e550f11329c1 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 17 Apr 2026 20:06:48 -0700 Subject: [PATCH 03/45] 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 575880f8b7a6199f9b4d26b38f72351dd61ab45d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 09:31:23 -0700 Subject: [PATCH 04/45] 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 6ee3e53a4eed6b028d90be11dccbe669d5c64efe Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sat, 18 Apr 2026 20:44:18 -0700 Subject: [PATCH 05/45] 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 6a4856d2147a05eaa67d426d4641683ce8b8d3fe Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:03:14 -0700 Subject: [PATCH 06/45] 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 491f8e6c80a3285722266a775dc6b581af1e5269 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:19:20 -0700 Subject: [PATCH 07/45] 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 7c001d0711c1bf5429ecb7b6de3e7ad942b25b42 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 08:23:03 -0700 Subject: [PATCH 08/45] 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 517fc670751a46061b9242985882bd58a94809eb Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 12:50:23 -0700 Subject: [PATCH 09/45] 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 10dddfe377f9d69ac278e793e691f34666c0c07d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 14:02:31 -0700 Subject: [PATCH 10/45] 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 2da9ddd8cc43d994fa71d1d67f59e46b0df9a338 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 17:26:17 -0700 Subject: [PATCH 11/45] 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 a0cf35831934e4ee496ce480794e5effa7cd0c89 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 19 Apr 2026 18:35:26 -0700 Subject: [PATCH 12/45] 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 c1022e78fc95e15dfa8d907690251a4843d0e501 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 20 Apr 2026 07:40:51 -0700 Subject: [PATCH 13/45] 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 2f41320289584021c8e5deee92122c4e7e99d00f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 15:33:55 -0700 Subject: [PATCH 14/45] 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 cd8a6b47105ce1382f40f24649dd6af8f396fda4 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 21 Apr 2026 16:40:22 -0700 Subject: [PATCH 15/45] 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 86ea1b7cc59d864130d99b0fdddcf14b6320d514 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:25:36 -0700 Subject: [PATCH 16/45] 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 54cb4eb241483474022ec20a84326928e108232a Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 10:28:22 -0700 Subject: [PATCH 17/45] 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 7b07e79c716528a07731f178afadb174170483e1 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 20:32:12 -0700 Subject: [PATCH 18/45] 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 7d451a702602b40fb0374fe7a46a661e9501ea91 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:26:19 -0700 Subject: [PATCH 19/45] 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 39b457f6d7dadcdf1025c995d61831ff87c1949e Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Tue, 28 Apr 2026 21:31:21 -0700 Subject: [PATCH 20/45] 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 dc82c730a5a2c2c0738bcb53c2935586d9330887 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 07:52:09 -0700 Subject: [PATCH 21/45] 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 b1a341a762a5c609c2677d31387adebc80fb397f Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 09:32:36 -0700 Subject: [PATCH 22/45] 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 54d5e9a4c9c3fff82659fd9f9fad6c7d255ba7ac Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 15:14:18 -0700 Subject: [PATCH 23/45] 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 986c29b6a2da1b5c2c300524c603a9ef82b57833 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 15:22:01 -0700 Subject: [PATCH 24/45] 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 769055843362deb1ae512f716051db78ac4b2bd4 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 17:15:20 -0700 Subject: [PATCH 25/45] 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 f3255b4eb0a3828f3de08f224e2a0593825f7290 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Wed, 29 Apr 2026 17:20:00 -0700 Subject: [PATCH 26/45] 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 7dc7d5264e3cb576209b44811dbbebdf56d25923 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:24:28 -0700 Subject: [PATCH 27/45] 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 3ea9d189b3e347f9a97ca0a9a736305481540bcd Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:25:08 -0700 Subject: [PATCH 28/45] 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 6418bea28dbf658234bec232fbdb4fd119e387ba Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 10:46:09 -0700 Subject: [PATCH 29/45] 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 8cd2ee6234224edfaac2d889a41e3a4dc3df2b2d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 11:52:35 -0700 Subject: [PATCH 30/45] 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 25de5f1b79885921d91a90525ec905b1e00750eb Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 12:23:16 -0700 Subject: [PATCH 31/45] 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) From 53f371053a384ef285a71f6f35bcc17544a3ef85 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 14:26:14 -0700 Subject: [PATCH 32/45] Ensure completion_signature order is irrelevant This diff changes the implementation of `function<...>` to ensure that every specialization of the template always sorts-and-uniques the signatures in the `completion_signatures` specialization given to the `_func_impl` base class. This way we both minimize the number of base class template instantiations, and make it easier to make two function types that happened to specify their completion signaturs in a different order are "the same" (mutually assignable, comparable, and constructible). --- include/exec/function.hpp | 235 +++++++++++++++++++++++- include/stdexec/__detail/__concepts.hpp | 9 + include/stdexec/__detail/__config.hpp | 6 + test/exec/test_function.cpp | 109 +++++++++++ 4 files changed, 350 insertions(+), 9 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 3f867b992..bb6efae01 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -140,6 +140,20 @@ namespace experimental::execution } } + template + bool _equal(std::index_sequence, __tuple const &lhs, __tuple const &rhs) + noexcept(noexcept(((__get(lhs) == __get(rhs)) && ...))) + { + return ((__get(lhs) == __get(rhs)) && ...); + } + + template + bool _equal(__tuple const &lhs, __tuple const &rhs) + noexcept(noexcept(_equal(std::index_sequence_for{}, lhs, rhs))) + { + return _equal(std::index_sequence_for{}, lhs, rhs); + } + template class _func_impl; @@ -173,11 +187,20 @@ namespace experimental::execution // because it internally knows the concrete type of the user-provided sender factory. // We're reserving 2 * sizeof(void *) bytes to permit the factory to be a pointer to // member function, which usually requires two pointers. - std::byte make_sender_[2 * sizeof(void *)]; + std::byte make_sender_[2 * sizeof(void *)]{}; // The curried arguments that will be passed to make_sender_ from inside make_op_. STDEXEC_ATTRIBUTE(no_unique_address) STDEXEC::__tuple args_; + // equal args and equal pointers-to-factories are equal + friend constexpr bool operator==(_func_impl const &lhs, _func_impl const &rhs) + noexcept(noexcept(_equal(lhs.args_, rhs.args_))) + { + return lhs.make_op_ == rhs.make_op_ + && std::ranges::equal(lhs.make_sender_, rhs.make_sender_) + && _equal(lhs.args_, rhs.args_); + } + public: using sender_concept = SndrCncpt; @@ -209,6 +232,19 @@ namespace experimental::execution }; } + template <__std::derived_from<_func_impl> Func> + requires __not_decays_to + constexpr _func_impl(Func &&other) noexcept(__nothrow_move_constructible<__tuple>) + : _func_impl(static_cast<_func_impl &&>(other)) + {} + + template <__std::derived_from<_func_impl> Func> + requires __not_decays_to && __std::copy_constructible<__tuple> + constexpr _func_impl(Func const &other) + noexcept(__nothrow_copy_constructible<__tuple>) + : _func_impl(static_cast<_func_impl const &>(other)) + {} + // this implementation of get_completion_signatures is taken directly from // the equivalent function on any_sender_of template @@ -250,15 +286,38 @@ namespace experimental::execution } }; + template + struct _canonical_t; + + template + struct _canonical_t> + { + consteval auto operator()() const noexcept + { + constexpr auto make_sigs = []() noexcept + { + return __cmplsigs::__to_array(completion_signatures{}); + }; + + return __cmplsigs::__completion_sigs_from(make_sigs); + } // namespace _func + }; // namespace experimental::execution + + template + inline constexpr _canonical_t _canonical{}; + + template + using _canonical_sigs_t = decltype(_canonical()); + // 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) template - using _sigs_from_t = STDEXEC::__concat_completion_signatures_t< + using _sigs_from_t = _canonical_sigs_t, STDEXEC::set_stopped_t()>, - STDEXEC::__eptr_completion_unless_t>>; + STDEXEC::__eptr_completion_unless_t>>>; } // namespace _func // the user-facing interface to exec::function that supports several different declaration @@ -300,6 +359,27 @@ namespace experimental::execution queries<>>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; template @@ -310,16 +390,59 @@ namespace experimental::execution _func::_func_impl, queries<>>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; template requires STDEXEC::__is_instance_of struct function - : _func::_func_impl> + : _func::_func_impl, queries<>> { - using base = _func::_func_impl>; + using base = + _func::_func_impl, queries<>>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; template @@ -333,6 +456,27 @@ namespace experimental::execution queries>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; template @@ -346,6 +490,27 @@ namespace experimental::execution queries>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; template @@ -353,15 +518,67 @@ namespace experimental::execution STDEXEC::completion_signatures, queries> : _func::_func_impl, + _func::_canonical_sigs_t>, queries> { - using base = _func::_func_impl, - queries>; + using base = + _func::_func_impl>, + queries>; using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } }; } // namespace experimental::execution namespace exec = experimental::execution; + +namespace std +{ + template class TQual, template class UQual> + struct basic_common_reference, + typename exec::function::base, + TQual, + UQual> + { + private: + using base = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; + + template class TQual, + template class UQual> + struct basic_common_reference, exec::function, TQual, UQual> + { + private: + using tbase = exec::function::base; + using ubase = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; +} // namespace std diff --git a/include/stdexec/__detail/__concepts.hpp b/include/stdexec/__detail/__concepts.hpp index af43ca566..2578830fd 100644 --- a/include/stdexec/__detail/__concepts.hpp +++ b/include/stdexec/__detail/__concepts.hpp @@ -300,12 +300,21 @@ namespace STDEXEC template concept __nothrow_copy_constructible = (__nothrow_constructible_from<_Ts, _Ts const &> && ...); + template + concept __assignable_from = STDEXEC_IS_ASSIGNABLE(_Ty, _A); + template concept __nothrow_assignable_from = STDEXEC_IS_NOTHROW_ASSIGNABLE(_Ty, _A); template concept __nothrow_move_assignable = (__nothrow_assignable_from<_Ts, _Ts> && ...); + template + concept __copy_assignable = (__assignable_from<_Ts, _Ts const &> && ...); + + template + concept __nothrow_copy_assignable = (__nothrow_assignable_from<_Ts, _Ts const &> && ...); + template concept __decay_copyable = (__std::constructible_from<__decay_t<_Ts>, _Ts> && ...); diff --git a/include/stdexec/__detail/__config.hpp b/include/stdexec/__detail/__config.hpp index 908d12655..2e20a26ba 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -520,6 +520,12 @@ namespace STDEXEC::__std # define STDEXEC_IS_NOTHROW_ASSIGNABLE(...) std::is_nothrow_assignable_v<__VA_ARGS__> #endif +#if STDEXEC_HAS_BUILTIN(__is_assignable) || STDEXEC_MSVC() +# define STDEXEC_IS_ASSIGNABLE(...) __is_assignable(__VA_ARGS__) +#else +# define STDEXEC_IS_ASSIGNABLE(...) std::is_assignable_v<__VA_ARGS__> +#endif + #if STDEXEC_HAS_BUILTIN(__is_empty) || STDEXEC_MSVC() # define STDEXEC_IS_EMPTY(...) __is_empty(__VA_ARGS__) #else diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index a2b5a5c5e..b3e88bfc0 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -282,4 +282,113 @@ namespace REQUIRE(ret == 42); } } + + TEST_CASE("completion_signature specification is order-independent", "[types][function]") + { + // by specifying the completions with a function signature, it's up to the library what + // order the completion signatures are specified in + using func1_t = exec::function; + // this declaration chooses value before stopped + using func2_t = + exec::function>; + // this declaration chooses stopped before value + using func3_t = + exec::function>; + + SECTION("the function types are not the same as each other...") + { + STATIC_REQUIRE(!std::same_as); + STATIC_REQUIRE(!std::same_as); + STATIC_REQUIRE(!std::same_as); + } + + SECTION("...but they all inherit from the same _func_impl base") + { + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + } + + SECTION("move-construction works in every direction between all three types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("copy-construction works in every direction between all three types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("move-assignment works in every direction between all three types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("copy-assignment works in every direction between all three types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("instances are mutually comparable with ==") + { + // identical copies in slightly different types + func1_t f1(42, ex::just); + func2_t f2(f1); + func3_t f3(f1); + + // differing curried arguments from above + func2_t f4(45, ex::just); + func3_t f5(45, ex::just); + + REQUIRE(f1 == f1); + REQUIRE(f1 == f2); + REQUIRE(f1 == f3); + REQUIRE(f2 == f1); + REQUIRE(f2 == f2); + REQUIRE(f2 == f3); + REQUIRE(f3 == f1); + REQUIRE(f3 == f2); + REQUIRE(f3 == f3); + + REQUIRE(f1 != f4); + REQUIRE(f2 != f4); + REQUIRE(f3 != f4); + + REQUIRE(f4 == f5); + } + } } // namespace From 1f5ea623e0c1e367a1841155b815091e571d4398 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 30 Apr 2026 14:32:05 -0700 Subject: [PATCH 33/45] Tweak comments --- include/exec/function.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index bb6efae01..07629cfa4 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -300,8 +300,8 @@ namespace experimental::execution }; return __cmplsigs::__completion_sigs_from(make_sigs); - } // namespace _func - }; // namespace experimental::execution + } + }; template inline constexpr _canonical_t _canonical{}; From 5339a85e5c17561a2b2782f7cf6a91b01aad0eb9 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 1 May 2026 11:52:59 -0700 Subject: [PATCH 34/45] Fix GCC builds I don't know why GCC needs this change, but using in-place `new` to initialize a member of an anonymous union with the result of a function call rather than directly initializing the same value in the member initialization clause from the same function allows GCC to recognize that initializing the value with a prvalue does not invoke the move constructor. --- include/stdexec/__detail/__any.hpp | 37 +++++++++++++++++++++++-- include/stdexec/__detail/__concepts.hpp | 3 ++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/include/stdexec/__detail/__any.hpp b/include/stdexec/__detail/__any.hpp index 3a1a051ea..0a2d1c0ff 100644 --- a/include/stdexec/__detail/__any.hpp +++ b/include/stdexec/__detail/__any.hpp @@ -468,9 +468,39 @@ namespace STDEXEC::__any template constexpr explicit __box(__in_place_from_t, _Fn &&__fn, _Args &&...__args) noexcept(__nothrow_callable<_Fn, _Args...>) - : __val_(static_cast<_Fn &&>(__fn)(static_cast<_Args &&>(__args)...)) { static_assert(__same_as<__call_result_t<_Fn, _Args...>, _Value>); + new ((void *) std::addressof(__val_)) + _Value(static_cast<_Fn &&>(__fn)(static_cast<_Args &&>(__args)...)); + } + + constexpr __box(__box &&__other) noexcept(__nothrow_move_constructible<_Value>) + requires __std::move_constructible<_Value> + : __val_(static_cast<_Value &&>(__other).__val_) + {} + + constexpr __box(__box const &__other) noexcept(__nothrow_copy_constructible<_Value>) + requires __std::copy_constructible<_Value> + : __val_(__other.__val_) + {} + + constexpr ~__box() + { + __val_.~_Value(); + } + + constexpr __box &operator=(__box &&__rhs) noexcept(__nothrow_move_assignable<_Value>) + requires __move_assignable<_Value> + { + __val_ = static_cast<__box &&>(__rhs).__val_; + return *this; + } + + constexpr __box &operator=(__box const &__rhs) noexcept(__nothrow_copy_assignable<_Value>) + requires __copy_assignable<_Value> + { + __val_ = __rhs.__val_; + return *this; } template @@ -482,7 +512,10 @@ namespace STDEXEC::__any private: STDEXEC_ATTRIBUTE(no_unique_address) - _Value __val_; + union + { + _Value __val_; + }; }; template diff --git a/include/stdexec/__detail/__concepts.hpp b/include/stdexec/__detail/__concepts.hpp index 2578830fd..094a2505c 100644 --- a/include/stdexec/__detail/__concepts.hpp +++ b/include/stdexec/__detail/__concepts.hpp @@ -306,6 +306,9 @@ namespace STDEXEC template concept __nothrow_assignable_from = STDEXEC_IS_NOTHROW_ASSIGNABLE(_Ty, _A); + template + concept __move_assignable = (__assignable_from<_Ts, _Ts> && ...); + template concept __nothrow_move_assignable = (__nothrow_assignable_from<_Ts, _Ts> && ...); From 8ce030c607fe77f2a6935da8d21ef11f71a909fe Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 1 May 2026 11:54:08 -0700 Subject: [PATCH 35/45] Hopefully fix MSVC Looks like MSVC doesn't like pure-virtual member functions on local classes so move the local types in `function`'s tests out to namespace scope. --- test/exec/test_function.cpp | 84 ++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index b3e88bfc0..c94fa03da 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -181,60 +181,60 @@ namespace REQUIRE(ret == 42); } - TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + struct iface { - struct iface - { - virtual exec::function get_i_virtually() const noexcept = 0; - }; + virtual exec::function get_i_virtually() const noexcept = 0; + }; - struct iface2 + struct iface2 + { + exec::function get_i_from_base() const noexcept { - exec::function get_i_from_base() const noexcept - { - return exec::function(this, &iface2::get_i_virtually); - } + return exec::function(this, &iface2::get_i_virtually); + } - virtual exec::function get_i_virtually() const noexcept = 0; - }; + virtual exec::function get_i_virtually() const noexcept = 0; + }; - struct impl - : iface - , iface2 - { - explicit impl(int i) noexcept - : i_(i) - {} + struct impl + : iface + , iface2 + { + explicit impl(int i) noexcept + : i_(i) + {} - auto just_i() const noexcept - { - return ex::just(i_); - } + auto just_i() const noexcept + { + return ex::just(i_); + } - static auto static_just_i(impl const *self) noexcept - { - return self->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_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_with_pmfn() const noexcept + { + return exec::function(this, &impl::just_i); + } - exec::function get_i_virtually() const noexcept override - { - return get_i_with_capture(); - } + exec::function get_i_virtually() const noexcept override + { + return get_i_with_capture(); + } - private: - int i_; - }; + private: + int i_; + }; + TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + { SECTION("function accepts a lambda capturing this") { auto [ret] = ex::sync_wait(impl{42}.get_i_with_capture()).value(); From 4db2566478311902cff292645cebb030da8e2773 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Fri, 1 May 2026 13:44:25 -0700 Subject: [PATCH 36/45] Make query specification order-independent Change the declarations of `function<...>` to inherit from canonical specializations of `_func_impl` so that the queries are always sorted and uniqued. --- include/exec/any_sender_of.hpp | 3 +- include/exec/function.hpp | 77 +++++++++++++++++++++++------- test/exec/test_function.cpp | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 18 deletions(-) diff --git a/include/exec/any_sender_of.hpp b/include/exec/any_sender_of.hpp index 8e538d7b6..3e811320a 100644 --- a/include/exec/any_sender_of.hpp +++ b/include/exec/any_sender_of.hpp @@ -56,7 +56,8 @@ namespace experimental::execution template requires(_qry_detail::is_query_function_v && ...) - struct queries; + struct queries + {}; template > struct any_receiver; diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 07629cfa4..35a22e474 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -17,10 +17,13 @@ #include "../stdexec/__detail/__completion_signatures.hpp" #include "../stdexec/__detail/__concepts.hpp" +#include "../stdexec/__detail/__meta.hpp" #include "../stdexec/__detail/__read_env.hpp" #include "../stdexec/__detail/__receivers.hpp" #include "../stdexec/__detail/__sender_concepts.hpp" +#include "../stdexec/__detail/__static_vector.hpp" #include "../stdexec/__detail/__tuple.hpp" +#include "../stdexec/__detail/__typeinfo.hpp" #include "../stdexec/__detail/__utility.hpp" #include "../stdexec/functional.hpp" @@ -287,10 +290,10 @@ namespace experimental::execution }; template - struct _canonical_t; + struct _canonical_fn; template - struct _canonical_t> + struct _canonical_fn> { consteval auto operator()() const noexcept { @@ -303,18 +306,59 @@ namespace experimental::execution } }; + template + struct _canonical_fn> + { + private: + // sort and unique the function types in Queries... into an array of __mtypeids + static consteval auto get_sigs() noexcept + { + using sig_array_t = __static_vector<__type_index, sizeof...(Queries)>; + auto sigs = sig_array_t{__mtypeid...}; + + std::ranges::sort(sigs); + + auto const end = std::ranges::unique(sigs).begin(); + sigs.erase(end, sigs.end()); + + return sigs; + } + + public: + consteval auto operator()() const noexcept + { + constexpr auto sigs = get_sigs(); + + constexpr auto fn = [=](__indices) + { + return queries<__msplice...>(); + }; + + return fn(__make_indices()); + } + }; + + template <> + struct _canonical_fn> + { + consteval auto operator()() const noexcept + { + return queries<>(); + } + }; + template - inline constexpr _canonical_t _canonical{}; + inline constexpr _canonical_fn _canonical{}; template - using _canonical_sigs_t = decltype(_canonical()); + using _canonical_t = decltype(_canonical()); // 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) template - using _sigs_from_t = _canonical_sigs_t, STDEXEC::set_stopped_t()>, STDEXEC::__eptr_completion_unless_t>>>; @@ -416,10 +460,10 @@ namespace experimental::execution template requires STDEXEC::__is_instance_of struct function - : _func::_func_impl, queries<>> + : _func::_func_impl, queries<>> { using base = - _func::_func_impl, queries<>>; + _func::_func_impl, queries<>>; using base::base; @@ -449,11 +493,11 @@ namespace experimental::execution struct function> : _func::_func_impl, - queries> + _func::_canonical_t>> { using base = _func::_func_impl, - queries>; + _func::_canonical_t>>; using base::base; @@ -483,11 +527,11 @@ namespace experimental::execution struct function> : _func::_func_impl, - queries> + _func::_canonical_t>> { using base = _func::_func_impl, - queries>; + _func::_canonical_t>>; using base::base; @@ -518,13 +562,12 @@ namespace experimental::execution STDEXEC::completion_signatures, queries> : _func::_func_impl>, - queries> + _func::_canonical_t>, + _func::_canonical_t>> { - using base = - _func::_func_impl>, - queries>; + using base = _func::_func_impl>, + _func::_canonical_t>>; using base::base; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index c94fa03da..aa4f78409 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -391,4 +391,91 @@ namespace REQUIRE(f4 == f5); } } + + TEST_CASE("queries specification is order-independent", "[types][function]") + { + constexpr auto query1 = [](auto const &) noexcept + { + return 0; + }; + + constexpr auto query2 = [](auto const &, int i) + { + return (double) i; + }; + + using query1_t = decltype(query1); + using query2_t = decltype(query2); + + using func1_t = + exec::function>; + + using func2_t = + exec::function>; + + SECTION("the function types are not the same as each other...") + { + STATIC_REQUIRE(!std::same_as); + } + + SECTION("...but they both inherit from the same _func_impl base") + { + STATIC_REQUIRE(std::same_as); + } + + SECTION("move construction works in all directions with both types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("copy construction works in all directions with both types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("move-assignment works in every direction with both types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("copy-assignment works in every direction with both types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("instances are mutually comparable with ==") + { + // identical copies in slightly different types + func1_t f1(42, ex::just); + func2_t f2(f1); + + // differing curried arguments from above + func1_t f3(45, ex::just); + func2_t f4(45, ex::just); + + REQUIRE(f1 == f1); + REQUIRE(f1 == f2); + REQUIRE(f2 == f1); + REQUIRE(f2 == f2); + + REQUIRE(f1 != f3); + REQUIRE(f3 != f1); + REQUIRE(f2 != f3); + REQUIRE(f3 != f2); + + REQUIRE(f3 == f4); + } + } } // namespace From 3b22b17cae225e8b7e34b1098d336c739dc13201 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 3 May 2026 09:31:13 -0700 Subject: [PATCH 37/45] Maybe fix MSVC + CUDA builds The MSVC build failure on the previous commit looks like a misplaced `[[no_unique_address]]`; the CUDA build failures are mysterious to me, but they appear to be downstream of changing `__any`'s `__box` type to use in-place new into an uninitialized member of an anonymous union, which I did to address a GCC build failure. This diff makes the anonymous union hack GCC-specific, to hopefull make all the compilers happy at the expense of preprocessor complexity. --- include/stdexec/__detail/__any.hpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/include/stdexec/__detail/__any.hpp b/include/stdexec/__detail/__any.hpp index 0a2d1c0ff..af73c0d2d 100644 --- a/include/stdexec/__detail/__any.hpp +++ b/include/stdexec/__detail/__any.hpp @@ -465,11 +465,25 @@ namespace STDEXEC::__any : __val_(static_cast<_Args &&>(__args)...) {} +#if !STDEXEC_GCC() template constexpr explicit __box(__in_place_from_t, _Fn &&__fn, _Args &&...__args) noexcept(__nothrow_callable<_Fn, _Args...>) + : __val_(static_cast<_Fn &&>(__fn)(static_cast<_Args &&>(__args)...)) { static_assert(__same_as<__call_result_t<_Fn, _Args...>, _Value>); + } +#else + template + constexpr explicit __box(__in_place_from_t, _Fn &&__fn, _Args &&...__args) + noexcept(__nothrow_callable<_Fn, _Args...>) + { + // for unknown reasons, GCC (sometimes) doesn't like initializing __val_ with the + // result of this function call in the member initialization clause when _Value + // is immovable even though the conditions should be right to trigger C++17's + // mandatory copy elision, but this in-place new into an uninitialized anonymous + // union member works just fine + static_assert(__same_as<__call_result_t<_Fn, _Args...>, _Value>); new ((void *) std::addressof(__val_)) _Value(static_cast<_Fn &&>(__fn)(static_cast<_Args &&>(__args)...)); } @@ -502,6 +516,7 @@ namespace STDEXEC::__any __val_ = __rhs.__val_; return *this; } +#endif template [[nodiscard]] @@ -511,11 +526,16 @@ namespace STDEXEC::__any } private: +#if !STDEXEC_GCC() STDEXEC_ATTRIBUTE(no_unique_address) + _Value __val_; +#else union { + STDEXEC_ATTRIBUTE(no_unique_address) _Value __val_; }; +#endif }; template From e2b7b4723dcf7f1a700ded6263cb0b3c5f829224 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 09:49:49 -0700 Subject: [PATCH 38/45] Implement choose_frame_allocator with __first_callable This diff takes @ericniebler's suggestion and reimplements `choose_frame_allocator` in terms of `__first_callable`. --- include/exec/function.hpp | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 35a22e474..e8d88afad 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -126,22 +126,10 @@ 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 - { - 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(); - } - } + inline constexpr auto choose_frame_allocator = + STDEXEC::__first_callable{get_frame_allocator, + get_allocator, + STDEXEC::__always{std::allocator()}}; template bool _equal(std::index_sequence, __tuple const &lhs, __tuple const &rhs) From fbd215ef43ab2ab8f9aae1ef066a352b5b665c63 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 09:57:16 -0700 Subject: [PATCH 39/45] Use STDEXEC::__invoke Switch from `std::invoke` and `std::invocable` to `STDEXEC::__invoke` and `STDEXEC::invocable`. --- include/exec/function.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index e8d88afad..a3ceecc87 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -195,7 +195,7 @@ namespace experimental::execution public: using sender_concept = SndrCncpt; - template Factory> + template Factory> requires STDEXEC::__not_decays_to // && (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) // && (sizeof(Factory) <= sizeof(make_sender_)) // @@ -218,7 +218,8 @@ namespace experimental::execution std::allocator_arg, alloc, STDEXEC::connect, - std::invoke(make_sender, static_cast(args)...), + STDEXEC::__invoke(make_sender, + static_cast(args)...), static_cast<_receiver_t &&>(rcvr)); }; } From 415e8ef1d79c9b0ea75c3897276ba90b7d733596 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 10:20:36 -0700 Subject: [PATCH 40/45] Remove && in file comment Hopefully this clears up the ambiguity in the file-level comment describing `exec::function`'s interface. --- 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 a3ceecc87..eca27da29 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -36,7 +36,7 @@ // This file defines function, which is a // type-erased sender that can complete with -// - set_value(ReturnType&&) +// - set_value(ReturnType) // - set_error(std::exception_ptr) // - set_stopped() // From cf3682e5a489aeab4830189671548052aa8138aa Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 10:33:34 -0700 Subject: [PATCH 41/45] Make the basic_common_reference specializations symmetric I realized that the specializations of `basic_common_reference` were asymmetric; this diff fixes that. --- include/exec/function.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index eca27da29..fc0b276e4 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -600,6 +600,19 @@ namespace std using type = common_reference_t, UQual>; }; + template class TQual, template class UQual> + struct basic_common_reference::base, + exec::function, + TQual, + UQual> + { + private: + using base = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; + template class TQual, From 45d18f01d840a23e7320729b7fc7b85aea84d166 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 11:05:26 -0700 Subject: [PATCH 42/45] Make function's implementation more concise Replace all the duplication in the various specializations of `exec::function` with a CRTP base class that implements `operator=`. --- include/exec/function.hpp | 154 +++++++------------------------------- 1 file changed, 28 insertions(+), 126 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index fc0b276e4..56a950dc1 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -351,6 +351,26 @@ namespace experimental::execution STDEXEC::completion_signatures, STDEXEC::set_stopped_t()>, STDEXEC::__eptr_completion_unless_t>>>; + + template + struct _func_ops_crtp + { + template + requires std::same_as + Derived &operator=(Base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + Base::operator=(static_cast(other)); + return *static_cast(this); + } + + template + requires std::same_as && STDEXEC::__copy_assignable + Derived &operator=(Base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + { + Base::operator=(other); + return *static_cast(this); + } + }; } // namespace _func // the user-facing interface to exec::function that supports several different declaration @@ -386,96 +406,36 @@ namespace experimental::execution // completion signatures struct function : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> { using base = _func::_func_impl, queries<>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; template struct function : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> { using base = _func::_func_impl, queries<>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; template requires STDEXEC::__is_instance_of struct function : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> { using base = _func::_func_impl, queries<>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; template @@ -483,33 +443,13 @@ namespace experimental::execution : _func::_func_impl, _func::_canonical_t>> + , _func::_func_ops_crtp>> { using base = _func::_func_impl, _func::_canonical_t>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; template @@ -517,33 +457,13 @@ namespace experimental::execution : _func::_func_impl, _func::_canonical_t>> + , _func::_func_ops_crtp>> { using base = _func::_func_impl, _func::_canonical_t>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; template @@ -553,33 +473,15 @@ namespace experimental::execution : _func::_func_impl>, _func::_canonical_t>> + , _func::_func_ops_crtp, + queries>> { using base = _func::_func_impl>, _func::_canonical_t>>; using base::base; - - function(function &&) = default; - function(function const &) = default; - - ~function() = default; - - function &operator=(function &&) = default; - function &operator=(function const &) = default; - - function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) - { - base::operator=(static_cast(other)); - return *this; - }; - - function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) - requires STDEXEC::__copy_assignable - { - base::operator=(other); - return *this; - } }; } // namespace experimental::execution From dc55b1bae9d394e01bd51b9e7e130d27432677b1 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 11:14:22 -0700 Subject: [PATCH 43/45] Remove operator== Resolve the GCC 14, Debug, ASAN build failure by removing `operator==` from `function` to follow `std::function`'s example rather than trying to make it type-safe. --- include/exec/function.hpp | 23 ----------------- test/exec/test_function.cpp | 51 ------------------------------------- 2 files changed, 74 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 56a950dc1..eef65551d 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -131,20 +131,6 @@ namespace experimental::execution get_allocator, STDEXEC::__always{std::allocator()}}; - template - bool _equal(std::index_sequence, __tuple const &lhs, __tuple const &rhs) - noexcept(noexcept(((__get(lhs) == __get(rhs)) && ...))) - { - return ((__get(lhs) == __get(rhs)) && ...); - } - - template - bool _equal(__tuple const &lhs, __tuple const &rhs) - noexcept(noexcept(_equal(std::index_sequence_for{}, lhs, rhs))) - { - return _equal(std::index_sequence_for{}, lhs, rhs); - } - template class _func_impl; @@ -183,15 +169,6 @@ namespace experimental::execution STDEXEC_ATTRIBUTE(no_unique_address) STDEXEC::__tuple args_; - // equal args and equal pointers-to-factories are equal - friend constexpr bool operator==(_func_impl const &lhs, _func_impl const &rhs) - noexcept(noexcept(_equal(lhs.args_, rhs.args_))) - { - return lhs.make_op_ == rhs.make_op_ - && std::ranges::equal(lhs.make_sender_, rhs.make_sender_) - && _equal(lhs.args_, rhs.args_); - } - public: using sender_concept = SndrCncpt; diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index aa4f78409..a7093b5a5 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -362,34 +362,6 @@ namespace STATIC_REQUIRE(std::assignable_from); STATIC_REQUIRE(std::assignable_from); } - - SECTION("instances are mutually comparable with ==") - { - // identical copies in slightly different types - func1_t f1(42, ex::just); - func2_t f2(f1); - func3_t f3(f1); - - // differing curried arguments from above - func2_t f4(45, ex::just); - func3_t f5(45, ex::just); - - REQUIRE(f1 == f1); - REQUIRE(f1 == f2); - REQUIRE(f1 == f3); - REQUIRE(f2 == f1); - REQUIRE(f2 == f2); - REQUIRE(f2 == f3); - REQUIRE(f3 == f1); - REQUIRE(f3 == f2); - REQUIRE(f3 == f3); - - REQUIRE(f1 != f4); - REQUIRE(f2 != f4); - REQUIRE(f3 != f4); - - REQUIRE(f4 == f5); - } } TEST_CASE("queries specification is order-independent", "[types][function]") @@ -454,28 +426,5 @@ namespace STATIC_REQUIRE(std::assignable_from); STATIC_REQUIRE(std::assignable_from); } - - SECTION("instances are mutually comparable with ==") - { - // identical copies in slightly different types - func1_t f1(42, ex::just); - func2_t f2(f1); - - // differing curried arguments from above - func1_t f3(45, ex::just); - func2_t f4(45, ex::just); - - REQUIRE(f1 == f1); - REQUIRE(f1 == f2); - REQUIRE(f2 == f1); - REQUIRE(f2 == f2); - - REQUIRE(f1 != f3); - REQUIRE(f3 != f1); - REQUIRE(f2 != f3); - REQUIRE(f3 != f2); - - REQUIRE(f3 == f4); - } } } // namespace From d7df333d9fd7003ef15e7bb70426d5efe6ffea6b Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 11:23:46 -0700 Subject: [PATCH 44/45] Remove _canonical_fn> specialization This diff adds support for `erase` and `operator[]` to `__static_vector`, which allows me to delete the specialization of `_canonical_fn>` because empty query lists can be sorted and uniqued uniformly. --- include/exec/function.hpp | 9 --------- include/stdexec/__detail/__static_vector.hpp | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index eef65551d..18a046856 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -304,15 +304,6 @@ namespace experimental::execution } }; - template <> - struct _canonical_fn> - { - consteval auto operator()() const noexcept - { - return queries<>(); - } - }; - template inline constexpr _canonical_fn _canonical{}; diff --git a/include/stdexec/__detail/__static_vector.hpp b/include/stdexec/__detail/__static_vector.hpp index 0f01046d8..ae81bc198 100644 --- a/include/stdexec/__detail/__static_vector.hpp +++ b/include/stdexec/__detail/__static_vector.hpp @@ -159,6 +159,18 @@ namespace STDEXEC __static_vector() = default; + [[nodiscard]] + constexpr auto operator[](std::size_t) noexcept -> value_type & + { + STDEXEC_ASSERT(false); + } + + [[nodiscard]] + constexpr auto operator[](std::size_t) const noexcept -> value_type const & + { + STDEXEC_ASSERT(false); + } + [[nodiscard]] constexpr auto begin() noexcept -> iterator { @@ -194,6 +206,14 @@ namespace STDEXEC { return 0; } + + constexpr auto erase(const_iterator __first, const_iterator __last) noexcept -> iterator + { + STDEXEC_ASSERT(__first == __last); + STDEXEC_ASSERT(__first == nullptr); + + return end(); + } }; template ... _Rest> From 0986575bce62aed1fb9150dfcfd5451075c35aee Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 4 May 2026 11:46:17 -0700 Subject: [PATCH 45/45] Make function::base private This diff removes the `function::base` nested alias from `function`'s public interface. --- include/exec/function.hpp | 30 ++++++++++++++++++++++++++++++ test/exec/test_function.cpp | 16 ++-------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/include/exec/function.hpp b/include/exec/function.hpp index 18a046856..b41b43684 100644 --- a/include/exec/function.hpp +++ b/include/exec/function.hpp @@ -376,10 +376,15 @@ namespace experimental::execution : _func::_func_impl, queries<>> , _func::_func_ops_crtp> { + private: using base = _func::_func_impl, queries<>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; @@ -388,9 +393,14 @@ namespace experimental::execution : _func::_func_impl, queries<>> , _func::_func_ops_crtp> { + private: using base = _func::_func_impl, queries<>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; @@ -400,9 +410,14 @@ namespace experimental::execution : _func::_func_impl, queries<>> , _func::_func_ops_crtp> { + private: using base = _func::_func_impl, queries<>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; @@ -413,10 +428,15 @@ namespace experimental::execution _func::_canonical_t>> , _func::_func_ops_crtp>> { + private: using base = _func::_func_impl, _func::_canonical_t>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; @@ -427,10 +447,15 @@ namespace experimental::execution _func::_canonical_t>> , _func::_func_ops_crtp>> { + private: using base = _func::_func_impl, _func::_canonical_t>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; @@ -445,10 +470,15 @@ namespace experimental::execution STDEXEC::completion_signatures, queries>> { + private: using base = _func::_func_impl>, _func::_canonical_t>>; + template class, template class> + friend struct std::basic_common_reference; + + public: using base::base; }; } // namespace experimental::execution diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp index a7093b5a5..58b490845 100644 --- a/test/exec/test_function.cpp +++ b/test/exec/test_function.cpp @@ -297,20 +297,13 @@ namespace exec::function>; - SECTION("the function types are not the same as each other...") + SECTION("the function types are not the same as each other") { STATIC_REQUIRE(!std::same_as); STATIC_REQUIRE(!std::same_as); STATIC_REQUIRE(!std::same_as); } - SECTION("...but they all inherit from the same _func_impl base") - { - STATIC_REQUIRE(std::same_as); - STATIC_REQUIRE(std::same_as); - STATIC_REQUIRE(std::same_as); - } - SECTION("move-construction works in every direction between all three types") { STATIC_REQUIRE(std::constructible_from); @@ -385,16 +378,11 @@ namespace using func2_t = exec::function>; - SECTION("the function types are not the same as each other...") + SECTION("the function types are not the same as each other") { STATIC_REQUIRE(!std::same_as); } - SECTION("...but they both inherit from the same _func_impl base") - { - STATIC_REQUIRE(std::same_as); - } - SECTION("move construction works in all directions with both types") { STATIC_REQUIRE(std::constructible_from);