<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang xml:lang>
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <!--meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" / -->
  <title>Reconsidering the std::execution::on algorithm</title>
  <style>
    code{white-space: pre-wrap;}
    span.smallcaps{font-variant: small-caps;}
    span.underline{text-decoration: underline;}
    div.column{display: inline-block; vertical-align: top; width: 50%;}
    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
    ul.task-list{list-style: none;}
    pre > code.sourceCode { white-space: pre; position: relative; }
    pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
    pre > code.sourceCode > span:empty { height: 1.2em; }
    code.sourceCode > span { color: inherit; text-decoration: inherit; }
    div.sourceCode { margin: 1em 0; }
    pre.sourceCode { margin: 0; }
    @media screen {
    div.sourceCode { overflow: auto; }
    }
    @media print {
    pre > code.sourceCode { white-space: pre-wrap; }
    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
    }
    pre.numberSource code
      { counter-reset: source-line 0; }
    pre.numberSource code > span
      { position: relative; left: -4em; counter-increment: source-line; }
    pre.numberSource code > span > a:first-child::before
      { content: counter(source-line);
        position: relative; left: -1em; text-align: right; vertical-align: baseline;
        border: none; display: inline-block;
        -webkit-touch-callout: none; -webkit-user-select: none;
        -khtml-user-select: none; -moz-user-select: none;
        -ms-user-select: none; user-select: none;
        padding: 0 4px; width: 4em;
        color: #aaaaaa;
      }
    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
    div.sourceCode
      {   }
    @media screen {
    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
    }
    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
    code span.at { color: #7d9029; } /* Attribute */
    code span.bn { color: #40a070; } /* BaseN */
    code span.bu { } /* BuiltIn */
    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
    code span.ch { color: #4070a0; } /* Char */
    code span.cn { color: #880000; } /* Constant */
    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
    code span.dt { color: #902000; } /* DataType */
    code span.dv { color: #40a070; } /* DecVal */
    code span.er { color: #ff0000; font-weight: bold; } /* Error */
    code span.ex { } /* Extension */
    code span.fl { color: #40a070; } /* Float */
    code span.fu { color: #06287e; } /* Function */
    code span.im { } /* Import */
    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
    code span.op { color: #666666; } /* Operator */
    code span.ot { color: #007020; } /* Other */
    code span.pp { color: #bc7a00; } /* Preprocessor */
    code span.sc { color: #4070a0; } /* SpecialChar */
    code span.ss { color: #bb6688; } /* SpecialString */
    code span.st { color: #4070a0; } /* String */
    code span.va { color: #19177c; } /* Variable */
    code span.vs { color: #4070a0; } /* VerbatimString */
    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  </style>
  <style type="text/css">body { max-width: 800px;
text-align: justify;
margin: auto
}
h1.title {text-align: center}
p {text-align:justify}
p.subtitle {
text-align: center;
font-weight: bold;
font-size: 1.4em;
}
li {text-align:justify}
blockquote.note {
background-color:#E0E0E0;
padding-left: 15px;
padding-right: 15px;
padding-top: 1px;
padding-bottom: 1px;
}
span.note:before {
content: "[Note: ";
font-style: italic;
}
span.note:after {
content: " -- end note]";
font-style: italic;
}
span.ednote:before {
content: "[Editorial note: ";
font-style: italic;
}
span.ednote:after {
content: " -- end note]";
font-style: italic;
}
span.ednote, span.ednote * {
color:blue !important;
margin-top: 0em;
margin-bottom: 0em;
}
ins, ins * {color:#00A000 !important}
del, del * {color:#A00000 !important}
div.ins, div.ins * {
color:#00A000 !important;
text-decoration-line: none;
}
div.del, div.del * {
color:#A00000 !important;
text-decoration-line: none;
}
dfn {
font-style: italic;
font-weight: bold;
}
code:not(sourceCode) {
white-space: normal;
font-size: 80% !important;
}
ins > code:not(sourceCode) {
white-space: normal;
font-size: 80% !important;
}
div.sourceCode {
margin-left: 20pt !important;
}
</style>
  <!--[if lt IE 9]>
    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
  <![endif]-->
</head>
<body>
<header id="title-block-header">
<h1 class="title">Reconsidering the <code>std::execution::on</code> algorithm</h1>
<p class="subtitle"><code>on</code> <em>second thought</em></p>
</header>
<table style="margin-left: auto; margin-right: 0; width: 40%; text-align: left; font-size: 90%;">
<tr>
<td>
<strong>Authors:</strong>
</td>
<td>
<a href="mailto:eric.niebler@gmail.com">Eric Niebler</a>
</td>
</tr>
<tr>
<td>
<strong>Date:</strong>
</td>
<td>
May 14, 2024
</td>
</tr>
<tr>
<td>
<strong>Source:</strong>
</td>
<td>
<a href="https://github.com/ericniebler/wg21/blob/main/P3175/P3175.md">GitHub</a>
</td>
</tr>
<tr>
<td>
<strong>Issue tracking:</strong>
</td>
<td>
<a href="https://github.com/ericniebler/wg21/issues">GitHub</a>
</td>
</tr>
<tr>
<td style=" vertical-align: top;">
<strong>Project:</strong>
</td>
<td>
ISO/IEC JTC1/SC22/WG21 14882:<br />Programming Language — C++
</td>
</tr>
<tr>
<td>
<strong>Audience:</strong>
</td>
<td>
LEWG
</td>
</tr>
</table>
<h2 id="synopsis">Synopsis</h2>
<p>Usage experience with P2300 has revealed a gap between users’ expectations and the actual behavior of the <code>std::execution::on</code> algorithm. This paper seeks to close that gap by making its behavior less surprising.</p>
<h2 id="executive-summary">Executive Summary</h2>
<p>Below are the specific changes this paper proposes:</p>
<ol type="1">
<li><p>Rename the current <code>std::execution::on</code> algorithm to <code>std::execution::start_on</code>.</p></li>
<li><p>Rename <code>std::execution::transfer</code> to <code>std::execution::continue_on</code></p></li>
<li><p>Optional: Add a new algorithm <code>std::execution::on</code> that, like <code>start_on</code>, starts a sender on a particular context, but that remembers where execution is transitioning <em>from</em>. After the sender completes, the <code>on</code> algorithm transitions <em>back</em> to the starting execution context, giving a scoped, there-and-back-again behavior.</p></li>
<li><p>Optional: Add a form of <code>execution::on</code> that lets you run part of a <em>continuation</em> on one scheduler, automatically transitioning back to the starting context.</p></li>
</ol>
<h2 id="revisions">Revisions</h2>
<ul>
<li><p><b>R1:</b></p>
<ul>
<li><p>Makes the <code>write_env</code> adaptor exposition-only, removes the <code>finally</code> and <code>unstoppable</code> adaptors, and reverts the changes to <code>schedule_from</code> and the <code>let_</code> algorithms.</p></li>
<li><p>A follow-on paper, <a href="https://isocpp.org/files/papers/P3284R0.html">P3284</a>, will propose to add <code>write_env</code>, <code>unstoppable</code>, and <code>finally</code> as proper members of the Standard Library.</p></li>
</ul></li>
<li><p><b>R0:</b></p>
<ul>
<li>Initial revision</li>
</ul></li>
</ul>
<h2 id="problem-description">Problem Description</h2>
<p>If, knowing little about senders and sender algorithms, someone showed you code such as the following:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true"></a><span class="kw">namespace</span> ex = <span class="bu">std::</span>execution;</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work1 = ex::just()</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true"></a>                      | ex::transfer(scheduler_A);</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work2 = ex::on(scheduler_B, <span class="bu">std::</span>move(work1))</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true"></a>                      | ex::then([] { <span class="bu">std::</span>puts(<span class="st">&quot;hello world!&quot;</span>); });</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work3 = ex::on(scheduler_C, <span class="bu">std::</span>move(work2))</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true"></a><span class="bu">std::</span>this_thread<span class="bu">::</span>sync_wait(<span class="bu">std::</span>move(work3));</span></code></pre></div>
<p>… and asked you, which scheduler, <code>scheduler_A</code> or <code>scheduler_B</code>, is used to execute the code that prints <code>&quot;hello world!&quot;</code>? You might reasonably think the answer is <code>scheduler_C</code>. Your reasoning would go something like this:</p>
<blockquote>
<p>Well clearly the first thing we execute is <code>on(scheduler_C, work2)</code>. I’m pretty sure that is going to execute <code>work2</code> on <code>scheduler_C</code>. The <code>printf</code> is a part of <code>work2</code>, so I’m going to guess that it executes on <code>scheduler_C</code>.</p>
</blockquote>
<p>This paper exists because the <code>on</code> algorithm as specified in P2300R8 does <em>not</em> print <code>&quot;hello world!&quot;</code> from <code>scheduler_C</code>. It prints it from <code>scheduler_A</code>. Surprise!</p>
<div style="text-align: center">
<strong><em>But why?</em></strong>
</div>
<p><code>work2</code> executes <code>work1</code> on <code>scheduler_B</code>. <code>work1</code> then rather rudely transitions to <code>scheduler_A</code> and doesn’t transition back. The <code>on</code> algorithm is cool with that. It just happily runs its continuation inline, <em>still on <code>scheduler_A</code></em>, which is where <code>&quot;hello world!&quot;</code> is printed from.</p>
<p>If there was more work tacked onto the end of <code>work3</code>, it too would execute on <code>scheduler_A</code>.</p>
<h3 id="user-expectations">User expectations</h3>
<p>The authors of P2300 have witnessed this confusion in the wild. And when this author has asked his programmer friends about the code above, every single one said they expected behavior different from what is specified. This is very concerning.</p>
<p>However, if we change some of the algorithm names, people are less likely to make faulty assumptions about their behavior. Consider the above code with different names:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true"></a><span class="kw">namespace</span> ex = <span class="bu">std::</span>execution;</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work1 = ex::just()</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true"></a>                      | ex::continue_on(scheduler_A);</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work2 = ex::start_on(scheduler_B, <span class="bu">std::</span>move(work1))</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true"></a>                      | ex::then([] { <span class="bu">std::</span>puts(<span class="st">&quot;hello world!&quot;</span>); });</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true"></a></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work3 = ex::start_on(scheduler_C, <span class="bu">std::</span>move(work2))</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true"></a><span class="bu">std::</span>this_thread<span class="bu">::</span>sync_wait(<span class="bu">std::</span>move(work3));</span></code></pre></div>
<p>Now the behavior is a little more clear. The names <code>start_on</code> and <code>continue_on</code> both suggest a one-way execution context transition, which matches their specified behavior.</p>
<h3 id="filling-the-gap">Filling the gap</h3>
<p><code>on</code> fooled people into thinking it was a there-and-back-again algorithm. We propose to fix that by renaming it to <code>start_on</code>. But what of the people who <em>want</em> a there-and-back-again algorithm?</p>
<p>Asynchronous work is better encapsulated when it completes on the same execution context that it started on. People are surprised, and reasonably so, if they <code>co_await</code> a task from a CPU thread pool and get resumed on, say, an OS timer thread. Yikes!</p>
<p>We have an opportunity to give the users of P2300 what they <em>thought</em> they were already getting, and now the right name is available: <strong><code>on</code></strong>.</p>
<p>We propose to add a new algorithm, called <code>on</code>, that remembers where execution came from and automatically transitions back there. Its operational semantics can be easily expressed in terms of the existing P2300 algorithms. It is approximately the following:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true"></a><span class="kw">template</span> &lt;ex::scheduler Sched, ex::sender Sndr&gt;</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true"></a>sender <span class="kw">auto</span> on(Sched sched, Sndr sndr) {</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true"></a>  <span class="cf">return</span> ex::read(ex::get_scheduler)</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true"></a>       | ex::let_value([=](<span class="kw">auto</span> old_sched) {</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true"></a>           <span class="cf">return</span> ex::start_on(sched, sndr)</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true"></a>                | ex::continue_on(old_sched);</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true"></a>         });</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true"></a>}</span></code></pre></div>
<h3 id="one-step-further">One step further?</h3>
<p>Once we recast <code>on</code> as a there-and-back-again algorithm, it opens up the possibility of another there-and-back-again algorithm, one that executes a part of a <em>continuation</em> on a given scheduler. Consider the following code, where <code>async_read_file</code> and <code>async_write_file</code> are functions that return senders (description after the break):</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work = async_read_file()</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true"></a>                     | ex::on(cpu_pool, ex::then(crunch_numbers))</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true"></a>                     | ex::let_value([](<span class="kw">auto</span> numbers) {</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true"></a>                         <span class="cf">return</span> async_write_file(numbers);</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true"></a>                       });</span></code></pre></div>
<p>Here, we read a file and then send it to an <code>on</code> sender. This would be a different overload of <code>on</code>, one that takes a sender, a scheduler, and a continuation. It saves the result of the sender, transitions to the given scheduler, and then forwards the results to the continuation, <code>then(crunch_numbers)</code>. After that, it returns to the previous execution context where it executes the <code>async_write_file(numbers)</code> sender.</p>
<p>The above would be roughly equivalent to:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true"></a>ex::sender <span class="kw">auto</span> work = async_read_file()</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true"></a>                     | ex::let_value([=](<span class="kw">auto</span> numbers) {</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true"></a>                         ex::sender <span class="kw">auto</span> work = ex::just(numbers)</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true"></a>                                              | ex::then(crunch_numbers);</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true"></a>                         <span class="cf">return</span> ex::on(cpu_pool, work)</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true"></a>                              | ex::let_value([=](<span class="kw">auto</span> numbers) {</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true"></a>                                  <span class="cf">return</span> async_write_file(numbers);</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true"></a>                                });</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true"></a>                       });</span></code></pre></div>
<p>This form of <code>on</code> would make it easy to, in the middle of a pipeline, pop over to another execution context to do a bit of work and then automatically pop back when it is done.</p>
<h2 id="implementation-experience">Implementation Experience</h2>
<p>The perennial question: has it been implemented? It has been implemented in stdexec for over a year, modulo the fact that <code>stdexec::on</code> has the behavior as specified in P2300R8, and a new algorithm <code>exec::on</code> has the there-and-back-again behavior proposed in this paper.</p>
<h2 id="design-considerations">Design Considerations</h2>
<h3 id="do-we-really-have-to-rename-the-transfer-algorithm">Do we really have to rename the <code>transfer</code> algorithm?</h3>
<p>We don’t! Within sender expressions, <code>work | transfer(over_there)</code> reads a bit nicer than <code>work | continue_on(over_there)</code>, and taken in isolation the name change is strictly for the worse.</p>
<p>However, the symmetry of the three operations:</p>
<ul>
<li><code>start_on</code></li>
<li><code>continue_on</code></li>
<li><code>on</code></li>
</ul>
<p>… encourages developers to infer their semantics correctly. The first two are one-way transitions before and after a piece of work, respectively; the third book-ends work with transitions. In the author’s opinion, this consideration outweighs the other.</p>
<h3 id="do-we-need-the-additional-form-of-on">Do we need the additional form of <code>on</code>?</h3>
<p>We don’t! Users can build it themselves from the other pieces of P2300 that will ship in C++26. But the extra overload makes it much simpler for developers to write well-behaved asynchronous operations that complete on the same execution contexts they started on, which is why it is included here.</p>
<h3 id="what-happens-if-theres-no-scheduler-for-on-to-go-back-to">What happens if there’s no scheduler for <code>on</code> to go back to?</h3>
<p>If we recast <code>on</code> as a there-and-back-again algorithm, the implication is that the receiver that gets <code>connect</code>-ed to the <code>on</code> sender must know the current scheduler. If it doesn’t, the code will not compile because there is no scheduler to go back to.</p>
<p>Passing an <code>on</code> sender to <code>sync_wait</code> will work because <code>sync_wait</code> provides a <code>run_loop</code> scheduler as the current scheduler. But what about algorithms like <code>start_detached</code> and <code>spawn</code> from <a href="https://wg21.link/P3149">P3149</a>? Those algorithms connect the input sender with a receiver whose environment lacks a value for the <code>get_scheduler</code> query. As specified in this paper, those algorithms will reject <code>on</code> senders, which is bad from a usability point of view.</p>
<p>There are a number of possible solutions to this problem:</p>
<ol type="1">
<li><p>Any algorithm that eagerly <code>connect</code>s a sender should take an environment as an optional extra argument. That way, users have a way to tell the algorithm what the current scheduler is. They can also pass additional information like allocators and stop tokens.</p></li>
<li><p>Those algorithms can specify a so-called “inline” scheduler as the current scheduler, essentially causing the <code>on</code> sender to perform a no-op transition when it completes.</p></li>
<li><p>Those algorithms can treat top-level <code>on</code> senders specially by converting them to <code>start_on</code> senders.</p></li>
<li><p>Those algorithms can set a hidden, non-forwarding “root” query in the environment. The <code>on</code> algorithm can test for this query and, if found, perform a no-op transition when it completes. This has the advantage of not setting a “current” scheduler, which could interfere with the behavior of nested senders.</p></li>
</ol>
<p>The author of this paper likes options (1) and (4) and intends to write a paper proposing both of these changes.</p>
<h2 id="questions-for-lewgs-consideration">Questions for LEWG’s consideration</h2>
<p>The author would like LEWG’s feedback on the following two questions:</p>
<ol type="1">
<li><p>If <code>on</code> is renamed <code>start_on</code>, do we also want to rename <code>transfer</code> to <code>continue_on</code>?</p></li>
<li><p>If <code>on</code> is renamed <code>start_on</code>, do we want to add a new algorithm named <code>on</code> that book-ends a piece of work with transitions to and from a scheduler?</p></li>
<li><p>If we want the new scoped form of <code>on</code>, do we want to add the <code>on(sndr,     sched, continuation)</code> algorithm overload to permit scoped execution of continuations?</p></li>
</ol>
<h2 id="proposed-wording">Proposed Wording</h2>
<p><span class="ednote">The wording in this section is based on <a href="https://wg21.link/P2300R9">P2300R9</a> with the addition of <a href="https://wg21.link/P2855R1">P8255R1</a>.</span></p>
<p><span class="ednote">Change [exec.syn] as follows:</span></p>
<blockquote>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
...

  struct <ins>start_</ins>on_t;
  struct <del>transfer_t</del><ins>continue_on_t</ins>;
  <ins>struct on_t;</ins>
  struct schedule_from_t;
...

  inline constexpr <ins>start_</ins>on_t <ins>start_</ins>on{};</ins>
  inline constexpr <del>transfer_t transfer</del><ins>continue_on_t continue_on</ins>{};
  <ins>inline constexpr on_t on{};</ins>
  inline constexpr schedule_from_t schedule_from{};
</pre>
</blockquote>
<p><span class="ednote">Add a new paragraph (15) to section [exec.snd.general], paragraph 3 as follows:</span></p>
<div class="ins">
<blockquote>
<ol start="15" type="1">
<li><pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
template&lt;sender Sndr, queryable Env&gt;
constexpr auto <i>write-env</i>(Sndr&amp;&amp; sndr, Env&amp;&amp; env); // exposition only
</pre>
<ol type="1">
<li><p><i><code>write-env</code></i> is an exposition-only sender adaptor that, when connected with a receiver <code>rcvr</code>, connects the adapted sender with a receiver whose execution environment is the result of joining the <code>queryable</code> argument <code>env</code> to the result of <code>get_env(rcvr)</code>.</p></li>
<li><p>Let <i><code>write-env-t</code></i> be an exposition-only empty class type.</p></li>
<li><p><em>Returns:</em> <code><i>make-sender</i>(<i>make-env-t</i>(), std::forward&lt;Env&gt;(env), std::forward&lt;Sndr&gt;(sndr))</code>.</p></li>
<li><p><em>Remarks:</em> The exposition-only class template <i><code>impls-for</code></i> ([exec.snd.general]) is specialized for <i><code>write-env-t</code></i> as follows:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 template&lt;&gt;
 struct <i>impls-for</i>&lt;<i>write-env-t</i>&gt; : <i>default-impls</i> {
   static constexpr auto <em>get-env</em> =
     [](auto, const auto&amp; state, const auto&amp; rcvr) noexcept {
       return <em>JOIN-ENV</em>(state, get_env(rcvr));
     };
 };
 </pre></li>
</ol></li>
</ol>
</blockquote>
</div>
<p><span class="ednote">Change subsection “<code>execution::on</code> [exec.on]” to “<code>execution::start_on</code> [exec.start.on]”, and within that subsection, replace every instance of “<code>on</code>” with “<code>start_on</code>” and every instance of “<code>on_t</code>” with “<code>start_on_t</code>”.</span></p>
<p><span class="ednote">Change subsection “<code>execution::transfer</code> [exec.transfer]” to “<code>execution::continue_on</code> [exec.complete.on]”, and within that subsection, replace every instance of “<code>transfer</code>” with “<code>continue_on</code>” and every instance of “<code>transfer_t</code>” with “<code>continue_on_t</code>”.</span></p>
<p><span class="ednote">Insert a new subsection “<code>execution::on</code> [exec.on]” as follows:</span></p>
<div class="ins">
<blockquote>
<h4 id="executionon-exec.on"><code>execution::on</code> <b>[exec.on]</b></h4>
<ol type="1">
<li><p>The <code>on</code> sender adaptor has two forms:</p>
<ul>
<li><p>one that starts a sender <code>sndr</code> on an execution agent belonging to a particular scheduler’s associated execution resource and that restores execution to the starting execution resource when the sender completes, and</p></li>
<li><p>one that, upon completion of a sender <code>sndr</code>, transfers execution to an execution agent belonging to a particular scheduler’s associated execution resource, then executes a sender adaptor closure with the async results of the sender, and that then transfers execution back to the execution resource <code>sndr</code> completed on.</p></li>
</ul></li>
<li><p>The name <code>on</code> denotes a customization point object. For some subexpressions <code>sch</code> and <code>sndr</code>, if <code>decltype((sch))</code> does not satisfy <code>scheduler</code>, or <code>decltype((sndr))</code> does not satisfy <code>sender</code>, <code>on(sch, sndr)</code> is ill-formed.</p></li>
<li><p>Otherwise, the expression <code>on(sch, sndr)</code> is expression-equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 transform_sender(
   <em>query-or-default</em>(get_domain, sch, default_domain()),
   <em>make-sender</em>(on, sch, sndr));
 </pre></li>
<li><p>For a subexpression <code>closure</code>, if <code>decltype((closure))</code> is not a sender adaptor closure object ([exec.adapt.objects]), the expression <code>on(sndr, sch,     closure)</code> is ill-formed; otherwise, it is expression-equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 transform_sender(
   <em>get-domain-early</em>(sndr),
   <em>make-sender</em>(on, pair{sch, closure}, sndr));
 </pre></li>
<li><p>Let <code>out_sndr</code> and <code>env</code> be subexpressions such that <code>OutSndr</code> is <code>decltype((out_sndr))</code>. If <code><em>sender-for</em>&lt;OutSndr, on_t&gt;</code> is <code>false</code>, then the expressions <code>on.transform_env(out_sndr,     env)</code> and <code>on.transform_sender(out_sndr, env)</code> are ill-formed; otherwise:</p>
<ol type="1">
<li><p>Let <em><code>none-such</code></em> be an unspecified empty class type, and let <em><code>not-a-sender</code></em> be the exposition-only type:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 struct <em>not-a-sender</em> {
   using sender_concept = sender_t;

   auto get_completion_signatures(auto&amp;&amp;) const {
     return <em>see below</em>;
   }
 };
 </pre>
<p>… where the member function <code>get_completion_signatures</code> returns an object of a type that is not a specialization of the <code>completion_signatures</code> class template.</p></li>
<li><p><code>on.transform_env(out_sndr, env)</code> is equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 auto&amp;&amp; [ign1, data, ign2] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)&gt;) {
   return <em>JOIN-ENV</em>(<em>SCHED-ENV</em>(data), <em>FWD-ENV</em>(env));
 } else {
   using Env = decltype((env));
   return static_cast&lt;remove_rvalue_reference_t&lt;Env&gt;&gt;(std::forward&lt;Env&gt;(env));
 }
 </pre></li>
<li><p><code>on.transform_sender(out_sndr, env)</code> is equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 auto&amp;&amp; [ign, data, sndr] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)&gt;) {
   auto old_sch =
     <em>query-with-default</em>(get_scheduler, env, <em>none-such</em>());

   if constexpr (same_as&lt;decltype(old_sch), <em>none-such</em>&gt;) {
     return <em>not-a-sender</em>{};
   } else {
     return continue_on(
       start_on(std::forward_like&lt;OutSndr&gt;(data), std::forward_like&lt;OutSndr&gt;(sndr)),
       std::move(old_sch));
   }
 } else {
   auto&amp;&amp; [sch, closure] = std::forward_like&lt;OutSndr&gt;(data);
   auto old_sch = <em>query-with-default</em>(
     get_completion_scheduler&lt;set_value_t&gt;,
     get_env(sndr),
     <em>query-with-default</em>(get_scheduler, env, <em>none-such</em>()));

   if constexpr (same_as&lt;decltype(old_sch), <em>none-such</em>&gt;) {
     return <em>not-a-sender</em>{};
   } else {
     return <em>write-env</em>(
       continue_on(
         std::forward_like&lt;OutSndr&gt;(closure)(
           continue_on(
             <em>write-env</em>(std::forward_like&lt;OutSndr&gt;(sndr), <em>SCHED-ENV</em>(old_sch)),
             sch)),
         old_sch),
       <em>SCHED-ENV</em>(sch));
   }
 }
 </pre></li>
<li><p><em>Recommended practice:</em> Implementations should use the return type of <code><em>not-a-sender</em>::get_completion_signatures</code> to inform users that their usage of <code>on</code> is incorrect because there is no available scheduler onto which to restore execution.</p></li>
</ol></li>
</ol>
</blockquote>
</div>
<h2 id="acknowlegments">Acknowlegments</h2>
<p>I’d like to thank my dog, Luna.</p>
</body>
</html>
