Created on 2026-03-10.00:00:00 last changed 3 days ago
Proposed resolution:
This wording is relative to N5032.
Modify [exec.spawn.future] as indicated:
-2- The name `spawn_future` denotes a customization point object. For subexpressions `sndr`, `token`, and `env`,
[…] If any of sender<Sndr>, scope_token<Token>, or queryable<Env> are not satisfied, the expression `spawn_future(sndr, token, env)` is ill-formed. -?- Let try-cancelable be the exposition-only class:namespace std::execution { struct try-cancelable { // exposition only virtual void try-cancel() noexcept = 0; // exposition only }; }-3- Let spawn-future-state-base be the exposition-only class template: […]
namespace std::execution { template<class Completions> struct spawn-future-state-base; // exposition only template<class... Sigs> struct spawn-future-state-base<completion_signatures<Sigs...>>{// exposition only : try-cancelable { using variant-t = see below; // exposition only variant-t result; // exposition only virtual void complete() noexcept = 0; // exposition only }; }[…]
-7- Let spawn-future-state be the exposition-only class template:namespace std::execution { template<class Alloc, scope_token Token, sender Sender, class Env> struct spawn-future-state // exposition only : spawn-future-state-base<completion_signatures_of_t<future-spawned-sender<Sender, Env>>> { […] void complete() noexcept override; // exposition only void consume(receiver auto& rcvr) noexcept; // exposition only void abandon() noexcept; // exposition only void try-cancel() noexcept override { // exposition only ssource.request_stop(); try-set-stopped(); } void try-set-stopped() noexcept; // exposition only […] }; […] }-8- For purposes of determining the existence of a data race, complete, consume, try-set-stopped, and abandon behave as atomic operations ([intro.multithread]). These operations on a single object of a type that is a specialization of spawn-future-state appear to occur in a single total order.
void complete() noexcept;-9- Effects:
(9.1) — No effects if this invocation of complete happens before an invocation of consume, try-set-stopped, or abandon on `*this`;
(9.2) — otherwise, if an invocation of consume on `*this` happens before this invocation of complete and no invocation of try-set-stopped on `*this` happened before this invocation of complete then there is a receiver, `rcvr`, registered and that receiver is deregistered and completed as if by consume(rcvr);
(9.3) — otherwise, destroy is invoked.
void consume(receiver auto& rcvr) noexcept;-10- Effects:
(10.1) — If this invocation of consume happens before an invocation of complete on `*this` and no invocation of try-set-stopped on `*this` happened before this invocation of consume then `rcvr` is registered to be completed when complete is subsequently invoked on `*this`;
(10.?) — otherwise, if this invocation of consume happens after an invocation of try-set-stopped on `*this` and no invocation of complete on `*this` happened before this invocation of consume then `rcvr` is completed as if by `set_stopped(std::move(rcvr))`;
(10.2) — otherwise, `rcvr` is completed as if by:
std::move(this->result).visit( [&rcvr](auto&& tuple) noexcept { if constexpr (!same_as<remove_reference_t<decltype(tuple)>, monostate>) { apply([&rcvr](auto cpo, auto&&... vals) { cpo(std::move(rcvr), std::move(vals)...); }, std::move(tuple)); } }); destroy();void try-set-stopped() noexcept;-?- Effects:
(?.1) — If an invocation of consume on `*this` happens before this invocation of try-set-stopped and no invocation of complete on `*this` happened before this invocation of try-set-stopped then there is a receiver, `rcvr`, registered and that receiver is deregistered and completed as if by set_stopped(std::move(rcvr)), destroy();
(?.2) — otherwise, no effects.
void abandon() noexcept;[…]
void destroy() noexcept;-12- Effects: Equivalent to:
auto associated = std::move(this->associated); { using traits = allocator_traits<Alloc>::template rebind_traits<spawn-future-state>; typename traits::allocator_type alloc(std::move(this->alloc)); traits::destroy(alloc, this); traits::deallocate(alloc, this, 1); }-?- Let future-operation be the exposition-only class template:
namespace std::execution { template<class StatePtr, class Rcvr> struct future-operation { // exposition only struct callback { // exposition only try-cancelable* state; // exposition only void operator()() noexcept { state->try-cancel(); }; }; using stop-token-t = // exposition only stop_token_of_t<env_of_t<Rcvr>>; using stop-callback-t = // exposition only stop_callback_for_t<stop-token-t, callback>; struct receiver { // exposition only using receiver_concept = receiver_t; future-operation* op; // exposition only template<class... T> void set_value(T&&... ts) && noexcept { op->set-complete<set_value_t>(std::forward<T>(ts)...); } template<class E> void set_error(E&& e) && noexcept { op->set-complete<set_error_t>(std::forward<E>(e)); } void set_stopped() && noexcept { op->set-complete<set_stopped_t>(); } env_of_t<Rcvr> get_env() const noexcept { return execution::get_env(op->rcvr); } }; Rcvr rcvr; // exposition only StatePtr state; // exposition only receiver inner; // exposition only optional<stop-callback-t> stopCallback; // exposition only future-operation(StatePtr state, Rcvr rcvr) noexcept // exposition only : rcvr(std::move(rcvr)), state(std::move(state)), inner(this) {} future-operation(future-operation&&) = delete; void run() & noexcept { // exposition only constexpr bool nothrow = is_nothrow_constructible_v<stop-callback-t, stop-token-t, callback>; try { stopCallback.emplace(get_stop_token(rcvr), callback(state.get())); } catch (...) { if constexpr (!nothrow) { set_error(std::move(rcvr), current_exception()); return; } } state.release()->consume(inner); } template<class CPO, class... T> void set-complete(T&&... ts) noexcept { // exposition only stopCallback.reset(); CPO{}(std::move(rcvr), std::forward<T>(ts)...); } }; }-13- The exposition-only class template impls-for ([exec.snd.expos]) is specialized for `spawn_future_t` as follows:
namespace std::execution { template<> struct impls-for<spawn_future_t> : default-impls { static constexpr auto start = see below; // exposition only static constexpr auto get-state = see below; // exposition only }; }-14- The member impls-for<spawn_future_t>::start is initialized with a callable object equivalent to the following lambda:
[](auto& state, auto&rcvr) noexcept -> void { state.run()->consume(rcvr); }-?- The member impls-for<spawn_future_t>::get-state is initialized with a callable object equivalent to the following lambda:
[]<class Sndr, class Rcvr>(Sndr sndr, Rcvr& rcvr) noexcept { auto& [_, data] = sndr; using state_ptr = remove_cvref_t<decltype(data)>; return future-operation<state_ptr, Rcvr>(std::move(data), std::move(rcvr)); }
[ Croydon 2026-03-27; Status changed: New → Immediate. ]
[ 2026-03-25; Tomasz provides updated wording. ]
The updated wording removes the union inside future-operation.
[ 2026-03-25; Reflector poll. ]
Set priority to 1 after reflector poll.
This wording is relative to N5032.
Modify [exec.spawn.future] as indicated:
-2- The name `spawn_future` denotes a customization point object. For subexpressions `sndr`, `token`, and `env`,
[…] If any of sender<Sndr>, scope_token<Token>, or queryable<Env> are not satisfied, the expression `spawn_future(sndr, token, env)` is ill-formed. -?- Let try-cancelable be the exposition-only class:namespace std::execution { struct try-cancelable { // exposition only virtual void try-cancel() noexcept = 0; // exposition only }; }-3- Let spawn-future-state-base be the exposition-only class template: […]
namespace std::execution { template<class Completions> struct spawn-future-state-base; // exposition only template<class... Sigs> struct spawn-future-state-base<completion_signatures<Sigs...>>{// exposition only : try-cancelable { using variant-t = see below; // exposition only variant-t result; // exposition only virtual void complete() noexcept = 0; // exposition only }; }[…]
-7- Let spawn-future-state be the exposition-only class template:namespace std::execution { template<class Alloc, scope_token Token, sender Sender, class Env> struct spawn-future-state // exposition only : spawn-future-state-base<completion_signatures_of_t<future-spawned-sender<Sender, Env>>> { […] void complete() noexcept override; // exposition only void consume(receiver auto& rcvr) noexcept; // exposition only void abandon() noexcept; // exposition only void try-cancel() noexcept override { // exposition only ssource.request_stop(); try-set-stopped(); } void try-set-stopped() noexcept; // exposition only […] }; […] }-8- For purposes of determining the existence of a data race, complete, consume, try-set-stopped, and abandon behave as atomic operations ([intro.multithread]). These operations on a single object of a type that is a specialization of spawn-future-state appear to occur in a single total order.
void complete() noexcept;-9- Effects:
(9.1) — No effects if this invocation of complete happens before an invocation of consume, try-set-stopped, or abandon on `*this`;
(9.2) — otherwise, if an invocation of consume on `*this` happens before this invocation of complete and no invocation of try-set-stopped on `*this` happened before this invocation of complete then there is a receiver, `rcvr`, registered and that receiver is deregistered and completed as if by consume(rcvr);
(9.3) — otherwise, destroy is invoked.
void consume(receiver auto& rcvr) noexcept;-10- Effects:
(10.1) — If this invocation of consume happens before an invocation of complete on `*this` and no invocation of try-cancel on `*this` happened before this invocation of consume then `rcvr` is registered to be completed when complete is subsequently invoked on `*this`;
(10.?) — otherwise, if this invocation of consume happens after an invocation of try-cancel on `*this` and no invocation of complete on `*this` happened before this invocation of consume then `rcvr` is completed as if by `set_stopped(std::move(rcvr))`;
(10.2) — otherwise, `rcvr` is completed as if by: […]
void try-cancel() noexcept;-?- Effects:
(?.1) — No effects if this invocation of try-cancel happens after an invocation of complete on `*this`;
(?.2) — otherwise, if this invocation of try-cancel happens before an invocation of consume on `*this` then invokes ssource.request_stop();
(?.3) — otherwise,
(?.3.1) — invokes ssource.request_stop(), and
(?.3.2) — if there is a receiver, `rcvr`, still registered then that receiver is deregistered and completed as if by `set_stopped(std::move(rcvr))`.
[Note: an invocation of complete on `*this` may have happened after the just-described invocation of ssource.request_stop() and happened before the check to see if there is a receiver still registered; if so, it would have deregistered and completed the previously-registered receiver. Only one of try-cancel or complete completes the registered receiver and no data races are introduced between the two invocations. — end note]
void abandon() noexcept;[…]
void destroy() noexcept;-12- Effects: Equivalent to:
auto associated = std::move(this->associated); { using traits = allocator_traits<Alloc>::template rebind_traits<spawn-future-state>; typename traits::allocator_type alloc(std::move(this->alloc)); traits::destroy(alloc, this); traits::deallocate(alloc, this, 1); }-?- Let future-operation be the exposition-only class template:
namespace std::execution { template<class StatePtr, class Rcvr> struct future-operation { // exposition only struct callback { // exposition only try-cancelable* state; // exposition only void operator()() noexcept { state->try-cancel(); }; }; using stop-token-t = // exposition only stop_token_of_t<env_of_t<Rcvr>>; using stop-callback-t = // exposition only stop_callback_for_t<stop-token-t, callback>; struct receiver { // exposition only using receiver_concept = receiver_t; future-operation* op; // exposition only template<class... T> void set_value(T&&... ts) && noexcept { op->set-complete<set_value_t>(std::forward<T>(ts)...); } template<class E> void set_error(E&& e) && noexcept { op->set-complete<set_error_t>(std::forward<E>(e)); } void set_stopped() && noexcept { op->set-complete<set_stopped_t>(); } env_of_t<Rcvr> get_env() const noexcept { return op->rcvr.get_env(); } }; Rcvr rcvr; // exposition only union { StatePtr state; // exposition only receiver inner; // exposition only }; union { stop-callback-t stopCallback; // exposition only }; future-operation(StatePtr state, Rcvr rcvr) noexcept // exposition only : rcvr(std::move(rcvr)) { construct_at(addressof(state), std::move(state)); } future-operation(future-operation&&) = delete; ~future-operation() { destroy_at(addressof(state)); } void run() & noexcept { // exposition only constexpr bool nothrow = is_nothrow_constructible_v<stop-callback-t, stop-token-t, callback>; try { construct_at(addressof(stopCallback), get_stop_token(rcvr), callback(state.get())); } catch (...) { if constexpr (!nothrow) { set_error(std::move(rcvr), current_exception()); return; } } auto* state = state.release(); destroy_at(addressof(state)); construct_at(addressof(inner), this); state->consume(inner); } template<class CPO, class... T> void set-complete(T&&... ts) noexcept { // exposition only destroy_at(addressof(stopCallback)); destroy_at(addressof(inner)); construct_at(addressof(state), nullptr); CPO{}(std::move(rcvr), std::forward<T>(ts)...); } }; }-13- The exposition-only class template impls-for ([exec.snd.expos]) is specialized for `spawn_future_t` as follows:
namespace std::execution { template<> struct impls-for<spawn_future_t> : default-impls { static constexpr auto start = see below; // exposition only static constexpr auto get-state = see below; // exposition only }; }-14- The member impls-for<spawn_future_t>::start is initialized with a callable object equivalent to the following lambda:
[](auto& state, auto&rcvr) noexcept -> void { state.run()->consume(rcvr); }-?- The member impls-for<spawn_future_t>::get-state is initialized with a callable object equivalent to the following lambda:
[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept { auto& [_, data] = sndr; using state_ptr = remove_cvref_t<decltype(data)>; return future-operation<state_ptr, Rcvr>(std::move(data), std::move(rcvr)); }
The wording that describes `spawn_future` (specifically [exec.spawn.future] paragraph 13 and paragraph 14) does not capture a critical element of the design intent originally expressed in P3149R11, section 5.5.
P3149R11, section 5.5 reads in part,When `fsop` is started, if `fsop` receives a stop request from its receiver before the eagerly-started work has completed then an attempt is made to abandon the eagerly-started work. Note that it's possible for the eagerly-started work to complete while `fsop` is requesting stop; once the stop request has been delivered, either `fsop` completes with the result of the eagerly-started work if it's ready, or it completes with `set_stopped()` without waiting for the eagerly-started work to complete.
In the foregoing, `fsop` is the name of an operation state constructed by connecting a future-sender (i.e. a sender returned from `spawn_future`) to a receiver.
Paragraphs 13 and 14 of [exec.spawn.future] describe the behaviour of the future-sender returned from `spawn_future` in terms of the basic-sender machinery like so:-13- The exposition-only class template impls-for ([exec.snd.expos]) is specialized for `spawn_future_t` as follows:
namespace std::execution { template<> struct impls-for<spawn_future_t> : default-impls { static constexpr auto start = see below; // exposition only }; }-14- The member impls-for<spawn_future_t>::start is initialized with a callable object equivalent to the following lambda:
[](auto& state, auto& rcvr) noexcept -> void { state->consume(rcvr); }
Since there's no specification for the behaviour of std::execution::impls-for<spawn_future_t>::get-state, the behaviour is the default provided by std::execution::default-impls::get-state, which just returns the "data" member of the original result of make-sender. In this case, that is the object named `u` defined in [exec.spawn.future] bullet 16.2, which is an instance of a specialization of `std::unique_ptr`. There is therefore no wording to require that a future-sender that has been connected and started take any action in response to stop requests received through the receiver to which it was connected, contrary to the LEWG-approved design intent.
An implementation that addresses this issue is included in stdexec PR 1713, specifically in commit 5209ffdcaf9a3badf0079746b5578c12a1d0da4f, which is just the difference between the current wording and the intended design.| History | |||
|---|---|---|---|
| Date | User | Action | Args |
| 2026-03-27 14:30:00 | admin | set | messages: + msg16125 |
| 2026-03-27 14:30:00 | admin | set | status: new -> immediate |
| 2026-03-25 21:10:45 | admin | set | messages: + msg16092 |
| 2026-03-25 10:06:37 | admin | set | messages: + msg16071 |
| 2026-03-13 14:39:48 | admin | set | messages: + msg16023 |
| 2026-03-10 00:00:00 | admin | create | |