|
| 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<> |
| 44 | + struct <i>impls-for</i><spawn_future_t> : <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><spawn_future_t>::<i>start</i></tt> is initialized with a callable object |
| 51 | +equivalent to the following lambda: |
| 52 | +</p> |
| 53 | +<blockquote><pre> |
| 54 | +[](auto& state, auto& rcvr) noexcept -> void { |
| 55 | + state-><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><spawn_future_t>::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 | +[…] |
| 90 | +<p/> |
| 91 | +If any of <tt>sender<Sndr></tt>, <tt>scope_token<Token></tt>, or <tt><i>queryable</i><Env></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: […] |
| 105 | +</p> |
| 106 | +<blockquote><pre> |
| 107 | +namespace std::execution { |
| 108 | + template<class Completions> |
| 109 | + struct <i>spawn-future-state-base</i>; // <i>exposition only</i> |
| 110 | + |
| 111 | + template<class... Sigs> |
| 112 | + struct <i>spawn-future-state-base</i><completion_signatures<Sigs...>> <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 | +[…] |
| 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<class Alloc, scope_token Token, sender Sender, class Env> |
| 128 | + struct <i>spawn-future-state</i> // <i>exposition only</i> |
| 129 | + : <i>spawn-future-state-base</i><completion_signatures_of_t<<i>future-spawned-sender</i><Sender, Env>>> { |
| 130 | + […] |
| 131 | + void <i>complete</i>() noexcept override; // <i>exposition only</i> |
| 132 | + void <i>consume</i>(receiver auto& 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 | + […] |
| 136 | + }; |
| 137 | + […] |
| 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) — 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) — 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) — otherwise, <tt><i>destroy</i></tt> is invoked.</p></li> |
| 161 | +</ul> |
| 162 | +</blockquote> |
| 163 | +<pre> |
| 164 | +void <i>consume</i>(receiver auto& 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) — 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.?) — 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) — otherwise, `rcvr` is completed as if by: […]</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) — 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) — 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) — otherwise,</ins></p> |
| 195 | +<ul style="list-style-type: none"> |
| 196 | +<li><p><ins>(?.3.1) — invokes <tt><i>ssource</i>.request_stop()</tt>, and</ins></p></li> |
| 197 | +<li><p><ins>(?.3.2) — 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. — |
| 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 | +[…] |
| 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-><i>associated</i>); |
| 228 | +{ |
| 229 | + using traits = allocator_traits<Alloc>::template rebind_traits<<i>spawn-future-state</i>>; |
| 230 | + typename traits::allocator_type alloc(std::move(this-><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<class StatePtr, class Rcvr> |
| 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>-><i>try-cancel</i>(); |
| 247 | + }; |
| 248 | + }; |
| 249 | + |
| 250 | + using <i>stop-token-t</i> = // <i>exposition only</i> |
| 251 | + stop_token_of_t<env_of_t<Rcvr>>; |
| 252 | + |
| 253 | + using <i>stop-callback-t</i> = // <i>exposition only</i> |
| 254 | + stop_callback_for_t<stop-token-t, callback>; |
| 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<class... T> |
| 261 | + void set_value(T&&... ts) && noexcept { |
| 262 | + <i>op</i>-><i>set-complete</i><set_value_t>(std::forward<T>(ts)...); |
| 263 | + } |
| 264 | + |
| 265 | + template<class E> |
| 266 | + void set_error(E&& e) && noexcept { |
| 267 | + <i>op</i>->set-complete<set_error_t>(std::forward<E>(e)); |
| 268 | + } |
| 269 | + |
| 270 | + void set_stopped() && noexcept { |
| 271 | + <i>op</i>-><i>set-complete</i><set_stopped_t>(); |
| 272 | + } |
| 273 | + |
| 274 | + env_of_t<Rcvr> get_env() const noexcept { |
| 275 | + return <i>op</i>-><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>&&) = delete; |
| 297 | + |
| 298 | + ~<i>future-operation</i>() { |
| 299 | + destroy_at(addressof(<i>state</i>)); |
| 300 | + } |
| 301 | + |
| 302 | + void <i>run</i>() & noexcept { // <i>exposition only</i> |
| 303 | + constexpr bool nothrow = |
| 304 | + is_nothrow_constructible_v<<i>stop-callback-t</i>, <i>stop-token-t</i>, <i>callback</i>>; |
| 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->consume(<i>inner</i>); |
| 319 | + } |
| 320 | + |
| 321 | + template<class CPO, class... T> |
| 322 | + void <i>set-complete</i>(T&&... 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<T>(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<> |
| 338 | + struct <i>impls-for</i><spawn_future_t> : <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><spawn_future_t>::<i>start</i></tt> is initialized with |
| 346 | +a callable object equivalent to the following lambda: |
| 347 | +</p> |
| 348 | +<blockquote><pre> |
| 349 | +[](auto& state, auto& <del>rcvr</del>) noexcept -> void { |
| 350 | + state<ins>.<i>run</i>()</ins><del>-><i>consume</i>(rcvr)</del>; |
| 351 | +} |
| 352 | +</pre></blockquote> |
| 353 | +<p> |
| 354 | +<ins>-?- The member <tt><i>impls-for</i><spawn_future_t>::<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>[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept { |
| 359 | + auto& [_, data] = sndr; |
| 360 | + using state_ptr = remove_cvref_t<decltype(data)>; |
| 361 | + return <i>future-operation</i><state_ptr, Rcvr>(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