<!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;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}

ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}

pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
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 { display: inline-block; 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; } 
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } 
code span.at { color: #7d9029; } 
code span.bn { color: #40a070; } 
code span.bu { color: #008000; } 
code span.cf { color: #007020; font-weight: bold; } 
code span.ch { color: #4070a0; } 
code span.cn { color: #880000; } 
code span.co { color: #60a0b0; font-style: italic; } 
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } 
code span.do { color: #ba2121; font-style: italic; } 
code span.dt { color: #902000; } 
code span.dv { color: #40a070; } 
code span.er { color: #ff0000; font-weight: bold; } 
code span.ex { } 
code span.fl { color: #40a070; } 
code span.fu { color: #06287e; } 
code span.im { color: #008000; font-weight: bold; } 
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } 
code span.kw { color: #007020; font-weight: bold; } 
code span.op { color: #666666; } 
code span.ot { color: #007020; } 
code span.pp { color: #bc7a00; } 
code span.sc { color: #4070a0; } 
code span.ss { color: #bb6688; } 
code span.st { color: #4070a0; } 
code span.va { color: #19177c; } 
code span.vs { color: #4070a0; } 
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } 
</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;
text-decoration-line: none;
}
div.del, div.del * {
color:#A00000;
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>R2:</b></p>
<ul>
<li><p>Give the <code>on</code> algorithm an unspecified tag type to
discourage its customization. See discussion in <a href="#on-the-customizability-of-the-new-executionon-algorithm">On the
customizability of the new <code>execution::on</code>
algorithm</a>.</p></li>
<li><p>Place strict constraints on <code>on</code> customizations so
that they have the correct semantics.</p></li>
<li><p>Adds a discussion about naming for the <code>start_on</code> and
<code>continue_on</code> algorithms. See the discussion in <a href="#on-the-naming-of-start_on-and-continue_on">On the naming of
<code>start_on</code> and <code>continue_on</code></a>.</p></li>
</ul></li>
<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" tabindex="-1"></a><span class="kw">namespace</span> ex <span class="op">=</span> <span class="bu">std::</span>execution<span class="op">;</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work1 <span class="op">=</span> ex<span class="op">::</span>just<span class="op">()</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>                      <span class="op">|</span> ex<span class="op">::</span>transfer<span class="op">(</span>scheduler_A<span class="op">);</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work2 <span class="op">=</span> ex<span class="op">::</span>on<span class="op">(</span>scheduler_B<span class="op">,</span> <span class="bu">std::</span>move<span class="op">(</span>work1<span class="op">))</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>                      <span class="op">|</span> ex<span class="op">::</span>then<span class="op">([]</span> <span class="op">{</span> <span class="bu">std::</span>puts<span class="op">(</span><span class="st">&quot;hello world!&quot;</span><span class="op">);</span> <span class="op">});</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work3 <span class="op">=</span> ex<span class="op">::</span>on<span class="op">(</span>scheduler_C<span class="op">,</span> <span class="bu">std::</span>move<span class="op">(</span>work2<span class="op">))</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="bu">std::</span>this_thread::sync_wait<span class="op">(</span><span class="bu">std::</span>move<span class="op">(</span>work3<span class="op">));</span></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" tabindex="-1"></a><span class="kw">namespace</span> ex <span class="op">=</span> <span class="bu">std::</span>execution<span class="op">;</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work1 <span class="op">=</span> ex<span class="op">::</span>just<span class="op">()</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>                      <span class="op">|</span> ex<span class="op">::</span>continue_on<span class="op">(</span>scheduler_A<span class="op">);</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work2 <span class="op">=</span> ex<span class="op">::</span>start_on<span class="op">(</span>scheduler_B<span class="op">,</span> <span class="bu">std::</span>move<span class="op">(</span>work1<span class="op">))</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>                      <span class="op">|</span> ex<span class="op">::</span>then<span class="op">([]</span> <span class="op">{</span> <span class="bu">std::</span>puts<span class="op">(</span><span class="st">&quot;hello world!&quot;</span><span class="op">);</span> <span class="op">});</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work3 <span class="op">=</span> ex<span class="op">::</span>start_on<span class="op">(</span>scheduler_C<span class="op">,</span> <span class="bu">std::</span>move<span class="op">(</span>work2<span class="op">))</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="bu">std::</span>this_thread::sync_wait<span class="op">(</span><span class="bu">std::</span>move<span class="op">(</span>work3<span class="op">));</span></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" tabindex="-1"></a><span class="kw">template</span> <span class="op">&lt;</span>ex<span class="op">::</span>scheduler Sched<span class="op">,</span> ex<span class="op">::</span>sender Sndr<span class="op">&gt;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>sender <span class="kw">auto</span> on<span class="op">(</span>Sched sch<span class="op">,</span> Sndr sndr<span class="op">)</span> <span class="op">{</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> ex<span class="op">::</span>read<span class="op">(</span>ex<span class="op">::</span>get_scheduler<span class="op">)</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>       <span class="op">|</span> ex<span class="op">::</span>let_value<span class="op">([=](</span><span class="kw">auto</span> orig_sch<span class="op">)</span> <span class="op">{</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>           <span class="cf">return</span> ex<span class="op">::</span>start_on<span class="op">(</span>sch<span class="op">,</span> sndr<span class="op">)</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>                <span class="op">|</span> ex<span class="op">::</span>continue_on<span class="op">(</span>orig_sch<span class="op">);</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>         <span class="op">});</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></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" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work <span class="op">=</span> async_read_file<span class="op">()</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>                     <span class="op">|</span> ex<span class="op">::</span>on<span class="op">(</span>cpu_pool<span class="op">,</span> ex<span class="op">::</span>then<span class="op">(</span>crunch_numbers<span class="op">))</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>                     <span class="op">|</span> ex<span class="op">::</span>let_value<span class="op">([](</span><span class="kw">auto</span> numbers<span class="op">)</span> <span class="op">{</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>                         <span class="cf">return</span> async_write_file<span class="op">(</span>numbers<span class="op">);</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>                       <span class="op">});</span></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" tabindex="-1"></a>ex<span class="op">::</span>sender <span class="kw">auto</span> work <span class="op">=</span> async_read_file<span class="op">()</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>                     <span class="op">|</span> ex<span class="op">::</span>let_value<span class="op">([=](</span><span class="kw">auto</span> numbers<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>                         ex<span class="op">::</span>sender <span class="kw">auto</span> work <span class="op">=</span> ex<span class="op">::</span>just<span class="op">(</span>numbers<span class="op">)</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>                                              <span class="op">|</span> ex<span class="op">::</span>then<span class="op">(</span>crunch_numbers<span class="op">);</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>                         <span class="cf">return</span> ex<span class="op">::</span>on<span class="op">(</span>cpu_pool<span class="op">,</span> work<span class="op">)</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>                              <span class="op">|</span> ex<span class="op">::</span>let_value<span class="op">([=](</span><span class="kw">auto</span> numbers<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>                                  <span class="cf">return</span> async_write_file<span class="op">(</span>numbers<span class="op">);</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>                                <span class="op">});</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>                       <span class="op">});</span></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.
<b>UPDATE:</b> On 2024-05-21, straw polling indicated that LEWG would
like to see a paper proposing this.</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. <b>UPDATE:</b> On
2024-05-21, LEWG opted to not pursue this option.</p></li>
<li><p>Those algorithms can treat top-level <code>on</code> senders
specially by converting them to <code>start_on</code> senders.
<b>UPDATE:</b> On 2024-05-21, LEWG opted to not pursue this
option.</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. <b>UPDATE:</b> On
2024-05-21, LEWG opted to not pursue this option.</p></li>
</ol>
<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>?
<b>UPDATE:</b> On 2024-05-13, LEWG straw polling answered this question
in the affirmative.</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? <b>UPDATE:</b> On
2024-05-13, LEWG straw polling answered this question in the
affirmative.</p></li>
<li><p>If we want the new scoped form of <code>on</code>, do we want to
add the <code>on(sndr,     sch, continuation)</code> algorithm overload
to permit scoped execution of continuations? <b>UPDATE:</b> On
2024-05-13, LEWG straw polling answered this question in the
affirmative.</p></li>
</ol>
<h2 id="on-the-customizability-of-the-new-executionon-algorithm">On the
customizability of the new <code>execution::on</code> algorithm</h2>
<p>On the 2024-05-21 telecon, LEWG requested to see a revision of this
paper that removes the customizability of the proposed
<code>execution::on</code> algorithm. The author agrees with this
guidance in principle: the behavior of <code>on</code> should be
expressed in terms of <code>start_on</code> and
<code>continue_on</code>, and users should be customizing those
instead.</p>
<p>However, the author now realizes that to ban customization of
<code>on</code> would make it impossible to write a recursive sender
tree transformation without intrusive design changes to P2300. Consider
that the author of an execution domain <code>D</code> might want a
transformation to be applied to every sender in an expression tree. They
would like for this expression:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="bu">std::</span>execution::transform_sender<span class="op">(</span>D<span class="op">(),</span> <span class="bu">std::</span>execution::on<span class="op">(</span>sch<span class="op">,</span> child<span class="op">),</span> env<span class="op">);</span></span></code></pre></div>
<p>to be equivalent to:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode cpp"><code class="sourceCode cpp"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="bu">std::</span>execution::on<span class="op">(</span>sch<span class="op">,</span> <span class="bu">std::</span>execution::transform_sender<span class="op">(</span>D<span class="op">(),</span> child<span class="op">,</span> env<span class="op">));</span></span></code></pre></div>
<p>The ability to crack open a sender, transform the children, and
reassemble the sender is essential for these sorts of recursive
transformations, but that ability <em>also</em> permits other, more
general transformations. The author strongly feels that disallowing
transformations of <code>on</code> would be a step in the wrong
direction.</p>
<p>However, there are a few things we can do to discourage users from
customizing <code>on</code> in ways we disapprove.</p>
<ol type="1">
<li><p>Give the <code>on</code> algorithm an unspecified tag type so
that it is a little awkward to single the <code>on</code> algorithm out
for special treatment by a domain’s
<code>transform_sender</code>.</p></li>
<li><p>Place strict requirements on customizations of <code>on</code> to
ensure correct program semantics in the presence of customizations.
Violations of these requirements would lead to undefined
behavior.</p></li>
</ol>
<p>These changes have been applied as of revision 2 of this paper.</p>
<h2 id="on-the-naming-of-start_on-and-continue_on">On the naming of
<code>start_on</code> and <code>continue_on</code></h2>
<p>It was pointed out in the 2024-05-14 LEWG telecon, and again on
2024-05-21, that the name <code>start_on</code> is potentially confusing
given that “<code>start</code>” in P2300 means “start <em>now</em>.” The
<code>start_on</code> algorithms does not mean “start now”; it means,
“<em>when</em> the work is started, start it <em>there</em>.”</p>
<p>The authors of P2300 make the following suggestion:</p>
<ul>
<li>Rename <code>start_on</code> to <code>starts_on</code>.</li>
<li>Rename <code>continue_on</code> to <code>continues_on</code>.</li>
</ul>
<p>The naming of these algorithms should be determined by LEWG the next
time this paper is discussed.</p>
<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>;
  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 <i>unspecified</i> 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,
let <code>OutSndr</code> be <code>decltype((out_sndr))</code>, and let
<code>Env</code> be <code>decltype((env))</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>(std::forward&lt;Env&gt;(env)));
 } else {
   return 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 orig_sch =
     <em>query-with-default</em>(get_scheduler, env, <em>none-such</em>());

   if constexpr (same_as&lt;decltype(orig_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(orig_sch));
   }
 } else {
   auto&amp;&amp; [sch, closure] = std::forward_like&lt;OutSndr&gt;(data);
   auto orig_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(orig_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>(orig_sch)),
             sch)),
         orig_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>
<p><span class="ednote">The following two paragraphs are new in
R2.<span></p>
<ol start="6" type="1">
<li><p>Let the subexpression <code>out_sndr</code> denote the result of
the invocation <code>on(sch, sndr)</code> or an object copied or moved
from such, let <code>OutSndr</code> be
<code>decltype((out_sndr))</code>, let the subexpression
<code>rcvr</code> denote a receiver such that
<code>sender_to&lt;decltype((out_sndr)), decltype((rcvr))&gt;</code> is
<code>true</code>, and let <code>sch_copy</code> and
<code>sndr_copy</code> be lvalue subexpressions refering to objects
decay-copied from <code>sch</code> and <code>sndr</code>
respectively.</p>
<p>The expression <code>connect(out_sndr, rcvr)</code> has undefined
behavior unless it creates an asynchronous operation as if by calling
<code>connect(S, rcvr)</code>, where <code>S</code> is a sender
expression semantically equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 continue_on(
   start_on(std::forward_like&lt;OutSndr&gt;(sch_copy), std::forward_like&lt;OutSndr&gt;(sndr_copy)),
   orig_sch)
 </pre>
<p>where <code>orig_sch</code> is
<code>get_scheduler(rcvr)</code>.</p></li>
<li><p>Let the subexpression <code>out_sndr2</code> denote the result of
the invocation <code>on(sndr, sch, closure)</code> or an object copied
or moved from such, let <code>OutSndr2</code> be
<code>decltype((out_sndr2))</code>, let the subexpression
<code>rcvr2</code> denote a receiver such that
<code>sender_to&lt;decltype((out_sndr2)), decltype((rcvr2))&gt;</code>
is <code>true</code>, and let <code>sndr_copy</code>,
<code>sch_copy</code>, and <code>closure_copy</code> be lvalue
subexpressions refering to objects decay-copied from <code>sndr</code>,
<code>sch</code>, and <code>closure</code> respectively.</p>
<p>The expression <code>connect(out_sndr2, rcvr2)</code> has undefined
behavior unless it creates an asynchronous operation as if by calling
<code>connect(S2, rcvr2)</code>, where <code>S2</code> is a sender
expression semantically equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 <em>write-env</em>(
   continue_on(
     std::forward_like&lt;OutSndr2&gt;(closure_copy)(
       continue_on(
         <em>write-env</em>(std::forward_like&lt;OutSndr2&gt;(sndr_copy), <em>SCHED-ENV</em>(orig_sch)),
         sch_copy)),
     orig_sch),
   <em>SCHED-ENV</em>(sch_copy))
 </pre>
<p>where <code>orig_sch</code> is an lvalue refering to an object
decay-copied from
<code>get_completion_scheduler&lt;set_value_t&gt;(get_env(sndr_copy))</code>
if that expression is well-formed; otherwise,
<code>get_scheduler(get_env(rcvr2))</code>.</p></li>
</ol>
</blockquote>
</div>
<h2 id="acknowlegments">Acknowlegments</h2>
<p>I’d like to thank my dog, Luna.</p>
</body>
</html>
