<!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>
    html {
      color: #1a1a1a;
      background-color: #fdfdfd;
    }
    body {
      margin: 0 auto;
      max-width: 36em;
      padding-left: 50px;
      padding-right: 50px;
      padding-top: 50px;
      padding-bottom: 50px;
      hyphens: auto;
      overflow-wrap: break-word;
      text-rendering: optimizeLegibility;
      font-kerning: normal;
    }
    @media (max-width: 600px) {
      body {
        font-size: 0.9em;
        padding: 12px;
      }
      h1 {
        font-size: 1.8em;
      }
    }
    @media print {
      html {
        background-color: white;
      }
      body {
        background-color: transparent;
        color: black;
        font-size: 12pt;
      }
      p, h2, h3 {
        orphans: 3;
        widows: 3;
      }
      h2, h3, h4 {
        page-break-after: avoid;
      }
    }
    p {
      margin: 1em 0;
    }
    a {
      color: #1a1a1a;
    }
    a:visited {
      color: #1a1a1a;
    }
    img {
      max-width: 100%;
    }
    h1, h2, h3, h4, h5, h6 {
      margin-top: 1.4em;
    }
    h5, h6 {
      font-size: 1em;
      font-style: italic;
    }
    h6 {
      font-weight: normal;
    }
    ol, ul {
      padding-left: 1.7em;
      margin-top: 1em;
    }
    li > ol, li > ul {
      margin-top: 0;
    }
    blockquote {
      margin: 1em 0 1em 1.7em;
      padding-left: 1em;
      border-left: 2px solid #e6e6e6;
      color: #606060;
    }
    code {
      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
      font-size: 85%;
      margin: 0;
      hyphens: manual;
    }
    pre {
      margin: 1em 0;
      overflow: auto;
    }
    pre code {
      padding: 0;
      overflow: visible;
      overflow-wrap: normal;
    }
    .sourceCode {
     background-color: transparent;
     overflow: visible;
    }
    hr {
      background-color: #1a1a1a;
      border: none;
      height: 1px;
      margin: 1em 0;
    }
    table {
      margin: 1em 0;
      border-collapse: collapse;
      width: 100%;
      overflow-x: auto;
      display: block;
      font-variant-numeric: lining-nums tabular-nums;
    }
    table caption {
      margin-bottom: 0.75em;
    }
    tbody {
      margin-top: 0.5em;
      border-top: 1px solid #1a1a1a;
      border-bottom: 1px solid #1a1a1a;
    }
    th {
      border-top: 1px solid #1a1a1a;
      padding: 0.25em 0.5em 0.25em 0.5em;
    }
    td {
      padding: 0.125em 0.5em 0.25em 0.5em;
    }
    header {
      margin-bottom: 4em;
      text-align: center;
    }
    #TOC li {
      list-style: none;
    }
    #TOC ul {
      padding-left: 1.3em;
    }
    #TOC > ul {
      padding-left: 0;
    }
    #TOC a:not(:hover) {
      text-decoration: none;
    }
    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;}
    /* The extra [class] is a hack that increases specificity enough to
       override a similar rule in reveal.js */
    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;}
    /* CSS for syntax highlighting */
    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; }
    .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 { 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 { color: #008000; } /* 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 { color: #008000; font-weight: bold; } /* 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>
  <!--[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>
<style>
body { min-width: 50% !important; }
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>
<div>
<dl>
<dt>
<strong>Authors:</strong>
</dt>
<dd>
<a href="mailto:eric.niebler@gmail.com">Eric Niebler</a>
</dd>
<dt>
<strong>Date:</strong>
</dt>
<dd>
March 14, 2024
</dd>
<dt>
<strong>Source:</strong>
</dt>
<dd>
<a href="https://github.com/ericniebler/wg21/blob/main/P3175/P3175R0.md">GitHub</a>
</dd>
<dt>
<strong>Issue tracking:</strong>
</dt>
<dd>
<a href="https://github.com/ericniebler/wg21/issues">GitHub</a>
</dd>
<dt>
<strong>Project:</strong>
</dt>
<dd>
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
</dd>
<dt>
<strong>Audience:</strong>
</dt>
<dd>
LEWG
</dd>
</dl>
</div>
<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. (Alternative: don’t add a new
scoped <code>on</code> algorithm.)</p></li>
<li><p>Optional: Add a new uncustomizable adaptor <code>write_env</code>
for writing values into the receiver’s execution environment, and rename
<code>read</code> to <code>read_env</code> (“<code>read</code>” being
too vague and something of a land-grab). <code>write_env</code> is used
in the implementation of the new <code>on</code> algorithm and can
simplify the specification of the <code>let_</code> algorithms.
(Alternative: make <code>write_env</code> exposition-only.)</p></li>
<li><p>Optional: Add an uncustomizable <code>unstoppable</code> adaptor
that is a trivial application of <code>write_env</code>: it sets the
current stop token in the receiver’s environment to a
<code>never_stop_token</code>. <code>unstoppable</code> is used in the
re-specification of the <code>schedule_from</code> algorithm.
(Alternative: make <code>unstoppable</code> exposition-only.)</p></li>
<li><p>Optional: Generalize the specification for
<code>schedule_from</code> to take two senders instead of a sender and a
scheduler, name it <code>finally</code>, and make it uncustomizable.
Specify the default implementation of
<code>schedule_from(sch,     snd)</code> as
<code>finally(snd, unstoppable(schedule(sch)))</code>. (Alternative:
keep <code>finally</code> exposition-only.)</p></li>
<li><p>Optional: Add a form of <code>execution::on</code> that lets you
run part of a continuation on one scheduler, automatically transitioning
back to the starting context.</p></li>
</ol>
<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<span class="bu">::</span>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>"hello world!"</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>"hello world!"</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>"hello world!"</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<span class="bu">::</span>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 sched<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> old_sched<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>sched<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>old_sched<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.</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 will be
writing 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>
<li><p>Do we want to make the <code>write_env</code> adaptor
exposition-only, or make it public?</p></li>
<li><p>Do we want to make the <code>unstoppable</code> adaptor
exposition-only, or make it public?</p></li>
<li><p>Do we want to make the <code>finally</code> algorithm an
exposition-only detail of the <code>schedule_from</code> algorithm, or
make it public?</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/P2300R8">P2300R8</a> with the addition of <a
href="https://wg21.link/P2855R1">P8255R1</a>.</span></p>
<p>Change [exec.syn] as follows:</p>
<blockquote>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
  inline constexpr <em>unspecified</em> read<ins>_env</ins>{};
...

  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;
...

  <ins>inline constexpr <em>unspecified</em> write_env{};</ins>
  <ins>inline constexpr <em>unspecified</em> unstoppable{};</ins>
  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>
  <ins>inline constexpr <em>unspecified</em> finally{};</ins>
  inline constexpr schedule_from_t schedule_from{};
</pre>
</blockquote>
<p>Change subsection “<code>execution::read</code> [exec.read]” to
“<code>execution::read_env</code> [exec.read.env]”, and within that
subsection, replace every instance of “<code>read</code>” with
“<code>read_env</code>”.</p>
<p>After [exec.adapt.objects], add a new subsection
“<code>execution::write_env</code> [exec.write.env]” as follows:</p>
<div class="ins">
<blockquote>
<h4
id="executionwrite_env-exec.write.env"><code>execution::write_env</code>
<b>[exec.write.env]</b></h4>
<ol type="1">
<li><p><code>write_env</code> is a sender adaptor that connects its
inner sender with a receiver that has the execution environment of the
outer receiver joined with a specified execution environment.</p></li>
<li><p><code>write_env</code> is a customization point object. For some
subexpressions <code>sndr</code> and <code>env</code>, if
<code>decltype((sndr))</code> does not satisfy <code>sender</code> or if
<code>decltype((env))</code> does not satisfy <code>queryable</code>,
the expression <code>write_env(sndr, env)</code> is ill-formed.
Otherwise, it is expression-equivalent to
<code><i>make-sender</i>(write_env, env, sndr)</code>.</p></li>
<li><p>The exposition-only class template <code><i>impls-for</i></code>
([exec.snd.general]) is specialized for <code>write_env</code> as
follows:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 template&lt;>
 struct <i>impls-for</i>&lt;tag_t&lt;write_env>> : <i>default-impls</i> {
   static constexpr auto <em>get-env</em> =
     [](auto, const auto& state, const auto& rcvr) noexcept {
       return <em>JOIN-ENV</em>(state, get_env(rcvr));
     };
 };
 </pre></li>
</ol>
</blockquote>
</div>
<p>After [exec.write.env], add a new subsection
“<code>execution::unstoppable</code> [exec.unstoppable]” as follows:</p>
<div class="ins">
<blockquote>
<h4
id="executionunstoppable-exec.unstoppable"><code>execution::unstoppable</code>
<b>[exec.unstoppable]</b></h4>
<ol type="1">
<li><p><code>unstoppable</code> is a sender adaptor that connects its
inner sender with a receiver that has the execution environment of the
outer receiver but with a <code>never_stop_token</code> as the value of
the <code>get_stop_token</code> query.</p></li>
<li><p>For a subexpression <code>sndr</code>,
<code>unstoppable(sndr)</code> is expression equivalent to
<code>write_env(sndr, <em>MAKE-ENV</em>(get_stop_token,
never_stop_token{}))</code>.</p></li>
</ol>
</blockquote>
</div>
<p>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>”.</p>
<p>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>”.</p>
<p>Change subsection “<code>execution::schedule_from</code>
[exec.schedule.from]” to “<code>execution::finally</code>
[exec.finally]”, change every instance of “<code>schedule_from</code>”
to “<code>finally</code>” and “<code>schedule_from_t</code>” to
“<code>tag_t&lt;finally&gt;</code>”, and change the subsection as
follows:</p>
<blockquote>
<h4 id="executionfinally-exec.finally"><code>execution::finally</code>
<b>[exec.finally]</b></h4>
<p><span class="ednote">Replace paragraphs 1-3 with the
following:</span></p>
<div class="ins">
<ol type="1">
<li><p><code>finally</code> is a sender adaptor that starts one sender
unconditionally after another sender completes. If the second sender
completes successfully, the <code>finally</code> sender completes with
the async results of the first sender. If the second sender completes
with error or stopped, the async results of the first sender are
discarded, and the <code>finally</code> sender completes with the async
results of the second sender. <span class="note">It is similar in spirit
to the <code>try</code>/<code>finally</code> control structure of some
languages.</span></p></li>
<li><p>The name <code>finally</code> denotes a customization point
object. For some subexpressions <code>try_sndr</code> and
<code>finally_sndr</code>, if <code>try_sndr</code> or
<code>finally_sndr</code> do not satisfy <code>sender</code>, the
expression <code>finally(try_sndr,     finally_sndr)</code> is
ill-formed; otherwise, it is expression-equivalent to
<code><i>make-sender</i>(finally, {}, try_sndr,
finally_sndr)</code>.</p></li>
<li><p>Let <code>CS</code> be a specialization of
<code>completion_signatures</code> whose template parameters are the
pack <code>Sigs</code>. Let <code><em>VALID-FINALLY</em>(CS)</code> be
<code>true</code> if and only if there is no type in <code>Sigs</code>
of the form <code>set_value_t(Ts...)</code> for which
<code>sizeof...(Ts)</code> is greater than <code>0</code>. Let
<code>F</code> be <code>decltype((finally_sndr))</code>. If
<code>sender_in&lt;F&gt;</code> is <code>true</code> and
<code><em>VALID-FINALLY</em>(completion_signatures_of_t&lt;F&gt;)</code>
is <code>false</code>, the program is ill-formed.</p></li>
</ol>
</div>
<ol start="4" type="1">
<li><p>The exposition-only class template <code><i>impls-for</i></code>
([exec.snd.general]) is specialized for <code>finally</code> as
follows:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
   template&lt;>
   struct <i>impls-for</i>&lt;tag_t&lt;finally>> : <i>default-impls</i> {
     static constexpr auto <i>get-attrs</i> = <i>see below</i>;
     static constexpr auto <i>get-state</i> = <i>see below</i>;
     static constexpr auto <i>complete</i> = <i>see below</i>;
   };
   </pre>
<ol type="1">
<li><p>The member
<code><i>impls-for</i>&lt;tag_t&lt;finally&gt;&gt;::<i>get-attrs</i></code>
is initialized with a callable object equivalent to the following
lambda:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;"><div class="del">
   [](const auto& data, const auto& child) noexcept -> decltype(auto) {
     return <i>JOIN-ENV</i>(<i>SCHED-ATTRS</i>(data), <i>FWD-ENV</i>(get_env(child)));
   }</div><div class="ins">
   [](auto, const auto& tsndr, const auto& fsndr) noexcept -> decltype(auto) {
     return <i>JOIN-ENV</i>(<i>FWD-ENV</i>(get_env(fsndr)), <i>FWD-ENV</i>(get_env(tsndr)));
   }</div></pre></li>
<li><p>The member
<code><i>impls-for</i>&lt;tag_t&lt;finally&gt;&gt;::<i>get-state</i></code>
is initialized with a callable object equivalent to the following
lambda:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
   []&lt;class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr)
       requires sender_in&lt;<i>child-type</i>&lt;Sndr<ins>, 0</ins>>, env_of_t&lt;Rcvr>> <ins>&&</ins>
         <ins>sender_in&lt;<i>child-type</i>&lt;Sndr<ins>, 1</ins>>, env_of_t&lt;Rcvr>> &&</ins>
         <ins><em>VALID-FINALLY</em>(completion_signatures_of_t&lt;<i>child-type</i>&lt;Sndr<ins>, 1</ins>>, env_of_t&lt;Rcvr>>)</ins> {
     return apply(
       <del>[&]&lt;class Sch, class Child>(auto, Sch sch, Child&& child)</del>
       <ins>[&]&lt;class TSndr, class FSndr>(auto, auto, TSndr&& tsndr, FSndr&& fsndr)</ins> {
         using <i>variant-type</i> = <i>see below</i>;
         using <i>receiver-type</i> = <i>see below</i>;
         using <i>operation-type</i> = connect_result_t&lt;<del>schedule_result_t&lt;Sch></del><ins>FSndr</ins>, <i>receiver-type</i>>;

         struct <i>state-type</i> {
           Rcvr& <i>rcvr</i>;
           <i>variant-type</i> <i>async-result</i>;
           <i>operation-type</i> <i>op-state</i>;

           explicit <i>state-type</i>(<del>Sch sch</del><ins>FSndr&& fsndr</ins>, Rcvr& rcvr)
             : <i>rcvr</i>(rcvr)
             , <i>op-state</i>(connect(<del>schedule(sch)</del><ins>std::forward&lt;FSndr>(fsndr)</ins>, 
                                <i>receiver-type</i>{<i></i>{}, this})) {}
         };

         return <i>state-type</i>{<del>sch</del><ins>std::forward&lt;FSndr>(fsndr)</ins>, rcvr};
       },
       std::forward&lt;Sndr>(sndr));
   }
   </pre>
<ol type="1">
<li><p>The local class <i><code>state-type</code></i> is a structural
type.</p></li>
<li><p>Let <code>Sigs</code> be a pack of the arguments to the
<code>completion_signatures</code> specialization named by
<code>completion_signatures_of_t&lt;<del>Child</del><ins>TSndr</ins>,
env_of_t&lt;Rcvr&gt;&gt;</code>. Let <i><code>as-tuple</code></i> be an
alias template that transforms a completion signature
<code>Tag(Args...)</code> into the <code>tuple</code> specialization
<code><i>decayed-tuple</i>&lt;Tag, Args<code>...</code>&gt;</code>. Then
<i><code>variant-type</code></i> denotes the type
<code>variant&lt;monostate,
<i>as-tuple</i>&lt;Sigs&gt;<code>...</code>&gt;</code>, except with
duplicate types removed.</p></li>
<li><p>Let <i><code>receiver-type</code></i> denote the following
class:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 struct <i>receiver-type</i> : receiver_adaptor&lt;<i>receiver-type</i>> {
   <i>state-type</i>* <i>state</i>; <em>// exposition only</em>

   Rcvr&& base() && noexcept { return std::move(<i>state</i>-><i>rcvr</i>); }
   const Rcvr& base() const & noexcept { return <i>state</i>-><i>rcvr</i>; }

   void set_value() && noexcept {
     visit(
       [this]&lt;class Tuple>(Tuple& result) noexcept -> void {
         if constexpr (!same_as&lt;monostate, Tuple>) {
           auto& [tag, ...args] = result;
           tag(std::move(<i>state</i>-><i>rcvr</i>), std::move(args)...);
         }
       },
       <i>state</i>-><i>async-result</i>);
   }
 };
 </pre></li>
</ol></li>
<li><p>The member
<code><i>impls-for</i>&lt;tag_t&lt;finally&gt;&gt;::<i>complete</i></code>
is initialized with a callable object equivalent to the following
lambda:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 []&lt;class Tag, class... Args>(auto, auto& state, auto& rcvr, Tag, Args&&... args) noexcept -> void {
   using result_t = <i>decayed-tuple</i>&lt;Tag, Args...>;
   constexpr bool nothrow = is_nothrow_constructible_v&lt;result_t, Tag, Args...>;

   <i>TRY-EVAL</i>(std::move(rcvr), [&]() noexcept(nothrow) {
     state.<i>async-result</i>.template emplace&lt;result_t>(Tag(), std::forward&lt;Args>(args)...);
   }());

   if (state.<i>async-result</i>.valueless_by_exception())
     return;
   if (state.<i>async-result</i>.index() == 0)
     return;

   start(state.<i>op-state</i>);
 };
 </pre></li>
</ol></li>
</ol>
<p><span class="ednote">Remove paragraph 5, which is about the
requirements on customizations of the algorithm; <code>finally</code>
cannot be customized.</span></p>
</blockquote>
<p>Insert a new subsection “<code>execution::schedule_from</code>
[exec.schedule.from]” as follows:</p>
<blockquote>
<h4
id="executionschedule_from-exec.schedule.from"><code>execution::schedule_from</code>
<b>[exec.schedule.from]</b></h4>
<p><span class="ednote">These three paragraphs are taken unchanged from
P2300R8.</span></p>
<ol type="1">
<li><p><code>schedule_from</code> schedules work dependent on the
completion of a sender onto a scheduler’s associated execution resource.
<span class="wg21note"><code>schedule_from</code> is not meant to be
used in user code; it is used in the implementation of
<code>transfer</code>.</span></p></li>
<li><p>The name <code>schedule_from</code> denotes a customization point
object. For some subexpressions <code>sch</code> and <code>sndr</code>,
let <code>Sch</code> be <code>decltype((sch))</code> and
<code>Sndr</code> be <code>decltype((sndr))</code>. If <code>Sch</code>
does not satisfy <code>scheduler</code>, or <code>Sndr</code> does not
satisfy <code>sender</code>, <code>schedule_from</code> is
ill-formed.</p></li>
<li><p>Otherwise, the expression <code>schedule_from(sch, sndr)</code>
is expression-equivalent to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 transform_sender(
   <i>query-or-default</i>(get_domain, sch, default_domain()),
   <i>make-sender</i>(schedule_from, sch, sndr));
 </pre></li>
</ol>
<div class="ins">
<ol start="4" type="1">
<li><p>The exposition-only class template <code><i>impls-for</i></code>
is specialized for <code>schedule_from_t</code> as follows:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 template&lt;>
 struct <i>impls-for</i>&lt;schedule_from_t> : <i>default-impls</i> {
   static constexpr auto get_attrs =
     [](const auto& data, const auto& child) noexcept -> decltype(auto) {
       return <i>JOIN-ENV</i>(<i>SCHED-ATTRS</i>(data), <i>FWD-ENV</i>(get_env(child)));
     };
 };
 </pre></li>
<li><p>Let <code>sndr</code> and <code>env</code> be subexpressions such
that <code>Sndr</code> is <code>decltype((sndr))</code>. If
<code><i>sender-for</i>&lt;Sndr, schedule_from_t&gt;</code> is
<code>false</code>, then the expression
<code>schedule_from.transform_sender(sndr, env)</code> is ill-formed;
otherwise, it is equal to:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 auto&& [tag, sch, child] = sndr;
 return finally(std::forward_like&lt;Sndr>(child),
                unstoppable(schedule(std::forward_like&lt;Sndr>(sch))));
 </pre>
<p><span class="wg21note">This causes the
<code>schedule_from(sch, sndr)</code> sender to become
<code>finally(sndr, unstoppable(schedule(sch)))</code> when it is
connected with a receiver with an execution domain that does not
customize <code>schedule_from</code>.</span></p></li>
</ol>
</div>
<p><span class="ednote">The following paragraph is taken unchanged from
P2300R8.</span></p>
<ol start="6" type="1">
<li><p>Let the subexpression <code>out_sndr</code> denote the result of
the invocation <code>schedule_from(sch, sndr)</code> or an object copied
or moved from such, and let the subexpression <code>rcvr</code> denote a
receiver such that the expression <code>connect(out_sndr, rcvr)</code>
is well-formed. The expression <code>connect(out_sndr, rcvr)</code> has
undefined behavior unless it creates an asynchronous operation
([async.ops]) that, when started:</p>
<ul>
<li><p>eventually completes on an execution agent belonging to the
associated execution resource of <code>sch</code>, and</p></li>
<li><p>completes with the same async result as
<code>sndr</code>.</p></li>
</ul></li>
</ol>
</blockquote>
<p>Insert a new subsection “<code>execution::on</code> [exec.on]” as
follows:</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
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&&) 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&& [ign1, data, ign2] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)>) {
   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>>(std::forward&lt;Env>(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&& [ign, data, sndr] = out_sndr;
 if constexpr (scheduler&lt;decltype(data)>) {
   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>>) {
     return <em>not-a-sender</em>{};
   } else {
     return start_on(std::forward_like&lt;OutSndr>(data), std::forward_like&lt;OutSndr>(sndr))
          | continue_on(std::move(old_sch));
   }
 } else {
   auto&& [sch, closure] = std::forward_like&lt;OutSndr>(data);
   auto old_sch = <em>query-with-default</em>(
     get_completion_scheduler&lt;set_value_t>,
     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>>) {
     return <em>not-a-sender</em>{};
   } else {
     return std::forward_like&lt;OutSndr>(sndr)
          | <em>write-env</em>(<em>SCHED-ENV</em>(old_sch));
          | continue_on(sch)
          | std::forward_like&lt;OutSndr>(closure)
          | continue_on(old_sch)
          | <em>write-env</em>(<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>
<p><span class="ednote">The following changes to the <code>let_*</code>
algorithms are not strictly necessary; they are simplifications made
possible by the addition of the <code>write_env</code> adaptor
above.</span></p>
<p>Remove [exec.let]p5.1, which defines an exposition-only class
<em><code>receiver2</code></em>.</p>
Change [exec.let]p5.2.2 as follows:
<blockquote>
<ol start="2" type="1">
<li>Let <em><code>as-sndr2</code></em> be an alias template such that
<code><em>as-sndr2</em>&lt;Tag(Args<code>...</code>)&gt;</code> denotes
the type
<code><ins><em>call-result-t</em>&lt;tag_t&lt;write_env&gt;,</ins>
call-result-t&lt;Fn, decay_t&lt;Args&gt;&amp;<code>...</code>&gt;​<ins>,
Env&gt;</ins></code>. Then <em><code>ops2-variant-type</code></em>
denotes the type <code>variant&lt;monostate,
connect_result_t&lt;​<em>as-sndr2</em>&lt;​LetSigs​&gt;,
<del><em>receiver2</em>&lt;</del>Rcvr<del>,
Env&gt;</del>&gt;<code>...</code>&gt;</code>.</li>
</ol>
</blockquote>
<p>Change [exec.let]p5.3 as follows:</p>
<blockquote>
<ol start="3" type="1">
<li><p>The exposition-only function template
<em><code>let-bind</code></em> is <del>equal to</del><ins>as
follows</ins>:</p>
<pre style="white-space: pre-wrap; font-size: 85%; text-align: left;">
 <ins>template&lt;class State, class Rcvr, class... Args></ins>
 <ins>void <i>let-bind</i>(State& state, Rcvr& rcvr, Args&&... args) {</ins>
   auto& args = state.args.emplace&lt;<em>decayed-tuple</em>&lt;Args...>>(std::forward&lt;Args>(args)...);
   auto sndr2 = <ins>write_env(</ins>apply(std::move(state.fn), args)<ins>, std::move(state.env)</ins>); <ins><em>// see [exec.adapt.general]</em></ins>
   <del>auto rcvr2 = receiver2{std::move(rcvr), std::move(state.env)};</del>
   auto mkop2 = [&] { return connect(std::move(sndr2), std::move(rcvr<del>2</del>)); };
   auto& op2 = state.ops2.emplace&lt;decltype(mkop2())>(<em>emplace-from</em>{mkop2});
   start(op2);
 <ins>}</ins>
 </pre></li>
</ol>
</blockquote>
<h2 id="acknowlegments">Acknowlegments</h2>
<p>I’d like to thank my dog, Luna.</p>
</body>
</html>
