Skip to content

Commit 0f25c2a

Browse files
committed
New issue from Ian Petersen: "future-senders returned from spawn_future do not forward stop requests to spawned work"
1 parent 20bc07a commit 0f25c2a

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed

xml/issue4540.xml

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
<?xml version='1.0' encoding='utf-8' standalone='no'?>
2+
<!DOCTYPE issue SYSTEM "lwg-issue.dtd">
3+
4+
<issue num="4540" status="New">
5+
<title><tt><i>future-sender</i></tt>s returned from `spawn_future` do not forward stop requests to spawned work</title>
6+
<section>
7+
<sref ref="[exec.spawn.future]"/>
8+
</section>
9+
<submitter>Ian Petersen</submitter>
10+
<date>10 Mar 2026</date>
11+
<priority>99</priority>
12+
13+
<discussion>
14+
<p>
15+
The wording that describes `spawn_future` (specifically <sref ref="[exec.spawn.future]"/> paragraph 13 and paragraph 14)
16+
does not capture a critical element of the design intent originally expressed in <paper num="P3149R11"/>, section 5.5.
17+
<p/>
18+
<paper num="P3149R11"/>, section 5.5 reads in part,
19+
</p>
20+
<blockquote style="border-left: 3px solid #ccc;padding-left: 15px;">
21+
<p>
22+
When `fsop` is started, if `fsop` receives a stop request from its receiver before the eagerly-started work has
23+
completed then an attempt is made to abandon the eagerly-started work. Note that it's possible for the eagerly-started
24+
work to complete while `fsop` is requesting stop; once the stop request has been delivered, either `fsop` completes with
25+
the result of the eagerly-started work if it's ready, or it completes with `set_stopped()` without waiting for
26+
the eagerly-started work to complete.
27+
</p>
28+
</blockquote>
29+
<p>
30+
In the foregoing, `fsop` is the name of an operation state constructed by connecting a <tt><i>future-sender</i></tt>
31+
(i.e. a sender returned from `spawn_future`) to a receiver.
32+
<p/>
33+
Paragraphs 13 and 14 of <sref ref="[exec.spawn.future]"/> describe the behaviour of the <tt><i>future-sender</i></tt>
34+
returned from `spawn_future` in terms of the <tt><i>basic-sender</i></tt> machinery like so:
35+
</p>
36+
<blockquote style="border-left: 3px solid #ccc;padding-left: 15px;">
37+
<p>
38+
-13- The exposition-only class template <tt><i>impls-for</i></tt> (<sref ref="[exec.snd.expos]"/>) is specialized
39+
for `spawn_future_t` as follows:
40+
</p>
41+
<blockquote><pre>
42+
namespace std::execution {
43+
template&lt;&gt;
44+
struct <i>impls-for</i>&lt;spawn_future_t&gt; : <i>default-impls</i> {
45+
static constexpr auto <i>start</i> = <i>see below</i>; // <i>exposition only</i>
46+
};
47+
}
48+
</pre></blockquote>
49+
<p>
50+
-14- The member <tt><i>impls-for</i>&lt;spawn_future_t&gt;::<i>start</i></tt> is initialized with a callable object
51+
equivalent to the following lambda:
52+
</p>
53+
<blockquote><pre>
54+
[](auto&amp; state, auto&amp; rcvr) noexcept -&gt; void {
55+
state-&gt;<i>consume</i>(rcvr);
56+
}
57+
</pre></blockquote>
58+
</blockquote>
59+
<p>
60+
Since there's no specification for the behaviour of
61+
<tt>std::execution::<i>impls-for</i>&lt;spawn_future_t&gt;::get_state</tt>, the behaviour is the default
62+
provided by <tt>std::execution::<i>default-impls</i>::get_state</tt>, which just returns the "data" member
63+
of the original result of <tt><i>make-sender</i></tt>. In this case, that is the object named `u`
64+
defined in <sref ref="[exec.spawn.future]"/> bullet 16.2, which is an instance of a specialization of
65+
`std::unique_ptr`. There is therefore no wording to require that a <tt><i>future-sender</i></tt> that has
66+
been connected and started take any action in response to stop requests received through the receiver to
67+
which it was connected, contrary to the LEWG-approved design intent.
68+
<p/>
69+
An implementation that addresses this issue is included in
70+
<a href="https://github.com/NVIDIA/stdexec/pull/1713">stdexec PR 1713</a>, specifically in commit
71+
<a href="https://github.com/NVIDIA/stdexec/pull/1713/changes/5209ffdcaf9a3badf0079746b5578c12a1d0da4f">5209ffdcaf9a3badf0079746b5578c12a1d0da4f</a>,
72+
which is just the difference between the current wording and the intended design.
73+
</p>
74+
</discussion>
75+
76+
<resolution>
77+
<p>
78+
This wording is relative to <paper num="N5032"/>.
79+
</p>
80+
81+
<ol>
82+
83+
<li><p>Modify <sref ref="[exec.spawn.future]"/> as indicated:</p>
84+
85+
<blockquote>
86+
<p>
87+
-2- The name `spawn_future` denotes a customization point object. For subexpressions `sndr`, `token`, and `env`,
88+
<p/>
89+
[&hellip;]
90+
<p/>
91+
If any of <tt>sender&lt;Sndr&gt;</tt>, <tt>scope_token&lt;Token&gt;</tt>, or <tt><i>queryable</i>&lt;Env&gt;</tt>
92+
are not satisfied, the expression `spawn_future(sndr, token, env)` is ill-formed.
93+
<p/>
94+
<ins>Let <tt><i>try-cancelable</i></tt> be the exposition-only class:</ins>
95+
</p>
96+
<blockquote><pre>
97+
<ins>namespace std::execution {
98+
struct <i>try-cancelable</i> { // <i>exposition only</i>
99+
virtual void <i>try-cancel</i>() noexcept = 0; // <i>exposition only</i>
100+
};
101+
}</ins>
102+
</pre></blockquote>
103+
<p>
104+
-3- Let <tt><i>spawn-future-state-base</i></tt> be the exposition-only class template: [&hellip;]
105+
</p>
106+
<blockquote><pre>
107+
namespace std::execution {
108+
template&lt;class Completions&gt;
109+
struct <i>spawn-future-state-base</i>; // <i>exposition only</i>
110+
111+
template&lt;class... Sigs&gt;
112+
struct <i>spawn-future-state-base</i>&lt;completion_signatures&lt;Sigs...&gt;&gt; <del>{</del> // <i>exposition only</i>
113+
<ins>: <i>try-cancelable</i> {</ins>
114+
using <i>variant-t</i> = <i>see below</i>; // <i>exposition only</i>
115+
<i>variant-t result</i>; // <i>exposition only</i>
116+
virtual void <i>complete</i>() noexcept = 0; // <i>exposition only</i>
117+
};
118+
}
119+
</pre></blockquote>
120+
<p>
121+
[&hellip;]
122+
<p/>
123+
-7- Let <tt><i>spawn-future-state</i></tt> be the exposition-only class template:
124+
</p>
125+
<blockquote><pre>
126+
namespace std::execution {
127+
template&lt;class Alloc, scope_token Token, sender Sender, class Env&gt;
128+
struct <i>spawn-future-state</i> // <i>exposition only</i>
129+
: <i>spawn-future-state-base</i>&lt;completion_signatures_of_t&lt;<i>future-spawned-sender</i>&lt;Sender, Env&gt;&gt;&gt; {
130+
[&hellip;]
131+
void <i>complete</i>() noexcept override; // <i>exposition only</i>
132+
void <i>consume</i>(receiver auto&amp; rcvr) noexcept; // <i>exposition only</i>
133+
void <i>abandon</i>() noexcept; // <i>exposition only</i>
134+
<ins>void <i>try-cancel</i>() noexcept override; // <i>exposition only</i></ins>
135+
[&hellip;]
136+
};
137+
[&hellip;]
138+
}
139+
</pre></blockquote>
140+
<p>
141+
-8- For purposes of determining the existence of a data race, <tt><i>complete</i></tt>, <tt><i>consume</i></tt>,
142+
<ins><tt><i>try-cancel</i></tt>,</ins> and <tt><i>abandon</i></tt> behave as atomic operations
143+
(<sref ref="[intro.multithread]"/>). These operations on a single object of a type that is a specialization of
144+
<tt><i>spawn-future-state</i></tt> appear to occur in a single total order.
145+
</p>
146+
<pre>
147+
void <i>complete</i>() noexcept;
148+
</pre>
149+
<blockquote>
150+
<p>
151+
-9- <i>Effects</i>:
152+
</p>
153+
<ul style="list-style-type: none">
154+
<li><p>(9.1) &mdash; No effects if this invocation of <tt><i>complete</i></tt> happens before an invocation of
155+
<tt><i>consume</i></tt><ins><tt><i>try-cancel</i></tt>,</ins> or <tt><i>abandon</i></tt> on `*this`;</p></li>
156+
<li><p>(9.2) &mdash; otherwise, if an invocation of <tt><i>consume</i></tt> <ins>and no invocation of
157+
<tt><i>try-cancel</i></tt> on `*this` happened before this invocation of <tt><i>complete</i></tt></ins> on
158+
`*this` happens before this invocation of <tt><i>complete</i></tt> then there is a receiver, `rcvr`, registered
159+
and that receiver is <ins>deregistered and</ins> completed as if by <tt><i>consume</i>(rcvr)</tt>;</p></li>
160+
<li><p>(9.3) &mdash; otherwise, <tt><i>destroy</i></tt> is invoked.</p></li>
161+
</ul>
162+
</blockquote>
163+
<pre>
164+
void <i>consume</i>(receiver auto&amp; rcvr) noexcept;
165+
</pre>
166+
<blockquote>
167+
<p>
168+
-10- <i>Effects</i>:
169+
</p>
170+
<ul style="list-style-type: none">
171+
<li><p>(10.1) &mdash; If this invocation of <tt><i>consume</i></tt> happens before an invocation of
172+
<tt><i>complete</i></tt> on `*this` <ins>and no invocation of <tt><i>try-cancel</i></tt> on `*this`
173+
happened before this invocation of <tt><i>consume</i></tt></ins> then `rcvr` is registered to be
174+
completed when <tt><i>complete</i></tt> is subsequently invoked on `*this`;</p></li>
175+
<li><p><ins>(10.?) &mdash; otherwise, if this invocation of <tt><i>consume</i></tt> happens after an
176+
invocation of <tt><i>try-cancel</i></tt> on `*this` and no invocation of <tt><i>complete</i></tt> on
177+
`*this` happened before this invocation of <tt><i>consume</i></tt> then `rcvr` is completed as if by
178+
`set_stopped(std::move(rcvr))`;</ins></p></li>
179+
<li><p>(10.2) &mdash; otherwise, `rcvr` is completed as if by: [&hellip;]</p></li>
180+
</ul>
181+
</blockquote>
182+
<pre>
183+
<ins>void <i>try-cancel()</i> noexcept;</ins>
184+
</pre>
185+
<blockquote>
186+
<p>
187+
<ins>-?- <i>Effects</i>:</ins>
188+
</p>
189+
<ul style="list-style-type: none">
190+
<li><p><ins>(?.1) &mdash; No effects if this invocation of <tt><i>try-cancel</i></tt> happens after
191+
an invocation of <tt><i>complete</i></tt> on `*this`;</ins></p></li>
192+
<li><p><ins>(?.2) &mdash; otherwise, if this invocation of <tt><i>try-cancel</i></tt> happens before
193+
an invocation of <tt><i>consume</i></tt> on `*this` then invokes <tt><i>ssource</i>.request_stop()</tt>;</ins></p></li>
194+
<li><p><ins>(?.3) &mdash; otherwise,</ins></p>
195+
<ul style="list-style-type: none">
196+
<li><p><ins>(?.3.1) &mdash; invokes <tt><i>ssource</i>.request_stop()</tt>, and</ins></p></li>
197+
<li><p><ins>(?.3.2) &mdash; if there is a receiver, `rcvr`, still registered then that receiver is
198+
deregistered and completed as if by `set_stopped(std::move(rcvr))`.</ins></p></li>
199+
</ul>
200+
<p>
201+
<ins>[<i>Note</i>: an invocation of <tt><i>complete</i></tt> on `*this` may have happened after the
202+
just-described invocation of <tt><i>ssource</i>.request_stop()</tt> and happened before the check to see
203+
if there is a receiver still registered; if so, it would have deregistered and completed the
204+
previously-registered receiver. Only one of <tt><i>try-cancel</i></tt> or <tt><i>complete</i></tt>
205+
completes the registered receiver and no data races are introduced between the two invocations. &mdash;
206+
<i>end note</i>]</ins>
207+
</p>
208+
</li>
209+
</ul>
210+
</blockquote>
211+
<pre>
212+
void <i>abandon</i>() noexcept;
213+
</pre>
214+
<blockquote>
215+
<p>
216+
[&hellip;]
217+
</p>
218+
</blockquote>
219+
<pre>
220+
void <i>destroy</i>() noexcept;
221+
</pre>
222+
<blockquote>
223+
<p>
224+
-12- <i>Effects</i>: Equivalent to:
225+
</p>
226+
<blockquote><pre>
227+
auto associated = std::move(this-&gt;<i>associated</i>);
228+
{
229+
using traits = allocator_traits&lt;Alloc&gt;::template rebind_traits&lt;<i>spawn-future-state</i>&gt;;
230+
typename traits::allocator_type alloc(std::move(this-&gt;<i>alloc</i>));
231+
traits::destroy(alloc, this);
232+
traits::deallocate(alloc, this, 1);
233+
}
234+
</pre></blockquote>
235+
<p>
236+
<ins>Let <tt><i>future-operation</i></tt> be the exposition-only class template:</ins>
237+
</p>
238+
<blockquote><pre><ins>
239+
namespace std::execution {
240+
template&lt;class StatePtr, class Rcvr&gt;
241+
struct <i>future-operation</i> { // <i>exposition only</i>
242+
struct <i>callback</i> { // <i>exposition only</i>
243+
<i>try-cancelable</i>* <i>state</i>; // <i>exposition only</i>
244+
245+
void operator()() noexcept {
246+
<i>state</i>-&gt;<i>try-cancel</i>();
247+
};
248+
};
249+
250+
using <i>stop-token-t</i> = // <i>exposition only</i>
251+
stop_token_of_t&lt;env_of_t&lt;Rcvr&gt;&gt;;
252+
253+
using <i>stop-callback-t</i> = // <i>exposition only</i>
254+
stop_callback_for_t&lt;stop-token-t, callback&gt;;
255+
256+
struct <i>receiver</i> { // <i>exposition only</i>
257+
using receiver_concept = receiver_t;
258+
<i>future-operation</i>* <i>op</i>; // <i>exposition only</i>
259+
260+
template&lt;class... T&gt;
261+
void set_value(T&amp;&amp;... ts) &amp;&amp; noexcept {
262+
<i>op</i>-&gt;<i>set-complete</i>&lt;set_value_t&gt;(std::forward&lt;T&gt;(ts)...);
263+
}
264+
265+
template&lt;class E&gt;
266+
void set_error(E&amp;&amp; e) &amp;&amp; noexcept {
267+
<i>op</i>-&gt;set-complete&lt;set_error_t&gt;(std::forward&lt;E&gt;(e));
268+
}
269+
270+
void set_stopped() &amp;&amp; noexcept {
271+
<i>op</i>-&gt;<i>set-complete</i>&lt;set_stopped_t&gt;();
272+
}
273+
274+
env_of_t&lt;Rcvr&gt; get_env() const noexcept {
275+
return <i>op</i>-&gt;<i>rcvr</i>.get_env();
276+
}
277+
};
278+
279+
Rcvr <i>rcvr</i>; // <i>exposition only</i>
280+
281+
union {
282+
StatePtr <i>state</i>; // <i>exposition only</i>
283+
<i>receiver</i> <i>inner</i>; // <i>exposition only</i>
284+
};
285+
286+
union {
287+
<i>stop-callback-t stopCallback</i>; // <i>exposition only</i>
288+
};
289+
290+
<i>future-operation</i>(StatePtr state, Rcvr rcvr) noexcept // <i>exposition only</i>
291+
: <i>rcvr</i>(std::move(rcvr))
292+
{
293+
construct_at(addressof(<i>state</i>), std::move(state));
294+
}
295+
296+
<i>future-operation</i>(<i>future-operation</i>&amp;&amp;) = delete;
297+
298+
~<i>future-operation</i>() {
299+
destroy_at(addressof(<i>state</i>));
300+
}
301+
302+
void <i>run</i>() &amp; noexcept { // <i>exposition only</i>
303+
constexpr bool nothrow =
304+
is_nothrow_constructible_v&lt;<i>stop-callback-t</i>, <i>stop-token-t</i>, <i>callback</i>&gt;;
305+
try {
306+
construct_at(addressof(<i>stopCallback</i>), get_stop_token(<i>rcvr</i>), callback(<i>state</i>.get()));
307+
}
308+
catch (...) {
309+
if constexpr (!nothrow) {
310+
set_error(std::move(<i>rcvr</i>), current_exception());
311+
return;
312+
}
313+
}
314+
315+
auto* state = <i>state</i>.release();
316+
destroy_at(addressof(<i>state</i>));
317+
construct_at(addressof(<i>inner</i>), this);
318+
state-&gt;consume(<i>inner</i>);
319+
}
320+
321+
template&lt;class CPO, class... T&gt;
322+
void <i>set-complete</i>(T&amp;&amp;... ts) noexcept { // <i>exposition only</i>
323+
destroy_at(addressof(<i>stopCallback</i>));
324+
destroy_at(addressof(<i>inner</i>));
325+
construct_at(addressof(<i>state</i>), nullptr);
326+
CPO{}(std::move(<i>rcvr</i>), std::forward&lt;T&gt;(ts)...);
327+
}
328+
};
329+
}
330+
</ins></pre></blockquote>
331+
<p>
332+
-13- The exposition-only class template <tt><i>impls-for</i></tt> (<sref ref="[exec.snd.expos]"/>)
333+
is specialized for `spawn_future_t` as follows:
334+
</p>
335+
<blockquote><pre>
336+
namespace std::execution {
337+
template&lt;&gt;
338+
struct <i>impls-for</i>&lt;spawn_future_t&gt; : <i>default-impls</i> {
339+
static constexpr auto <i>start</i> = <i>see below</i>; // <i>exposition only</i>
340+
<ins>static constexpr auto <i>get-state</i> = <i>see below</i>; // <i>exposition only</i></ins>
341+
};
342+
}
343+
</pre></blockquote>
344+
<p>
345+
-14- The member <tt><i>impls-for</i>&lt;spawn_future_t&gt;::<i>start</i></tt> is initialized with
346+
a callable object equivalent to the following lambda:
347+
</p>
348+
<blockquote><pre>
349+
[](auto&amp; state, auto&amp; <del>rcvr</del>) noexcept -&gt; void {
350+
state<ins>.<i>run</i>()</ins><del>-&gt;<i>consume</i>(rcvr)</del>;
351+
}
352+
</pre></blockquote>
353+
<p>
354+
<ins>-?- The member <tt><i>impls-for</i>&lt;spawn_future_t&gt;::<i>get-state</i></tt> is initialized
355+
with a callable object equivalent to the following lambda:</ins>
356+
</p>
357+
<blockquote><pre>
358+
<ins>[]&lt;class Sndr, class Rcvr&gt;(Sndr&amp;&amp; sndr, Rcvr&amp; rcvr) noexcept {
359+
auto&amp; [_, data] = sndr;
360+
using state_ptr = remove_cvref_t&lt;decltype(data)&gt;;
361+
return <i>future-operation</i>&lt;state_ptr, Rcvr&gt;(std::move(data), std::move(rcvr));
362+
}</ins>
363+
</pre></blockquote>
364+
</blockquote>
365+
</blockquote>
366+
</li>
367+
368+
</ol>
369+
</resolution>
370+
371+
</issue>

0 commit comments

Comments
 (0)