This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.
future-senders returned from spawn_future do not forward stop requests to spawned workSection: 33.9.12.18 [exec.spawn.future] Status: New Submitter: Ian Petersen Opened: 2026-03-10 Last modified: 2026-03-13
Priority: Not Prioritized
View all issues with New status.
Discussion:
The wording that describes spawn_future (specifically 33.9.12.18 [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.
When
fsopis started, iffsopreceives 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 whilefsopis requesting stop; once the stop request has been delivered, eitherfsopcompletes with the result of the eagerly-started work if it's ready, or it completes withset_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.
future-sender
returned from spawn_future in terms of the basic-sender machinery like so:
-13- The exposition-only class template
impls-for(33.9.2 [exec.snd.expos]) is specialized forspawn_future_tas 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>::startis 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 33.9.12.18 [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.
Proposed resolution:
This wording is relative to N5032.
Modify 33.9.12.18 [exec.spawn.future] as indicated:
-2- The name
[…] If any ofspawn_futuredenotes a customization point object. For subexpressionssndr,token, andenv,sender<Sndr>,scope_token<Token>, orqueryable<Env>are not satisfied, the expressionspawn_future(sndr, token, env)is ill-formed. Lettry-cancelablebe 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-basebe 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- Letspawn-future-statebe 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 […] }; […] }-8- For purposes of determining the existence of a data race,
complete,consume,try-cancel, andabandonbehave as atomic operations (6.10.2 [intro.multithread]). These operations on a single object of a type that is a specialization ofspawn-future-stateappear to occur in a single total order.void complete() noexcept;-9- Effects:
(9.1) — No effects if this invocation of
completehappens before an invocation ofconsumetry-cancel, orabandonon*this;(9.2) — otherwise, if an invocation of
consumeand no invocation oftry-cancelon*thishappened before this invocation ofcompleteon*thishappens before this invocation ofcompletethen there is a receiver,rcvr, registered and that receiver is deregistered and completed as if byconsume(rcvr);(9.3) — otherwise,
destroyis invoked.void consume(receiver auto& rcvr) noexcept;-10- Effects:
(10.1) — If this invocation of
consumehappens before an invocation ofcompleteon*thisand no invocation oftry-cancelon*thishappened before this invocation ofconsumethenrcvris registered to be completed whencompleteis subsequently invoked on*this;(10.?) — otherwise, if this invocation of
consumehappens after an invocation oftry-cancelon*thisand no invocation ofcompleteon*thishappened before this invocation ofconsumethenrcvris completed as if byset_stopped(std::move(rcvr));(10.2) — otherwise,
rcvris completed as if by: […]void try-cancel() noexcept;-?- Effects:
(?.1) — No effects if this invocation of
try-cancelhappens after an invocation ofcompleteon*this;(?.2) — otherwise, if this invocation of
try-cancelhappens before an invocation ofconsumeon*thisthen invokesssource.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 byset_stopped(std::move(rcvr)).[Note: an invocation of
completeon*thismay have happened after the just-described invocation ofssource.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 oftry-cancelorcompletecompletes 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-operationbe 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(33.9.2 [exec.snd.expos]) is specialized forspawn_future_tas 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>::startis 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-stateis 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)); }