P2300R4
std::execution

Draft Proposal,

Authors:
Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Audience:
SG1, LEWG

1. Introduction

This paper proposes a self-contained design for a Standard C++ framework for managing asynchronous execution on generic execution contexts. It is based on the ideas in [P0443R14] and its companion papers.

1.1. Motivation

Today, C++ software is increasingly asynchronous and parallel, a trend that is likely to only continue going forward. Asynchrony and parallelism appears everywhere, from processor hardware interfaces, to networking, to file I/O, to GUIs, to accelerators. Every C++ domain and every platform needs to deal with asynchrony and parallelism, from scientific computing to video games to financial services, from the smallest mobile devices to your laptop to GPUs in the world’s fastest supercomputer.

While the C++ Standard Library has a rich set of concurrency primitives (std::atomic, std::mutex, std::counting_semaphore, etc) and lower level building blocks (std::thread, etc), we lack a Standard vocabulary and framework for asynchrony and parallelism that C++ programmers desperately need. std::async/std::future/std::promise, C++11’s intended exposure for asynchrony, is inefficient, hard to use correctly, and severely lacking in genericity, making it unusable in many contexts. We introduced parallel algorithms to the C++ Standard Library in C++17, and while they are an excellent start, they are all inherently synchronous and not composable.

This paper proposes a Standard C++ model for asynchrony, based around three key abstractions: schedulers, senders, and receivers, and a set of customizable asynchronous algorithms.

1.2. Priorities

1.3. Examples: End User

In this section we demonstrate the end-user experience of asynchronous programming directly with the sender algorithms presented in this paper. See § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers for short explanations of the algorithms used in these code examples.

1.3.1. Hello world

using namespace std::execution;

scheduler auto sch = thread_pool.scheduler();                                 // 1

sender auto begin = schedule(sch);                                            // 2
sender auto hi = then(begin, []{                                              // 3
    std::cout << "Hello world! Have an int.";                                 // 3
    return 13;                                                                // 3
});                                                                           // 3
sender auto add_42 = then(hi, [](int arg) { return arg + 42; });              // 4

auto [i] = this_thread::sync_wait(add_42).value();                            // 5

This example demonstrates the basics of schedulers, senders, and receivers:

  1. First we need to get a scheduler from somewhere, such as a thread pool. A scheduler is a lightweight handle to an execution resource.

  2. To start a chain of work on a scheduler, we call § 4.20.1 execution::schedule, which returns a sender that completes on the scheduler. A sender describes asynchronous work and sends a signal (value, error, or stopped) to some recipient(s) when that work completes.

  3. We use sender algorithms to produce senders and compose asynchronous work. § 4.21.2 execution::then is a sender adaptor that takes an input sender and a std::invocable, and calls the std::invocable on the signal sent by the input sender. The sender returned by then sends the result of that invocation. In this case, the input sender came from schedule, so its void, meaning it won’t send us a value, so our std::invocable takes no parameters. But we return an int, which will be sent to the next recipient.

  4. Now, we add another operation to the chain, again using § 4.21.2 execution::then. This time, we get sent a value - the int from the previous step. We add 42 to it, and then return the result.

  5. Finally, we’re ready to submit the entire asynchronous pipeline and wait for its completion. Everything up until this point has been completely asynchronous; the work may not have even started yet. To ensure the work has started and then block pending its completion, we use § 4.22.2 this_thread::sync_wait, which will either return a std::optional<std::tuple<...>> with the value sent by the last sender, or an empty std::optional if the last sender sent a stopped signal, or it throws an exception if the last sender sent an error.

1.3.2. Asynchronous inclusive scan

using namespace std::execution;

sender auto async_inclusive_scan(scheduler auto sch,                          // 2
                                 std::span<const double> input,               // 1
                                 std::span<double> output,                    // 1
                                 double init,                                 // 1
                                 std::size_t tile_count)                      // 3
{
  std::size_t const tile_size = (input.size() + tile_count - 1) / tile_count;

  std::vector<double> partials(tile_count + 1);                               // 4
  partials[0] = init;                                                         // 4

  return transfer_just(sch, std::move(partials))                              // 5
       | bulk(tile_count,                                                     // 6
           [=](std::size_t i, std::vector<double>& partials) {                // 7
             auto start = i * tile_size;                                      // 8
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 8
             partials[i + 1] = *--std::inclusive_scan(begin(input) + start,   // 9
                                                      begin(input) + end,     // 9
                                                      begin(output) + start); // 9
           })                                                                 // 10
       | then(                                                                // 11
           [](std::vector<double>&& partials) {
             std::inclusive_scan(begin(partials), end(partials),              // 12
                                 begin(partials));                            // 12
             return std::move(partials);                                      // 13
           })
       | bulk(tile_count,                                                     // 14
           [=](std::size_t i, std::vector<double>& partials) {                // 14
             auto start = i * tile_size;                                      // 14
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 14
             std::for_each(begin(output) + start, begin(output) + end,        // 14
               [&] (double& e) { e = partials[i] + e; }                       // 14
             );
           })
       | then(                                                                // 15
           [=](std::vector<double>&& partials) {                              // 15
             return output;                                                   // 15
           });                                                                // 15
}

This example builds an asynchronous computation of an inclusive scan:

  1. It scans a sequence of doubles (represented as the std::span<const double> input) and stores the result in another sequence of doubles (represented as std::span<double> output).

  2. It takes a scheduler, which specifies what execution context the scan should be launched on.

  3. It also takes a tile_count parameter that controls the number of execution agents that will be spawned.

  4. First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std::vector, partials. We need one double of temporary storage for each execution agent we create.

  5. Next we’ll create our initial sender with § 4.20.3 execution::transfer_just. This sender will send the temporary storage, which we’ve moved into the sender. The sender has a completion scheduler of sch, which means the next item in the chain will use sch.

  6. Senders and sender adaptors support composition via operator|, similar to C++ ranges. We’ll use operator| to attach the next piece of work, which will spawn tile_count execution agents using § 4.21.9 execution::bulk (see § 4.13 Most sender adaptors are pipeable for details).

  7. Each agent will call a std::invocable, passing it two arguments. The first is the agent’s index (i) in the § 4.21.9 execution::bulk operation, in this case a unique integer in [0, tile_count). The second argument is what the input sender sent - the temporary storage.

  8. We start by computing the start and end of the range of input and output elements that this agent is responsible for, based on our agent index.

  9. Then we do a sequential std::inclusive_scan over our elements. We store the scan result for our last element, which is the sum of all of our elements, in our temporary storage partials.

  10. After all computation in that initial § 4.21.9 execution::bulk pass has completed, every one of the spawned execution agents will have written the sum of its elements into its slot in partials.

  11. Now we need to scan all of the values in partials. We’ll do that with a single execution agent which will execute after the § 4.21.9 execution::bulk completes. We create that execution agent with § 4.21.2 execution::then.

  12. § 4.21.2 execution::then takes an input sender and an std::invocable and calls the std::invocable with the value sent by the input sender. Inside our std::invocable, we call std::inclusive_scan on partials, which the input senders will send to us.

  13. Then we return partials, which the next phase will need.

  14. Finally we do another § 4.21.9 execution::bulk of the same shape as before. In this § 4.21.9 execution::bulk, we will use the scanned values in partials to integrate the sums from other tiles into our elements, completing the inclusive scan.

  15. async_inclusive_scan returns a sender that sends the output std::span<double>. A consumer of the algorithm can chain additional work that uses the scan result. At the point at which async_inclusive_scan returns, the computation may not have completed. In fact, it may not have even started.

1.3.3. Asynchronous dynamically-sized read

using namespace std::execution;

sender_of<std::size_t> auto async_read(                                       // 1
    sender_of<std::span<std::byte>> auto buffer,                              // 1
    auto handle);                                                             // 1

struct dynamic_buffer {                                                       // 3
  std::unique_ptr<std::byte[]> data;                                          // 3
  std::size_t size;                                                           // 3
};                                                                            // 3

sender_of<dynamic_buffer> auto async_read_array(auto handle) {                // 2
  return just(dynamic_buffer{})                                               // 4
       | let_value([handle] (dynamic_buffer& buf) {                           // 5
           return just(std::as_writeable_bytes(std::span(&buf.size, 1))       // 6
                | async_read(handle)                                          // 7
                | then(                                                       // 8
                    [&buf] (std::size_t bytes_read) {                         // 9
                      assert(bytes_read == sizeof(buf.size));                 // 10
                      buf.data = std::make_unique<std::byte[]>(buf.size);     // 11
                      return std::span(buf.data.get(), buf.size);             // 12
                    })
                | async_read(handle)                                          // 13
                | then(
                    [&buf] (std::size_t bytes_read) {
                      assert(bytes_read == buf.size);                         // 14
                      return std::move(buf);                                  // 15
                    });
       });
}

This example demonstrates a common asynchronous I/O pattern - reading a payload of a dynamic size by first reading the size, then reading the number of bytes specified by the size:

  1. async_read is a pipeable sender adaptor. It’s a customization point object, but this is what it’s call signature looks like. It takes a sender parameter which must send an input buffer in the form of a std::span<std::byte>, and a handle to an I/O context. It will asynchronously read into the input buffer, up to the size of the std::span. It returns a sender which will send the number of bytes read once the read completes.

  2. async_read_array takes an I/O handle and reads a size from it, and then a buffer of that many bytes. It returns a sender that sends a dynamic_buffer object that owns the data that was sent.

  3. dynamic_buffer is an aggregate struct that contains a std::unique_ptr<std::byte[]> and a size.

  4. The first thing we do inside of async_read_array is create a sender that will send a new, empty dynamic_array object using § 4.20.2 execution::just. We can attach more work to the pipeline using operator| composition (see § 4.13 Most sender adaptors are pipeable for details).

  5. We need the lifetime of this dynamic_array object to last for the entire pipeline. So, we use let_value, which takes an input sender and a std::invocable that must return a sender itself (see § 4.21.4 execution::let_* for details). let_value sends the value from the input sender to the std::invocable. Critically, the lifetime of the sent object will last until the sender returned by the std::invocable completes.

  6. Inside of the let_value std::invocable, we have the rest of our logic. First, we want to initiate an async_read of the buffer size. To do that, we need to send a std::span pointing to buf.size. We can do that with § 4.20.2 execution::just.

  7. We chain the async_read onto the § 4.20.2 execution::just sender with operator|.

  8. Next, we pipe a std::invocable that will be invoked after the async_read completes using § 4.21.2 execution::then.

  9. That std::invocable gets sent the number of bytes read.

  10. We need to check that the number of bytes read is what we expected.

  11. Now that we have read the size of the data, we can allocate storage for it.

  12. We return a std::span<std::byte> to the storage for the data from the std::invocable. This will be sent to the next recipient in the pipeline.

  13. And that recipient will be another async_read, which will read the data.

  14. Once the data has been read, in another § 4.21.2 execution::then, we confirm that we read the right number of bytes.

  15. Finally, we move out of and return our dynamic_buffer object. It will get sent by the sender returned by async_read_array. We can attach more things to that sender to use the data in the buffer.

1.4. Asynchronous Windows socket recv

To get a better feel for how this interface might be used by low-level operations see this example implementation of a cancellable async_recv() operation for a Windows Socket.

struct operation_base : WSAOVERALAPPED {
    using completion_fn = void(operation_base* op, DWORD bytesTransferred, int errorCode) noexcept;

    // Assume IOCP event loop will call this when this OVERLAPPED structure is dequeued.
    completion_fn* completed;
};

template<typename Receiver>
struct recv_op : operation_base {
    recv_op(SOCKET s, void* data, size_t len, Receiver r)
    : receiver(std::move(r))
    , sock(s) {
        this->Internal = 0;
        this->InternalHigh = 0;
        this->Offset = 0;
        this->OffsetHigh = 0;
        this->hEvent = NULL;
        this->completed = &recv_op::on_complete;
        buffer.len = len;
        buffer.buf = static_cast<CHAR*>(data);
    }

    friend void tag_invoke(std::tag_t<std::execution::start>, recv_op& self) noexcept {
        // Avoid even calling WSARecv() if operation already cancelled
        auto st = std::execution::get_stop_token(
          std::execution::get_env(self.receiver));
        if (st.stop_requested()) {
            std::execution::set_stopped(std::move(self.receiver));
            return;
        }

        // Store and cache result here in case it changes during execution
        const bool stopPossible = st.stop_possible();
        if (!stopPossible) {
            self.ready.store(true, std::memory_order_relaxed);
        }

        // Launch the operation
        DWORD bytesTransferred = 0;
        DWORD flags = 0;
        int result = WSARecv(self.sock, &self.buffer, 1, &bytesTransferred, &flags,
                             static_cast<WSAOVERLAPPED*>(&self), NULL);
        if (result == SOCKET_ERROR) {
            int errorCode = WSAGetLastError();
            if (errorCode != WSA_IO_PENDING)) {
                if (errorCode == WSA_OPERATION_ABORTED) {
                    std::execution::set_stopped(std::move(self.receiver));
                } else {
                    std::execution::set_error(std::move(self.receiver),
                                              std::error_code(errorCode, std::system_category()));
                }
                return;
            }
        } else {
            // Completed synchronously (assuming FILE_SKIP_COMPLETION_PORT_ON_SUCCESS has been set)
            execution::set_value(std::move(self.receiver), bytesTransferred);
            return;
        }

        // If we get here then operation has launched successfully and will complete asynchronously.
        // May be completing concurrently on another thread already.
        if (stopPossible) {
            // Register the stop callback
            self.stopCallback.emplace(std::move(st), cancel_cb{self});

            // Mark as 'completed'
            if (self.ready.load(std::memory_order_acquire) ||
                self.ready.exchange(true, std::memory_order_acq_rel)) {
                // Already completed on another thread
                self.stopCallback.reset();

                BOOL ok = WSAGetOverlappedResult(self.sock, (WSAOVERLAPPED*)&self, &bytesTransferred, FALSE, &flags);
                if (ok) {
                    std::execution::set_value(std::move(self.receiver), bytesTransferred);
                } else {
                    int errorCode = WSAGetLastError();
                    std::execution::set_error(std::move(self.receiver),
                                              std::error_code(errorCode, std::system_category()));
                }
            }
        }
    }

    struct cancel_cb {
        recv_op& op;

        void operator()() noexcept {
            CancelIoEx((HANDLE)op.sock, (OVERLAPPED*)(WSAOVERLAPPED*)&op);
        }
    };

    static void on_complete(operation_base* op, DWORD bytesTransferred, int errorCode) noexcept {
        recv_op& self = *static_cast<recv_op*>(op);

        if (ready.load(std::memory_order_acquire) ||
            ready.exchange(true, std::memory_order_acq_rel)) {
            // Unsubscribe any stop-callback so we know that CancelIoEx() is not accessing 'op'
            // any more
            stopCallback.reset();

            if (errorCode == 0) {
                std::execution::set_value(std::move(receiver), bytesTransferred);
            } else {
                std::execution::set_error(std::move(receiver),
                                          std::error_code(errorCode, std::system_category()));
            }
        }
    }

    Receiver receiver;
    SOCKET sock;
    WSABUF buffer;
    std::optional<typename stop_callback_type_t<Receiver>
        ::template callback_type<cancel_cb>> stopCallback;
    std::atomic<bool> ready{false};
};

struct recv_sender {
    SOCKET sock;
    void* data;
    size_t len;

    template<typename Receiver>
    friend recv_op<Receiver> tag_invoke(std::tag_t<std::execution::connect>
                                        const recv_sender& s,
                                        Receiver r) {
        return recv_op<Receiver>{s.sock, s.data, s.len, std::move(r)};
    }
};

recv_sender async_recv(SOCKET s, void* data, size_t len) {
    return recv_sender{s, data, len};
}

1.4.1. More end-user examples

1.4.1.1. Sudoku solver

This example comes from Kirk Shoop, who ported an example from TBB’s documentation to sender/receiver in his fork of the libunifex repo. It is a Sudoku solver that uses a configurable number of threads to explore the search space for solutions.

The sender/receiver-based Sudoku solver can be found here. Some things that are worth noting about Kirk’s solution:

  1. Although it schedules asychronous work onto a thread pool, and each unit of work will schedule more work, its use of structured concurrency patterns make reference counting unnecessary. The solution does not make use of shared_ptr.

  2. In addition to eliminating the need for reference counting, the use of structured concurrency makes it easy to ensure that resources are cleaned up on all code paths. In contrast, the TBB example that inspired this one leaks memory.

For comparison, the TBB-based Sudoku solver can be found here.

1.4.1.2. File copy

This example also comes from Kirk Shoop which uses sender/receiver to recursively copy the files a directory tree. It demonstrates how sender/receiver can be used to do IO, using a scheduler that schedules work on Linux’s io_uring.

As with the Sudoku example, this example obviates the need for reference counting by employing structured concurrency. It uses iteration with an upper limit to avoid having too many open file handles.

You can find the example here.

1.4.1.3. Echo server

Dietmar Kuehl has a hobby project that implements networking APIs on top of sender/receiver. He recently implemented an echo server as a demo. His echo server code can be found here.

Below, I show the part of the echo server code. This code is executed for each client that connects to the echo server. In a loop, it reads input from a socket and echos the input back to the same socket. All of this, including the loop, is implemented with generic async algorithms.

outstanding.start(
    EX::repeat_effect_until(
          EX::let_value(
              NN::async_read_some(ptr->d_socket,
                                  context.scheduler(),
                                  NN::buffer(ptr->d_buffer))
        | EX::then([ptr](::std::size_t n){
            ::std::cout << "read='" << ::std::string_view(ptr->d_buffer, n) << "'\n";
            ptr->d_done = n == 0;
            return n;
        }),
          [&context, ptr](::std::size_t n){
            return NN::async_write_some(ptr->d_socket,
                                        context.scheduler(),
                                        NN::buffer(ptr->d_buffer, n));
          })
        | EX::then([](auto&&...){})
        , [owner = ::std::move(owner)]{ return owner->d_done; }
    )
);

In this code, NN::async_read_some and NN::async_write_some are asynchronous socket-based networking APIs that return senders. EX::repeat_effect_until, EX::let_value, and EX::then are fully generic sender adaptor algorithms that accept and return senders.

This is a good example of seamless composition of async IO functions with non-IO operations. And by composing the senders in this structured way, all the state for the composite operation -- the repeat_effect_until expression and all its child operations -- is stored altogether in a single object.

1.5. Examples: Algorithms

In this section we show a few simple sender/receiver-based algorithm implementations.

1.5.1. then

namespace exec = std::execution;

template<class R, class F>
class _then_receiver
    : exec::receiver_adaptor<_then_receiver<R, F>, R> {
  friend exec::receiver_adaptor<_then_receiver, R>;
  F f_;

  // Customize set_value by invoking the callable and passing the result to the inner receiver
  template<class... As>
    requires exec::receiver_of<R, std::invoke_result_t<F, As...>>
  void set_value(As&&... as) && noexcept try {
    exec::set_value(std::move(*this).base(), std::invoke((F&&) f_, (As&&) as...));
  } catch(...) {
    exec::set_error(std::move(*this).base(), std::current_exception());
  }

 public:
  _then_receiver(R r, F f)
   : exec::receiver_adaptor<_then_receiver, R>{std::move(r)}
   , f_(std::move(f)) {}
};

template<exec::sender S, class F>
struct _then_sender {
  S s_;
  F f_;

  // Connect:
  template<exec::receiver R>
    requires exec::sender_to<S, _then_receiver<R, F>>
  friend auto tag_invoke(exec::connect_t, _then_sender&& self, R r)
    -> exec::connect_result_t<S, _then_receiver<R, F>> {
      return exec::connect(
        (S&&) self.s_, _then_receiver<R, F>{(R&&) r, (F&&) self.f_});
  }

  // Compute the completion signatures
  template <class...Args>
    using _set_value = exec::set_value_t(std::invoke_result_t<F, Args...>);

  template<class Env>
  friend auto tag_invoke(exec::get_completion_signatures_t, _then_sender&&, Env)
    -> exec::make_completion_signatures<S, Env,
        exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
        _set_value>;
};

template<exec::sender S, class F>
exec::sender auto then(S s, F f) {
  return _then_sender<S, F>{(S&&) s, (F&&) f};
}

This code builds a then algorithm that transforms the value(s) from the input sender with a transformation function. The result of the transformation becomes the new value. The other receiver functions (set_error and set_stopped), as well as all receiver queries, are passed through unchanged.

In detail, it does the following:

  1. Defines a receiver in terms of execution::receiver_adaptor that aggregates another receiver and an invocable that:

    • Defines a constrained tag_invoke overload for transforming the value channel.

    • Defines another constrained overload of tag_invoke that passes all other customizations through unchanged.

    The tag_invoke overloads are actually implemented by execution::receiver_adaptor; they dispatch either to named members, as shown above with _then_receiver::set_value, or to the adapted receiver.

  2. Defines a sender that aggregates another sender and the invocable, which defines a tag_invoke customization for std::execution::connect that wraps the incoming receiver in the receiver from (1) and passes it and the incoming sender to std::execution::connect, returning the result. It also defines a tag_invoke customization of get_completion_signatures that declares the sender’s completion signatures when executed within a particular environment.

1.5.2. retry

using namespace std;
namespace exec = execution;

template <class From, class To>
using _decays_to = same_as<decay_t<From>, To>;

// _conv needed so we can emplace construct non-movable types into
// a std::optional.
template<invocable F>
  requires is_nothrow_move_constructible_v<F>
struct _conv {
  F f_;
  explicit _conv(F f) noexcept : f_((F&&) f) {}
  operator invoke_result_t<F>() && {
    return ((F&&) f_)();
  }
};

template<class S, class R>
struct _op;

// pass through all customizations except set_error, which retries the operation.
template<class S, class R>
struct _retry_receiver
  : exec::receiver_adaptor<_retry_receiver<S, R>> {
  _op<S, R>* o_;

  R&& base() && noexcept { return (R&&) o_->r_; }
  const R& base() const & noexcept { return o_->r_; }

  explicit _retry_receiver(_op<S, R>* o) : o_(o) {}

  void set_error(auto&&) && noexcept {
    o_->_retry(); // This causes the op to be retried
  }
};

// Hold the nested operation state in an optional so we can
// re-construct and re-start it if the operation fails.
template<class S, class R>
struct _op {
  S s_;
  R r_;
  optional<
      exec::connect_result_t<S&, _retry_receiver<S, R>>> o_;

  _op(S s, R r): s_((S&&)s), r_((R&&)r), o_{_connect()} {}
  _op(_op&&) = delete;

  auto _connect() noexcept {
    return _conv{[this] {
      return exec::connect(s_, _retry_receiver<S, R>{this});
    }};
  }
  void _retry() noexcept try {
    o_.emplace(_connect()); // potentially throwing
    exec::start(*o_);
  } catch(...) {
    exec::set_error((R&&) r_, std::current_exception());
  }
  friend void tag_invoke(exec::start_t, _op& o) noexcept {
    exec::start(*o.o_);
  }
};

template<class S>
struct _retry_sender {
  S s_;
  explicit _retry_sender(S s) : s_((S&&) s) {}

  template<exec::receiver R>
    requires exec::sender_to<S&, R>
  friend _op<S, R> tag_invoke(exec::connect_t, _retry_sender&& self, R r) {
    return {(S&&) self.s_, (R&&) r};
  }

  template <class> using _void = void;
  template <class... Ts> using _value = exec::set_value_t(Ts...);

  // Declare the signatures with which this sender can complete
  template <class Env>
  friend auto tag_invoke(exec::get_completion_signatures_t, const _retry_sender&, Env)
    -> exec::make_completion_signatures<
        const S&, Env,
        exec::completion_signatures<exec::set_error_t(std::exception_ptr)>,
        _value, _void>;
};

template<exec::sender S>
exec::sender auto retry(S s) {
  return _retry_sender{(S&&) s};
}

The retry algorithm takes a multi-shot sender and causes it to repeat on error, passing through values and stopped signals. Each time the input sender is restarted, a new receiver is connected and the resulting operation state is stored in an optional, which allows us to reinitialize it multiple times.

This example does the following:

  1. Defines a _conv utility that takes advantage of C++17’s guaranteed copy elision to emplace a non-movable type in a std::optional.

  2. Defines a _retry_receiver that holds a pointer back to the operation state. It passes all customizations through unmodified to the inner receiver owned by the operation state except for set_error, which causes a _retry() function to be called instead.

  3. Defines an operation state that aggregates the input sender and receiver, and declares storage for the nested operation state in an optional. Constructing the operation state constructs a _retry_receiver with a pointer to the (under construction) operation state and uses it to connect to the aggregated sender.

  4. Starting the operation state dispatches to start on the inner operation state.

  5. The _retry() function reinitializes the inner operation state by connecting the sender to a new receiver, holding a pointer back to the outer operation state as before.

  6. After reinitializing the inner operation state, _retry() calls start on it, causing the failed operation to be rescheduled.

  7. Defines a _retry_sender that implements the connect customization point to return an operation state constructed from the passed-in sender and receiver.

  8. _retry_sender also implements the get_completion_signatures customization point to describe the ways this sender may complete when executed in a particular execution context.

1.6. Examples: Schedulers

In this section we look at some schedulers of varying complexity.

1.6.1. Inline scheduler

class inline_scheduler {
  template <class R>
    struct _op {
      [[no_unique_address]] R rec_;
      friend void tag_invoke(std::execution::start_t, _op& op) noexcept try {
        std::execution::set_value((R&&) op.rec_);
      } catch(...) {
        std::execution::set_error((R&&) op.rec_, std::current_exception());
      }
    };

  struct _sender {
    using completion_signatures =
      std::execution::completion_signatures<
        std::execution::set_value_t(),
        std::execution::set_error_t(std::exception_ptr)>;

    template <std::execution::receiver_of R>
      friend auto tag_invoke(std::execution::connect_t, _sender, R&& rec)
        noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<R>, R>)
        -> _op<std::remove_cvref_t<R>> {
        return {(R&&) rec};
      }
  };

  friend _sender tag_invoke(std::execution::schedule_t, const inline_scheduler&) noexcept {
    return {};
  }

 public:
  inline_scheduler() = default;
  bool operator==(const inline_scheduler&) const noexcept = default;
};

The inline scheduler is a trivial scheduler that completes immediately and synchronously on the thread that calls std::execution::start on the operation state produced by its sender. In other words, start(connect(schedule(inline-scheduler), receiver)) is just a fancy way of saying set_value(receiver), with the exception of the fact that start wants to be passed an lvalue.

Although not a particularly useful scheduler, it serves to illustrate the basics of implementing one. The inline_scheduler:

  1. Customizes execution::schedule to return an instance of the sender type _sender.

  2. The _sender type models the sender concept and provides the metadata needed to describe it as a sender of no values that can send an exception_ptr as an error and that never calls set_stopped. This metadata is provided with the help of the execution::completion_signatures utility.

  3. The _sender type customizes execution::connect to accept a receiver of no values. It returns an instance of type _op that holds the receiver by value.

  4. The operation state customizes std::execution::start to call std::execution::set_value on the receiver, passing any exceptions to std::execution::set_error as an exception_ptr.

1.6.2. Single thread scheduler

This example shows how to create a scheduler for an execution context that consists of a single thread. It is implemented in terms of a lower-level execution context called std::execution::run_loop.

class single_thread_context {
  std::execution::run_loop loop_;
  std::thread thread_;

public:
  single_thread_context()
    : loop_()
    , thread_([this] { loop_.run(); })
  {}

  ~single_thread_context() {
    loop_.finish();
    thread_.join();
  }

  auto get_scheduler() noexcept {
    return loop_.get_scheduler();
  }

  std::thread::id get_thread_id() const noexcept {
    return thread_.get_id();
  }
};

The single_thread_context owns an event loop and a thread to drive it. In the destructor, it tells the event loop to finish up what it’s doing and then joins the thread, blocking for the event loop to drain.

The interesting bits are in the execution::run_loop context implementation. It is slightly too long to include here, so we only provide a reference to it, but there is one noteworthy detail about its implementation: It uses space in its operation states to build an intrusive linked list of work items. In structured concurrency patterns, the operation states of nested operations compose statically, and in an algorithm like this_thread::sync_wait, the composite operation state lives on the stack for the duration of the operation. The end result is that work can be scheduled onto this thread with zero allocations.

1.7. Examples: Server theme

In this section we look at some examples of how one would use senders to implement an HTTP server. The examples ignore the low-level details of the HTTP server and looks at how senders can be combined to achieve the goals of the project.

General application context:

1.7.1. Composability with execution::let_*

Example context:

Goals:

namespace ex = std::execution;

// Returns a sender that yields an http_request object for an incoming request
ex::sender auto schedule_request_start(read_requests_ctx ctx) {...}
// Sends a response back to the client; yields a void signal on success
ex::sender auto send_response(const http_response& resp) {...}
// Validate that the HTTP request is well-formed; forwards the request on success
ex::sender auto validate_request(const http_request& req) {...}

// Handle the request; main application logic
ex::sender auto handle_request(const http_request& req) {
  //...
  return ex::just(http_response{200, result_body});
}

// Transforms server errors into responses to be sent to the client
ex::sender auto error_to_response(std::exception_ptr err) {
  try {
    std::rethrow_exception(err);
  } catch (const std::invalid_argument& e) {
    return ex::just(http_response{404, e.what()});
  } catch (const std::exception& e) {
    return ex::just(http_response{500, e.what()});
  } catch (...) {
    return ex::just(http_response{500, "Unknown server error"});
  }
}
// Transforms cancellation of the server into responses to be sent to the client
ex::sender auto stopped_to_response() {
  return ex::just(http_response{503, "Service temporarily unavailable"});
}
//...
// The whole flow for transforming incoming requests into responses
ex::sender auto snd =
    // get a sender when a new request comes
    schedule_request_start(the_read_requests_ctx)
    // make sure the request is valid; throw if not
    | ex::let_value(validate_request)
    // process the request in a function that may be using a different execution context
    | ex::let_value(handle_request)
    // If there are errors transform them into proper responses
    | ex::let_error(error_to_response)
    // If the flow is cancelled, send back a proper response
    | ex::let_stopped(stopped_to_response)
    // write the result back to the client
    | ex::let_value(send_response)
    // done
    ;
// execute the whole flow asynchronously
ex::start_detached(std::move(snd));
The example shows how one can separate out the concerns for interpreting requests, validating requests, running the main logic for handling the request, generating error responses, handling cancellation and sending the response back to the client. They are all different phases in the application, and can be joined together with the let_* functions.

All our functions return execution::sender objects, so that they can all generate success, failure and cancellation paths. For example, regardless where an error is generated (reading request, validating request or handling the response), we would have one common block to handle the error, and following error flows is easy.

Also, because of using execution::sender objects at any step, we might expect any of these steps to be completely asynchronous; the overall flow doesn’t care. Regardless of the execution context in which the steps, or part of the steps are executed in, the flow is still the same.

1.7.2. Moving between execution contexts with execution::on and execution::transfer

Example context:

Goals:

namespace ex = std::execution;

size_t legacy_read_from_socket(int sock, char* buffer, size_t buffer_len) {}
void process_read_data(const char* read_data, size_t read_len) {}
//...

// A sender that just calls the legacy read function
auto snd_read = ex::just(sock, buf, buf_len) | ex::then(legacy_read_from_socket);
// The entire flow
auto snd =
    // start by reading data on the I/O thread
    ex::on(io_sched, std::move(snd_read))
    // do the processing on the worker threads pool
    | ex::transfer(work_sched)
    // process the incoming data (on worker threads)
    | ex::then([buf](int read_len) { process_read_data(buf, read_len); })
    // done
    ;
// execute the whole flow asynchronously
ex::start_detached(std::move(snd));

The example assume that we need to wrap some legacy code of reading sockets, and handle execution context switching. (This style of reading from socket may not be the most efficient one, but it’s working for our purposes.) For performance reasons, the reading from the socket needs to be done on the I/O thread, and all the processing needs to happen on a work-specific execution context (i.e., thread pool).

Calling execution::on will ensure that the given sender will be started on the given scheduler. In our example, snd_read is going to be started on the I/O scheduler. This sender will just call the legacy code.

The completion signal will be issued in the I/O execution context, so we have to move it to the work thread pool. This is achieved with the help of the execution::transfer algorithm. The rest of the processing (in our case, the last call to then) will happen in the work thread pool.

The reader should notice the difference between execution::on and execution::transfer. The execution::on algorithm will ensure that the given sender will start in the specified context, and doesn’t care where the completion signal for that sender is sent. The execution::transfer algorithm will not care where the given sender is going to be started, but will ensure that the completion signal of will be transferred to the given context.

1.8. What this proposal is not

This paper is not a patch on top of [P0443R14]; we are not asking to update the existing paper, we are asking to retire it in favor of this paper, which is already self-contained; any example code within this paper can be written in Standard C++, without the need to standardize any further facilities.

This paper is not an alternative design to [P0443R14]; rather, we have taken the design in the current executors paper, and applied targeted fixes to allow it to fulfill the promises of the sender/receiver model, as well as provide all the facilities we consider essential when writing user code using standard execution concepts; we have also applied the guidance of removing one-way executors from the paper entirely, and instead provided an algorithm based around senders that serves the same purpose.

1.9. Design changes from P0443

  1. The executor concept has been removed and all of its proposed functionality is now based on schedulers and senders, as per SG1 direction.

  2. Properties are not included in this paper. We see them as a possible future extension, if the committee gets more comfortable with them.

  3. Senders now advertise what scheduler, if any, their evaluation will complete on.

  4. The places of execution of user code in P0443 weren’t precisely defined, whereas they are in this paper. See § 4.5 Senders can propagate completion schedulers.

  5. P0443 did not propose a suite of sender algorithms necessary for writing sender code; this paper does. See § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers.

  6. P0443 did not specify the semantics of variously qualified connect overloads; this paper does. See § 4.7 Senders can be either multi-shot or single-shot.

  7. This paper extends the sender traits/typed sender design to support typed senders whose value/error types depend on type information provided late via the receiver.

  8. Support for untyped senders is dropped; the typed_sender concept is renamed sender; sender_traits is replaced with completion_signatures_of_t.

  9. Specific type erasure facilities are omitted, as per LEWG direction. Type erasure facilities can be built on top of this proposal, as discussed in § 5.9 Ranges-style CPOs vs tag_invoke.

  10. A specific thread pool implementation is omitted, as per LEWG direction.

  11. Some additional utilities are added:

    • run_loop: An execution context that provides a multi-producer, single-consumer, first-in-first-out work queue.

    • receiver_adaptor: A utility for algorithm authors for defining one receiver type in terms of another.

    • completion_signatures and make_completion_signatures: Utilities for describing the ways in which a sender can complete in a declarative syntax.

1.10. Prior art

This proposal builds upon and learns from years of prior art with asynchronous and parallel programming frameworks in C++. In this section, we discuss async abstractions that have previously been suggested as a possible basis for asynchronous algorithms and why they fall short.

1.10.1. Futures

A future is a handle to work that has already been scheduled for execution. It is one end of a communication channel; the other end is a promise, used to receive the result from the concurrent operation and to communicate it to the future.

Futures, as traditionally realized, require the dynamic allocation and management of a shared state, synchronization, and typically type-erasure of work and continuation. Many of these costs are inherent in the nature of "future" as a handle to work that is already scheduled for execution. These expenses rule out the future abstraction for many uses and makes it a poor choice for a basis of a generic mechanism.

1.10.2. Coroutines

C++20 coroutines are frequently suggested as a basis for asynchronous algorithms. It’s fair to ask why, if we added coroutines to C++, are we suggesting the addition of a library-based abstraction for asynchrony. Certainly, coroutines come with huge syntactic and semantic advantages over the alternatives.

Although coroutines are lighter weight than futures, coroutines suffer many of the same problems. Since they typically start suspended, they can avoid synchronizing the chaining of dependent work. However in many cases, coroutine frames require an unavoidable dynamic allocation and indirect function calls. This is done to hide the layout of the coroutine frame from the C++ type system, which in turn makes possible the separate compilation of coroutines and certain compiler optimizations, such as optimization of the coroutine frame size.

Those advantages come at a cost, though. Because of the dynamic allocation of coroutine frames, coroutines in embedded or heterogeneous environments, which often lack support for dynamic allocation, require great attention to detail. And the allocations and indirections tend to complicate the job of the inliner, often resulting in sub-optimal codegen.

The coroutine language feature mitigates these shortcomings somewhat with the HALO optimization [P0981R0], which leverages existing compiler optimizations such as allocation elision and devirtualization to inline the coroutine, completely eliminating the runtime overhead. However, HALO requires a sophisiticated compiler, and a fair number of stars need to align for the optimization to kick in. In our experience, more often than not in real-world code today’s compilers are not able to inline the coroutine, resulting in allocations and indirections in the generated code.

In a suite of generic async algorithms that are expected to be callable from hot code paths, the extra allocations and indirections are a deal-breaker. It is for these reasons that we consider coroutines a poor choise for a basis of all standard async.

1.10.3. Callbacks

Callbacks are the oldest, simplest, most powerful, and most efficient mechanism for creating chains of work, but suffer problems of their own. Callbacks must propagate either errors or values. This simple requirement yields many different interface possibilities. The lack of a standard callback shape obstructs generic design.

Additionally, few of these possibilities accommodate cancellation signals when the user requests upstream work to stop and clean up.

1.11. Field experience

1.11.1. libunifex

This proposal draws heavily from our field experience with libunifex. Libunifex implements all of the concepts and customization points defined in this paper (with slight variations -- the design of P2300 has evolved due to LEWG feedback), many of this paper’s algorithms (some under different names), and much more besides.

Libunifex has several concrete schedulers in addition to the run_loop suggested here (where it is called manual_event_loop). It has schedulers that dispatch efficiently to epoll and io_uring on Linux and the Windows Thread Pool on Windows.

In addition to the proposed interfaces and the additional schedulers, it has several important extensions to the facilities described in this paper, which demonstrate directions in which these abstractions may be evolved over time, including:

Libunifex has seen heavy production use at Facebook. As of October 2021, it is currently used in production within the following applications and platforms:

All of these applications are making direct use of the sender/receiver abstraction as presented in this paper. One product (Instagram on iOS) is making use of the sender/coroutine integration as presented. The monthly active users of these products number in the billions.

1.11.2. Other implementations

The authors are aware of a number of other implementations of sender/receiver from this paper. These are presented here in perceived order of maturity and field experience.

1.11.3. Inspirations

This proposal also draws heavily from our experience with Thrust and Agency. It is also inspired by the needs of countless other C++ frameworks for asynchrony, parallelism, and concurrency, including:

2. Revision history

2.1. R4

The changes since R3 are as follows:

Fixes:

Enhancements:

2.1.1. Dependently-typed senders

Background:

In the sender/receiver model, as with coroutines, contextual information about the current execution is most naturally propagated from the consumer to the producer. In coroutines, that means information like stop tokens, allocators and schedulers are propagated from the calling coroutine to the callee. In sender/receiver, that means that that contextual information is associated with the receiver and is queried by the sender and/or operation state after the sender and the receiver are connect-ed.

Problem:

The implication of the above is that the sender alone does not have all the information about the async computation it will ultimately initiate; some of that information is provided late via the receiver. However, the sender_traits mechanism, by which an algorithm can introspect the value and error types the sender will propagate, only accepts a sender parameter. It does not take into consideration the type information that will come in late via the receiver. The effect of this is that some senders cannot be typed senders when they otherwise could be.

Example:

To get concrete, consider the case of the "get_scheduler()" sender: when connect-ed and start-ed, it queries the receiver for its associated scheduler and passes it back to the receiver through the value channel. That sender’s "value type" is the type of the receiver’s scheduler. What then should sender_traits<get_scheduler_sender>::value_types report for the get_scheduler()'s value type? It can’t answer because it doesn’t know.

This causes knock-on problems since some important algorithms require a typed sender, such as sync_wait. To illustrate the problem, consider the following code:

namespace ex = std::execution;

ex::sender auto task =
  ex::let_value(
    ex::get_scheduler(), // Fetches scheduler from receiver.
    [](auto current_sched) {
      // Lauch some nested work on the current scheduler:
      return ex::on(current_sched, nested work...);
    });

std::this_thread::sync_wait(std::move(task));

The code above is attempting to schedule some work onto the sync_wait's run_loop execution context. But let_value only returns a typed sender when the input sender is typed. As we explained above, get_scheduler() is not typed, so task is likewise not typed. Since task isn’t typed, it cannot be passed to sync_wait which is expecting a typed sender. The above code would fail to compile.

Solution:

The solution is conceptually quite simple: extend the sender_traits mechanism to optionally accept a receiver in addition to the sender. The algorithms can use sender_traits<Sender, Receiver> to inspect the async operation’s completion signals. The typed_sender concept would also need to take an optional receiver parameter. This is the simplest change, and it would solve the immediate problem.

Design:

Using the receiver type to compute the sender traits turns out to have pitfalls in practice. Many receivers make use of that type information in their implementation. It is very easy to create cycles in the type system, leading to inscrutible errors. The design pursued in R4 is to give receivers an associated environment object -- a bag of key/value pairs -- and to move the contextual information (schedulers, etc) out of the receiver and into the environment. The sender_traits template and the typed_sender concept, rather than taking a receiver, take an environment. This is a much more robust design.

A further refinement of this design would be to separate the receiver and the environment entirely, passing then as separate arguments along with the sender to connect. This paper does not propose that change.

Impact:

This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:

"Has it been implemented?"

Yes, the reference implementation, which can be found at https://github.com/brycelelbach/wg21_p2300_std_execution, has implemented this design as well as some dependently-typed senders to confirm that it works.

Implementation experience

Although this change has not yet been made in libunifex, the most widely adopted sender/receiver implementation, a similar design can be found in Folly’s coroutine support library. In Folly.Coro, it is possible to await a special awaitable to obtain the current coroutine’s associated scheduler (called an executor in Folly).

For instance, the following Folly code grabs the current executor, schedules a task for execution on that executor, and starts the resulting (scheduled) task by enqueueing it for execution.

// From Facebook’s Folly open source library:
template <class T>
folly::coro::Task<void> CancellableAsyncScope::co_schedule(folly::coro::Task<T>&& task) {
  this->add(std::move(task).scheduleOn(co_await co_current_executor));
  co_return;
}

Facebook relies heavily on this pattern in its coroutine code. But as described above, this pattern doesn’t work with R3 of std::execution because of the lack of dependently-typed schedulers. The change to sender_traits in R4 rectifies that.

Why now?

The authors are loathe to make any changes to the design, however small, at this stage of the C++23 release cycle. But we feel that, for a relatively minor design change -- adding an extra template parameter to sender_traits and typed_sender -- the returns are large enough to justify the change. And there is no better time to make this change than as early as possible.

One might wonder why this missing feature not been added to sender/receiver before now. The designers of sender/receiver have long been aware of the need. What was missing was a clean, robust, and simple design for the change, which we now have.

Drive-by:

We took the opportunity to make an additional drive-by change: Rather than providing the sender traits via a class template for users to specialize, we changed it into a sender query: get_completion_signatures(sender, env). That function’s return type is used as the sender’s traits. The authors feel this leads to a more uniform design and gives sender authors a straightforward way to make the value/error types dependent on the cv- and ref-qualification of the sender if need be.

Details:

Below are the salient parts of the new support for dependently-typed senders in R4:

2.2. R3

The changes since R2 are as follows:

Fixes:

Enhancements:

2.3. R2

The changes since R1 are as follows:

2.4. R1

The changes since R0 are as follows:

2.5. R0

Initial revision.

3. Design - introduction

The following three sections describe the entirety of the proposed design.

3.1. Conventions

The following conventions are used throughout the design section:

  1. The namespace proposed in this paper is the same as in [P0443R14]: std::execution; however, for brevity, the std:: part of this name is omitted. When you see execution::foo, treat that as std::execution::foo.

  2. Universal references and explicit calls to std::move/std::forward are omitted in code samples and signatures for simplicity; assume universal references and perfect forwarding unless stated otherwise.

  3. None of the names proposed here are names that we are particularly attached to; consider the names to be reasonable placeholders that can freely be changed, should the committee want to do so.

3.2. Queries and algorithms

A query is a std::invocable that takes some set of objects (usually one) as parameters and returns facts about those objects without modifying them. Queries are usually customization point objects, but in some cases may be functions.

An algorithm is a std::invocable that takes some set of objects as parameters and causes those objects to do something. Algorithms are usually customization point objects, but in some cases may be functions.

4. Design - user side

4.1. Execution contexts describe the place of execution

An execution context is a resource that represents the place where execution will happen. This could be a concrete resource - like a specific thread pool object, or a GPU - or a more abstract one, like the current thread of execution. Execution contexts don’t need to have a representation in code; they are simply a term describing certain properties of execution of a function.

4.2. Schedulers represent execution contexts

A scheduler is a lightweight handle that represents a strategy for scheduling work onto an execution context. Since execution contexts don’t necessarily manifest in C++ code, it’s not possible to program directly against their API. A scheduler is a solution to that problem: the scheduler concept is defined by a single sender algorithm, schedule, which returns a sender that will complete on an execution context determined by the scheduler. Logic that you want to run on that context can be placed in the receiver’s completion-signalling method.

execution::scheduler auto sch = thread_pool.scheduler();
execution::sender auto snd = execution::schedule(sch);
// snd is a sender (see below) describing the creation of a new execution resource
// on the execution context associated with sch

Note that a particular scheduler type may provide other kinds of scheduling operations which are supported by its associated execution context. It is not limited to scheduling purely using the execution::schedule API.

Future papers will propose additional scheduler concepts that extend scheduler to add other capabilities. For example:

4.3. Senders describe work

A sender is an object that describes work. Senders are similar to futures in existing asynchrony designs, but unlike futures, the work that is being done to arrive at the values they will send is also directly described by the sender object itself. A sender is said to send some values if a receiver connected (see § 5.3 execution::connect) to that sender will eventually receive said values.

The primary defining sender algorithm is § 5.3 execution::connect; this function, however, is not a user-facing API; it is used to facilitate communication between senders and various sender algorithms, but end user code is not expected to invoke it directly.

The way user code is expected to interact with senders is by using sender algorithms. This paper proposes an initial set of such sender algorithms, which are described in § 4.4 Senders are composable through sender algorithms, § 4.20 User-facing sender factories, § 4.21 User-facing sender adaptors, and § 4.22 User-facing sender consumers. For example, here is how a user can create a new sender on a scheduler, attach a continuation to it, and then wait for execution of the continuation to complete:

execution::scheduler auto sch = thread_pool.scheduler();
execution::sender auto snd = execution::schedule(sch);
execution::sender auto cont = execution::then(snd, []{
    std::fstream file{ "result.txt" };
    file << compute_result;
});

this_thread::sync_wait(cont);
// at this point, cont has completed execution

4.4. Senders are composable through sender algorithms

Asynchronous programming often departs from traditional code structure and control flow that we are familiar with. A successful asynchronous framework must provide an intuitive story for composition of asynchronous work: expressing dependencies, passing objects, managing object lifetimes, etc.

The true power and utility of senders is in their composability. With senders, users can describe generic execution pipelines and graphs, and then run them on and across a variety of different schedulers. Senders are composed using sender algorithms:

4.5. Senders can propagate completion schedulers

One of the goals of executors is to support a diverse set of execution contexts, including traditional thread pools, task and fiber frameworks (like HPX and Legion), and GPUs and other accelerators (managed by runtimes such as CUDA or SYCL). On many of these systems, not all execution agents are created equal and not all functions can be run on all execution agents. Having precise control over the execution context used for any given function call being submitted is important on such systems, and the users of standard execution facilities will expect to be able to express such requirements.

[P0443R14] was not always clear about the place of execution of any given piece of code. Precise control was present in the two-way execution API present in earlier executor designs, but it has so far been missing from the senders design. There has been a proposal ([P1897R3]) to provide a number of sender algorithms that would enforce certain rules on the places of execution of the work described by a sender, but we have found those sender algorithms to be insufficient for achieving the best performance on all platforms that are of interest to us. The implementation strategies that we are aware of result in one of the following situations:

  1. trying to submit work to one execution context (such as a CPU thread pool) from another execution context (such as a GPU or a task framework), which assumes that all execution agents are as capable as a std::thread (which they aren’t).

  2. forcibly interleaving two adjacent execution graph nodes that are both executing on one execution context (such as a GPU) with glue code that runs on another execution context (such as a CPU), which is prohibitively expensive for some execution contexts (such as CUDA or SYCL).

  3. having to customise most or all sender algorithms to support an execution context, so that you can avoid problems described in 1. and 2, which we believe is impractical and brittle based on months of field experience attempting this in Agency.

None of these implementation strategies are acceptable for many classes of parallel runtimes, such as task frameworks (like HPX) or accelerator runtimes (like CUDA or SYCL).

Therefore, in addition to the on sender algorithm from [P1897R3], we are proposing a way for senders to advertise what scheduler (and by extension what execution context) they will complete on. Any given sender may have completion schedulers for some or all of the signals (value, error, or stopped) it completes with (for more detail on the completion signals, see § 5.1 Receivers serve as glue between senders). When further work is attached to that sender by invoking sender algorithms, that work will also complete on an appropriate completion scheduler.

4.5.1. execution::get_completion_scheduler

get_completion_scheduler is a query that retrieves the completion scheduler for a specific completion signal from a sender. Calling get_completion_scheduler on a sender that does not have a completion scheduler for a given signal is ill-formed. If a sender advertises a completion scheduler for a signal in this way, that sender must ensure that it sends that signal on an execution agent belonging to an execution context represented by a scheduler returned from this function. See § 4.5 Senders can propagate completion schedulers for more details.

execution::scheduler auto cpu_sched = new_thread_scheduler{};
execution::scheduler auto gpu_sched = cuda::scheduler();

execution::sender auto snd0 = execution::schedule(cpu_sched);
execution::scheduler auto completion_sch0 =
  execution::get_completion_scheduler<execution::set_value_t>(snd0);
// completion_sch0 is equivalent to cpu_sched

execution::sender auto snd1 = execution::then(snd0, []{
    std::cout << "I am running on cpu_sched!\n";
});
execution::scheduler auto completion_sch1 =
  execution::get_completion_scheduler<execution::set_value_t>(snd1);
// completion_sch1 is equivalent to cpu_sched

execution::sender auto snd2 = execution::transfer(snd1, gpu_sched);
execution::sender auto snd3 = execution::then(snd2, []{
    std::cout << "I am running on gpu_sched!\n";
});
execution::scheduler auto completion_sch3 =
  execution::get_completion_scheduler<execution::set_value_t>(snd3);
// completion_sch3 is equivalent to gpu_sched

4.6. Execution context transitions are explicit

[P0443R14] does not contain any mechanisms for performing an execution context transition. The only sender algorithm that can create a sender that will move execution to a specific execution context is execution::schedule, which does not take an input sender. That means that there’s no way to construct sender chains that traverse different execution contexts. This is necessary to fulfill the promise of senders being able to replace two-way executors, which had this capability.

We propose that, for senders advertising their completion scheduler, all execution context transitions must be explicit; running user code anywhere but where they defined it to run must be considered a bug.

The execution::transfer sender adaptor performs a transition from one execution context to another:

execution::scheduler auto sch1 = ...;
execution::scheduler auto sch2 = ...;

execution::sender auto snd1 = execution::schedule(sch1);
execution::sender auto then1 = execution::then(snd1, []{
    std::cout << "I am running on sch1!\n";
});

execution::sender auto snd2 = execution::transfer(then1, sch2);
execution::sender auto then2 = execution::then(snd2, []{
    std::cout << "I am running on sch2!\n";
});

this_thread::sync_wait(then2);

4.7. Senders can be either multi-shot or single-shot

Some senders may only support launching their operation a single time, while others may be repeatable and support being launched multiple times. Executing the operation may consume resources owned by the sender.

For example, a sender may contain a std::unique_ptr that it will be transferring ownership of to the operation-state returned by a call to execution::connect so that the operation has access to this resource. In such a sender, calling execution::connect consumes the sender such that after the call the input sender is no longer valid. Such a sender will also typically be move-only so that it can maintain unique ownership of that resource.

A single-shot sender can only be connected to a receiver at most once. Its implementation of execution::connect only has overloads for an rvalue-qualified sender. Callers must pass the sender as an rvalue to the call to execution::connect, indicating that the call consumes the sender.

A multi-shot sender can be connected to multiple receivers and can be launched multiple times. Multi-shot senders customise execution::connect to accept an lvalue reference to the sender. Callers can indicate that they want the sender to remain valid after the call to execution::connect by passing an lvalue reference to the sender to call these overloads. Multi-shot senders should also define overloads of execution::connect that accept rvalue-qualified senders to allow the sender to be also used in places where only a single-shot sender is required.

If the user of a sender does not require the sender to remain valid after connecting it to a receiver then it can pass an rvalue-reference to the sender to the call to execution::connect. Such usages should be able to accept either single-shot or multi-shot senders.

If the caller does wish for the sender to remain valid after the call then it can pass an lvalue-qualified sender to the call to execution::connect. Such usages will only accept multi-shot senders.

Algorithms that accept senders will typically either decay-copy an input sender and store it somewhere for later usage (for example as a data-member of the returned sender) or will immediately call execution::connect on the input sender, such as in this_thread::sync_wait or execution::start_detached.

Some multi-use sender algorithms may require that an input sender be copy-constructible but will only call execution::connect on an rvalue of each copy, which still results in effectively executing the operation multiple times. Other multi-use sender algorithms may require that the sender is move-constructible but will invoke execution::connect on an lvalue reference to the sender.

For a sender to be usable in both multi-use scenarios, it will generally be required to be both copy-constructible and lvalue-connectable.

4.8. Senders are forkable

Any non-trivial program will eventually want to fork a chain of senders into independent streams of work, regardless of whether they are single-shot or multi-shot. For instance, an incoming event to a middleware system may be required to trigger events on more than one downstream system. This requires that we provide well defined mechanisms for making sure that connecting a sender multiple times is possible and correct.

The split sender adaptor facilitates connecting to a sender multiple times, regardless of whether it is single-shot or multi-shot:

auto some_algorithm(execution::sender auto&& input) {
    execution::sender auto multi_shot = split(input);
    // "multi_shot" is guaranteed to be multi-shot,
    // regardless of whether "input" was multi-shot or not

    return when_all(
      then(multi_shot, [] { std::cout << "First continuation\n"; }),
      then(multi_shot, [] { std::cout << "Second continuation\n"; })
    );
}

4.9. Senders are joinable

Similarly to how it’s hard to write a complex program that will eventually want to fork sender chains into independent streams, it’s also hard to write a program that does not want to eventually create join nodes, where multiple independent streams of execution are merged into a single one in an asynchronous fashion.

when_all is a sender adaptor that returns a sender that completes when the last of the input senders completes. It sends a pack of values, where the elements of said pack are the values sent by the input senders, in order. when_all returns a sender that also does not have an associated scheduler.

transfer_when_all accepts an additional scheduler argument. It returns a sender whose value completion scheduler is the scheduler provided as an argument, but otherwise behaves the same as when_all. You can think of it as a composition of transfer(when_all(inputs...), scheduler), but one that allows for better efficiency through customization.

4.10. Senders support cancellation

Senders are often used in scenarios where the application may be concurrently executing multiple strategies for achieving some program goal. When one of these strategies succeeds (or fails) it may not make sense to continue pursuing the other strategies as their results are no longer useful.

For example, we may want to try to simultaneously connect to multiple network servers and use whichever server responds first. Once the first server responds we no longer need to continue trying to connect to the other servers.

Ideally, in these scenarios, we would somehow be able to request that those other strategies stop executing promptly so that their resources (e.g. cpu, memory, I/O bandwidth) can be released and used for other work.

While the design of senders has support for cancelling an operation before it starts by simply destroying the sender or the operation-state returned from execution::connect() before calling execution::start(), there also needs to be a standard, generic mechanism to ask for an already-started operation to complete early.

The ability to be able to cancel in-flight operations is fundamental to supporting some kinds of generic concurrency algorithms.

For example:

The mechanism used for communcating cancellation-requests, or stop-requests, needs to have a uniform interface so that generic algorithms that compose sender-based operations, such as the ones listed above, are able to communicate these cancellation requests to senders that they don’t know anything about.

The design is intended to be composable so that cancellation of higher-level operations can propagate those cancellation requests through intermediate layers to lower-level operations that need to actually respond to the cancellation requests.

For example, we can compose the algorithms mentioned above so that child operations are cancelled when any one of the multiple cancellation conditions occurs:

sender auto composed_cancellation_example(auto query) {
  return stop_when(
    timeout(
      when_all(
        first_successful(
          query_server_a(query),
          query_server_b(query)),
        load_file("some_file.jpg")),
      5s),
    cancelButton.on_click());
}

In this example, if we take the operation returned by query_server_b(query), this operation will receive a stop-request when any of the following happens:

Note that within this code there is no explicit mention of cancellation, stop-tokens, callbacks, etc. yet the example fully supports and responds to the various cancellation sources.

The intent of the design is that the common usage of cancellation in sender/receiver-based code is primarily through use of concurrency algorithms that manage the detailed plumbing of cancellation for you. Much like algorithms that compose senders relieve the user from having to write their own receiver types, algorithms that introduce concurrency and provide higher-level cancellation semantics relieve the user from having to deal with low-level details of cancellation.

4.10.1. Cancellation design summary

The design of cancellation described in this paper is built on top of and extends the std::stop_token-based cancellation facilities added in C++20, first proposed in [P2175R0].

At a high-level, the facilities proposed by this paper for supporting cancellation include:

In addition, there are requirements added to some of the algorithms to specify what their cancellation behaviour is and what the requirements of customisations of those algorithms are with respect to cancellation.

The key component that enables generic cancellation within sender-based operations is the execution::get_stop_token() CPO. This CPO takes a single parameter, which is the execution environment of the receiver passed to execution::connect, and returns a std::stoppable_token that the operation can use to check for stop-requests for that operation.

As the caller of execution::connect typically has control over the receiver type it passes, it is able to customise the execution::get_env() CPO for that receiver to return an execution environment that hooks the execution::get_stop_token() CPO to return a stop-token that the receiver has control over and that it can use to communicate a stop-request to the operation once it has started.

4.10.2. Support for cancellation is optional

Support for cancellation is optional, both on part of the author of the receiver and on part of the author of the sender.

If the receiver’s execution environment does not customise the execution::get_stop_token() CPO then invoking the CPO on that receiver’s environment will invoke the default implementation which returns std::never_stop_token. This is a special stoppable_token type that is statically known to always return false from the stop_possible() method.

Sender code that tries to use this stop-token will in general result in code that handles stop-requests being compiled out and having little to no run-time overhead.

If the sender doesn’t call execution::get_stop_token(), for example because the operation does not support cancellation, then it will simply not respond to stop-requests from the caller.

Note that stop-requests are generally racy in nature as there is often a race betwen an operation completing naturally and the stop-request being made. If the operation has already completed or past the point at which it can be cancelled when the stop-request is sent then the stop-request may just be ignored. An application will typically need to be able to cope with senders that might ignore a stop-request anyway.

4.10.3. Cancellation is inherently racy

Usually, an operation will attach a stop-callback at some point inside the call to execution::start() so that a subsequent stop-request will interrupt the logic.

A stop-request can be issued concurrently from another thread. This means the implementation of execution::start() needs to be careful to ensure that, once a stop-callback has been registered, that there are no data-races between a potentially concurrently-executing stop-callback and the rest of the execution::start() implementation.

An implementation of execution::start() that supports cancellation will generally need to perform (at least) two separate steps: launch the operation, subscribe a stop-callback to the receiver’s stop-token. Care needs to be taken depending on the order in which these two steps are performed.

If the stop-callback is subscribed first and then the operation is launched, care needs to be taken to ensure that a stop-request that invokes the stop-callback on another thread after the stop-callback is registered but before the operation finishes launching does not either result in a missed cancellation request or a data-race. e.g. by performing an atomic write after the launch has finished executing

If the operation is launched first and then the stop-callback is subscribed, care needs to be taken to ensure that if the launched operation completes concurrently on another thread that it does not destroy the operation-state until after the stop-callback has been registered. e.g. by having the execution::start implementation write to an atomic variable once it has finished registering the stop-callback and having the concurrent completion handler check that variable and either call the completion-signalling operation or store the result and defer calling the receiver’s completion-signalling operation to the execution::start() call (which is still executing).

For an example of an implementation strategy for solving these data-races see § 1.4 Asynchronous Windows socket recv.

4.10.4. Cancellation design status

This paper currently includes the design for cancellation as proposed in [P2175R0] - "Composable cancellation for sender-based async operations". P2175R0 contains more details on the background motivation and prior-art and design rationale of this design.

It is important to note, however, that initial review of this design in the SG1 concurrency subgroup raised some concerns related to runtime overhead of the design in single-threaded scenarios and these concerns are still being investigated.

The design of P2175R0 has been included in this paper for now, despite its potential to change, as we believe that support for cancellation is a fundamental requirement for an async model and is required in some form to be able to talk about the semantics of some of the algorithms proposed in this paper.

This paper will be updated in the future with any changes that arise from the investigations into P2175R0.

4.11. Sender factories and adaptors are lazy

In an earlier revision of this paper, some of the proposed algorithms supported executing their logic eagerly; i.e., before the returned sender has been connected to a receiver and started. These algorithms were removed because eager execution has a number of negative semantic and performance implications.

We have originally included this functionality in the paper because of a long-standing belief that eager execution is a mandatory feature to be included in the standard Executors facility for that facility to be acceptable for accelerator vendors. A particular concern was that we must be able to write generic algorithms that can run either eagerly or lazily, depending on the kind of an input sender or scheduler that have been passed into them as arguments. We considered this a requirement, because the _latency_ of launching work on an accelerator can sometimes be considerable.

However, in the process of working on this paper and implementations of the features proposed within, our set of requirements has shifted, as we understood the different implementation strategies that are available for the feature set of this paper better, and, after weighting the earlier concerns against the points presented below, we have arrived at the conclusion that a purely lazy model is enough for most algorithms, and users who intend to launch work earlier may use an algorithm such as ensure_started to achieve that goal. We have also come to deeply appreciate the fact that a purely lazy model allows both the implementation and the compiler to have a much better understanding of what the complete graph of tasks looks like, allowing them to better optimize the code - also when targetting accelerators.

4.11.1. Eager execution leads to detached work or worse

One of the questions that arises with APIs that can potentially return eagerly-executing senders is "What happens when those senders are destructed without a call to execution::connect?" or similarly, "What happens if a call to execution::connect is made, but the returned operation state is destroyed before execution::start is called on that operation state"?

In these cases, the operation represented by the sender is potentially executing concurrently in another thread at the time that the destructor of the sender and/or operation-state is running. In the case that the operation has not completed executing by the time that the destructor is run we need to decide what the semantics of the destructor is.

There are three main strategies that can be adopted here, none of which is particularly satisfactory:

  1. Make this undefined-behaviour - the caller must ensure that any eagerly-executing sender is always joined by connecting and starting that sender. This approach is generally pretty hostile to programmers, particularly in the presence of exceptions, since it complicates the ability to compose these operations.

    Eager operations typically need to acquire resources when they are first called in order to start the operation early. This makes eager algorithms prone to failure. Consider, then, what might happen in an expression such as when_all(eager_op_1(), eager_op_2()). Imagine eager_op_1() starts an asynchronous operation successfully, but then eager_op_2() throws. For lazy senders, that failure happens in the context of the when_all algorithm, which handles the failure and ensures that async work joins on all code paths. In this case though -- the eager case -- the child operation has failed even before when_all has been called.

    It then becomes the responsibility, not of the algorithm, but of the end user to handle the exception and ensure that eager_op_1() is joined before allowing the exception to propagate. If they fail to do that, they incur undefined behavior.

  2. Detach from the computation - let the operation continue in the background - like an implicit call to std::thread::detach(). While this approach can work in some circumstances for some kinds of applications, in general it is also pretty user-hostile; it makes it difficult to reason about the safe destruction of resources used by these eager operations. In general, detached work necessitates some kind of garbage collection; e.g., std::shared_ptr, to ensure resources are kept alive until the operations complete, and can make clean shutdown nigh impossible.

  3. Block in the destructor until the operation completes. This approach is probably the safest to use as it preserves the structured nature of the concurrent operations, but also introduces the potential for deadlocking the application if the completion of the operation depends on the current thread making forward progress.

    The risk of deadlock might occur, for example, if a thread-pool with a small number of threads is executing code that creates a sender representing an eagerly-executing operation and then calls the destructor of that sender without joining it (e.g. because an exception was thrown). If the current thread blocks waiting for that eager operation to complete and that eager operation cannot complete until some entry enqueued to the thread-pool’s queue of work is run then the thread may wait for an indefinite amount of time. If all thread of the thread-pool are simultaneously performing such blocking operations then deadlock can result.

There are also minor variations on each of these choices. For example:

  1. A variation of (1): Call std::terminate if an eager sender is destructed without joining it. This is the approach that std::thread destructor takes.

  2. A variation of (2): Request cancellation of the operation before detaching. This reduces the chances of operations continuing to run indefinitely in the background once they have been detached but does not solve the lifetime- or shutdown-related challenges.

  3. A variation of (3): Request cancellation of the operation before blocking on its completion. This is the strategy that std::jthread uses for its destructor. It reduces the risk of deadlock but does not eliminate it.

4.11.2. Eager senders complicate algorithm implementations

Algorithms that can assume they are operating on senders with strictly lazy semantics are able to make certain optimizations that are not available if senders can be potentially eager. With lazy senders, an algorithm can safely assume that a call to execution::start on an operation state strictly happens before the execution of that async operation. This frees the algorithm from needing to resolve potential race conditions. For example, consider an algorithm sequence that puts async operations in sequence by starting an operation only after the preceding one has completed. In an expression like sequence(a(), then(src, [] { b(); }), c()), one my reasonably assume that a(), b() and c() are sequenced and therefore do not need synchronisation. Eager algorithms break that assumption.

When an algorithm needs to deal with potentially eager senders, the potential race conditions can be resolved one of two ways, neither of which is desirable:

  1. Assume the worst and implement the algorithm defensively, assuming all senders are eager. This obviously has overheads both at runtime and in algorithm complexity. Resolving race conditions is hard.

  2. Require senders to declare whether they are eager or not with a query. Algorithms can then implement two different implementation strategies, one for strictly lazy senders and one for potentially eager senders. This addresses the performance problem of (1) while compounding the complexity problem.

4.11.3. Eager senders incur cancellation-related overhead

Another implication of the use of eager operations is with regards to cancellation. The eagerly executing operation will not have access to the caller’s stop token until the sender is connected to a receiver. If we still want to be able to cancel the eager operation then it will need to create a new stop source and pass its associated stop token down to child operations. Then when the returned sender is eventually connected it will register a stop callback with the receiver’s stop token that will request stop on the eager sender’s stop source.

As the eager operation does not know at the time that it is launched what the type of the receiver is going to be, and thus whether or not the stop token returned from execution::get_stop_token is an std::unstoppable_token or not, the eager operation is going to need to assume it might be later connected to a receiver with a stop token that might actually issue a stop request. Thus it needs to declare space in the operation state for a type-erased stop callback and incur the runtime overhead of supporting cancellation, even if cancellation will never be requested by the caller.

The eager operation will also need to do this to support sending a stop request to the eager operation in the case that the sender representing the eager work is destroyed before it has been joined (assuming strategy (5) or (6) listed above is chosen).

4.11.4. Eager senders cannot access execution context from the receiver

In sender/receiver, contextual information is passed from parent operations to their children by way of receivers. Information like stop tokens, allocators, current scheduler, priority, and deadline are propagated to child operations with custom receivers at the time the operation is connected. That way, each operation has the contextual information it needs before it is started.

But if the operation is started before it is connected to a receiver, then there isn’t a way for a parent operation to communicate contextual information to its child operations, which may complete before a receiver is ever attached.

4.12. Schedulers advertise their forward progress guarantees

To decide whether a scheduler (and its associated execution context) is sufficient for a specific task, it may be necessary to know what kind of forward progress guarantees it provides for the execution agents it creates. The C++ Standard defines the following forward progress guarantees:

This paper introduces a scheduler query function, get_forward_progress_guarantee, which returns one of the enumerators of a new enum type, forward_progress_guarantee. Each enumerator of forward_progress_guarantee corresponds to one of the aforementioned guarantees.

4.13. Most sender adaptors are pipeable

To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped) together with operator|. This mechanism is similar to the operator| composition that C++ range adaptors support and draws inspiration from piping in *nix shells. Pipeable sender adaptors take a sender as their first parameter and have no other sender parameters.

a | b will pass the sender a as the first argument to the pipeable sender adaptor b. Pipeable sender adaptors support partial application of the parameters after the first. For example, all of the following are equivalent:

execution::bulk(snd, N, [] (std::size_t i, auto d) {});
execution::bulk(N, [] (std::size_t i, auto d) {})(snd);
snd | execution::bulk(N, [] (std::size_t i, auto d) {});

Piping enables you to compose together senders with a linear syntax. Without it, you’d have to use either nested function call syntax, which would cause a syntactic inversion of the direction of control flow, or you’d have to introduce a temporary variable for each stage of the pipeline. Consider the following example where we want to execute first on a CPU thread pool, then on a CUDA GPU, then back on the CPU thread pool:

Syntax Style Example
Function call
(nested)
auto snd = execution::then(
             execution::transfer(
               execution::then(
                 execution::transfer(
                   execution::then(
                     execution::schedule(thread_pool.scheduler())
                     []{ return 123; }),
                   cuda::new_stream_scheduler()),
                 [](int i){ return 123 * 5; }),
               thread_pool.scheduler()),
             [](int i){ return i - 5; });
auto [result] = this_thread::sync_wait(snd).value();
// result == 610
Function call
(named temporaries)
auto snd0 = execution::schedule(thread_pool.scheduler());
auto snd1 = execution::then(snd0, []{ return 123; });
auto snd2 = execution::transfer(snd1, cuda::new_stream_scheduler());
auto snd3 = execution::then(snd2, [](int i){ return 123 * 5; })
auto snd4 = execution::transfer(snd3, thread_pool.scheduler())
auto snd5 = execution::then(snd4, [](int i){ return i - 5; });
auto [result] = *this_thread::sync_wait(snd4);
// result == 610
Pipe
auto snd = execution::schedule(thread_pool.scheduler())
         | execution::then([]{ return 123; })
         | execution::transfer(cuda::new_stream_scheduler())
         | execution::then([](int i){ return 123 * 5; })
         | execution::transfer(thread_pool.scheduler())
         | execution::then([](int i){ return i - 5; });
auto [result] = this_thread::sync_wait(snd).value();
// result == 610

Certain sender adaptors are not be pipeable, because using the pipeline syntax can result in confusion of the semantics of the adaptors involved. Specifically, the following sender adaptors are not pipeable.

Sender consumers could be made pipeable, but we have chosen to not do so. However, since these are terminal nodes in a pipeline and nothing can be piped after them, we believe a pipe syntax may be confusing as well as unnecessary, as consumers cannot be chained. We believe sender consumers read better with function call syntax.

4.14. A range of senders represents an async sequence of data

Senders represent a single unit of asynchronous work. In many cases though, what is being modelled is a sequence of data arriving asynchronously, and you want computation to happen on demand, when each element arrives. This requires nothing more than what is in this paper and the range support in C++20. A range of senders would allow you to model such input as keystrikes, mouse movements, sensor readings, or network requests.

Given some expression R that is a range of senders, consider the following in a coroutine that returns an async generator type:

for (auto snd : R) {
  if (auto opt = co_await execution::stopped_as_optional(std::move(snd)))
    co_yield fn(*std::move(opt));
  else
    break;
}

This transforms each element of the asynchronous sequence R with the function fn on demand, as the data arrives. The result is a new asynchronous sequence of the transformed values.

Now imagine that R is the simple expression views::iota(0) | views::transform(execution::just). This creates a lazy range of senders, each of which completes immediately with monotonically increasing integers. The above code churns through the range, generating a new infine asynchronous range of values [fn(0), fn(1), fn(2), ...].

Far more interesting would be if R were a range of senders representing, say, user actions in a UI. The above code gives a simple way to respond to user actions on demand.

4.15. Senders can represent partial success

Receivers have three ways they can complete: with success, failure, or cancellation. This begs the question of how they can be used to represent async operations that partially succeed. For example, consider an API that reads from a socket. The connection could drop after the API has filled in some of the buffer. In cases like that, it makes sense to want to report both that the connection dropped and that some data has been successfully read.

Often in the case of partial success, the error condition is not fatal nor does it mean the API has failed to satisfy its post-conditions. It is merely an extra piece of information about the nature of the completion. In those cases, "partial success" is another way of saying "success". As a result, it is sensible to pass both the error code and the result (if any) through the value channel, as shown below:

// Capture a buffer for read_socket_async to fill in
execution::just(array<byte, 1024>{})
  | execution::let_value([socket](array<byte, 1024>& buff) {
      // read_socket_async completes with two values: an error_code and
      // a count of bytes:
      return read_socket_async(socket, span{buff})
          // For success (partial and full), specify the next action:
        | execution::let_value([](error_code err, size_t bytes_read) {
            if (err != 0) {
              // OK, partial success. Decide how to deal with the partial results
            } else {
              // OK, full success here.
            }
          });
    })

In other cases, the partial success is more of a partial failure. That happens when the error condition indicates that in some way the function failed to satisfy its post-conditions. In those cases, sending the error through the value channel loses valuable contextual information. It’s possible that bundling the error and the incomplete results into an object and passing it through the error channel makes more sense. In that way, generic algorithms will not miss the fact that a post-condition has not been met and react inappropriately.

Another possibility is for an async API to return a range of senders: if the API completes with full success, full error, or cancellation, the returned range contains just one sender with the result. Otherwise, if the API partially fails (doesn’t satisfy its post-conditions, but some incomplete result is available), the returned range would have two senders: the first containing the partial result, and the second containing the error. Such an API might be used in a coroutine as follows:

// Declare a buffer for read_socket_async to fill in
array<byte, 1024> buff;

for (auto snd : read_socket_async(socket, span{buff})) {
  try {
    if (optional<size_t> bytes_read =
          co_await execution::stopped_as_optional(std::move(snd)))
      // OK, we read some bytes into buff. Process them here....
    } else {
      // The socket read was cancelled and returned no data. React
      // appropriately.
    }
  } catch (...) {
    // read_socket_async failed to meet its post-conditions.
    // Do some cleanup and propagate the error...
  }
}

Finally, it’s possible to combine these two approaches when the API can both partially succeed (meeting its post-conditions) and partially fail (not meeting its post-conditions).

4.16. All awaitables are senders

Since C++20 added coroutines to the standard, we expect that coroutines and awaitables will be how a great many will choose to express their asynchronous code. However, in this paper, we are proposing to add a suite of asynchronous algorithms that accept senders, not awaitables. One might wonder whether and how these algorithms will be accessible to those who choose coroutines instead of senders.

In truth there will be no problem because all generally awaitable types automatically model the sender concept. The adaptation is transparent and happens in the sender customization points, which are aware of awaitables. (By "generally awaitable" we mean types that don’t require custom await_transform trickery from a promise type to make them awaitable.)

For an example, imagine a coroutine type called task<T> that knows nothing about senders. It doesn’t implement any of the sender customization points. Despite that fact, and despite the fact that the this_thread::sync_wait algorithm is constrained with the sender concept, the following would compile and do what the user wants:

task<int> doSomeAsyncWork();

int main() {
  // OK, awaitable types satisfy the requirements for senders:
  auto o = this_thread::sync_wait(doSomeAsyncWork());
}

Since awaitables are senders, writing a sender-based asynchronous algorithm is trivial if you have a coroutine task type: implement the algorithm as a coroutine. If you are not bothered by the possibility of allocations and indirections as a result of using coroutines, then there is no need to ever write a sender, a receiver, or an operation state.

4.17. Many senders can be trivially made awaitable

If you choose to implement your sender-based algorithms as coroutines, you’ll run into the issue of how to retrieve results from a passed-in sender. This is not a problem. If the coroutine type opts in to sender support -- trivial with the execution::with_awaitable_senders utility -- then a large class of senders are transparently awaitable from within the coroutine.

For example, consider the following trivial implementation of the sender-based retry algorithm:

template <class S>
  requires single-sender<S&> // See [exec.as_awaitable]
task<single-sender-value-type<S>> retry(S s) {
  for (;;) {
    try {
      co_return co_await s;
    } catch(...) {
    }
  }
}

Only some senders can be made awaitable directly because of the fact that callbacks are more expressive than coroutines. An awaitable expression has a single type: the result value of the async operation. In contrast, a callback can accept multiple arguments as the result of an operation. What’s more, the callback can have overloaded function call signatures that take different sets of arguments. There is no way to automatically map such senders into awaitables. The with_awaitable_senders utility recognizes as awaitables those senders that send a single value of a single type. To await another kind of sender, a user would have to first map its value channel into a single value of a single type -- say, with the into_variant sender algorithm -- before co_await-ing that sender.

4.18. Cancellation of a sender can unwind a stack of coroutines

When looking at the sender-based retry algorithm in the previous section, we can see that the value and error cases are correctly handled. But what about cancellation? What happens to a coroutine that is suspended awaiting a sender that completes by calling execution::set_stopped?

When your task type’s promise inherits from with_awaitable_senders, what happens is this: the coroutine behaves as if an uncatchable exception had been thrown from the co_await expression. (It is not really an exception, but it’s helpful to think of it that way.) Provided that the promise types of the calling coroutines also inherit from with_awaitable_senders, or more generally implement a member function called unhandled_stopped, the exception unwinds the chain of coroutines as if an exception were thrown except that it bypasses catch(...) clauses.

In order to "catch" this uncatchable stopped exception, one of the calling coroutines in the stack would have to await a sender that maps the stopped channel into either a value or an error. That is achievable with the execution::let_stopped, execution::upon_stopped, execution::stopped_as_optional, or execution::stopped_as_error sender adaptors. For instance, we can use execution::stopped_as_optional to "catch" the stopped signal and map it into an empty optional as shown below:

if (auto opt = co_await execution::stopped_as_optional(some_sender)) {
  // OK, some_sender completed successfully, and opt contains the result.
} else {
  // some_sender completed with a cancellation signal.
}

As described in the section "All awaitables are senders", the sender customization points recognize awaitables and adapt them transparently to model the sender concept. When connect-ing an awaitable and a receiver, the adaptation layer awaits the awaitable within a coroutine that implements unhandled_stopped in its promise type. The effect of this is that an "uncatchable" stopped exception propagates seamlessly out of awaitables, causing execution::set_stopped to be called on the receiver.

Obviously, unhandled_stopped is a library extension of the coroutine promise interface. Many promise types will not implement unhandled_stopped. When an uncatchable stopped exception tries to propagate through such a coroutine, it is treated as an unhandled exception and terminate is called. The solution, as described above, is to use a sender adaptor to handle the stopped exception before awaiting it. It goes without saying that any future Standard Library coroutine types ought to implement unhandled_stopped. The author of [P1056R1], which proposes a standard coroutine task type, is in agreement.

4.19. Composition with parallel algorithms

The C++ Standard Library provides a large number of algorithms that offer the potential for non-sequential execution via the use of execution policies. The set of algorithms with execution policy overloads are often referred to as "parallel algorithms", although additional policies are available.

Existing policies, such as execution::par, give the implementation permission to execute the algorithm in parallel. However, the choice of execution resources used to perform the work is left to the implementation.

We will propose a customization point for combining schedulers with policies in order to provide control over where work will execute.

template<class ExecutionPolicy>
implementation-defined executing_on(
    execution::scheduler auto scheduler,
    ExecutionPolicy && policy
);

This function would return an object of an implementation-defined type which can be used in place of an execution policy as the first argument to one of the parallel algorithms. The overload selected by that object should execute its computation as requested by policy while using scheduler to create any work to be run. The expression may be ill-formed if scheduler is not able to support the given policy.

The existing parallel algorithms are synchronous; all of the effects performed by the computation are complete before the algorithm returns to its caller. This remains unchanged with the executing_on customization point.

In the future, we expect additional papers will propose asynchronous forms of the parallel algorithms which (1) return senders rather than values or void and (2) where a customization point pairing a sender with an execution policy would similarly be used to obtain an object of implementation-defined type to be provided as the first argument to the algorithm.

4.20. User-facing sender factories

A sender factory is an algorithm that takes no senders as parameters and returns a sender.

4.20.1. execution::schedule

execution::sender auto schedule(
    execution::scheduler auto scheduler
);

Returns a sender describing the start of a task graph on the provided scheduler. See § 4.2 Schedulers represent execution contexts.

execution::scheduler auto sch1 = get_system_thread_pool().scheduler();

execution::sender auto snd1 = execution::schedule(sch1);
// snd1 describes the creation of a new task on the system thread pool

4.20.2. execution::just

execution::sender auto just(
    auto ...&& values
);

Returns a sender with no completion schedulers, which sends the provided values. The input values are decay-copied into the returned sender. When the returned sender is connected to a receiver, the values are moved into the operation state if the sender is an rvalue; otherwise, they are copied. Then xvalues referencing the values in the operation state are passed to the receiver’s set_value.

execution::sender auto snd1 = execution::just(3.14);
execution::sender auto then1 = execution::then(snd1, [] (double d) {
  std::cout << d << "\n";
});

execution::sender auto snd2 = execution::just(3.14, 42);
execution::sender auto then2 = execution::then(snd1, [] (double d, int i) {
  std::cout << d << ", " << i << "\n";
});

std::vector v3{1, 2, 3, 4, 5};
execution::sender auto snd3 = execution::just(v3);
execution::sender auto then3 = execution::then(snd3, [] (std::vector<int>&& v3copy) {
  for (auto&& e : v3copy) { e *= 2; }
  return std::move(v3copy);
}
auto&& [v3copy] = this_thread::sync_wait(then3).value();
// v3 contains {1, 2, 3, 4, 5}; v3copy will contain {2, 4, 6, 8, 10}.

execution::sender auto snd4 = execution::just(std::vector{1, 2, 3, 4, 5});
execution::sender auto then4 = execution::then(std::move(snd4), [] (std::vector<int>&& v4) {
  for (auto&& e : v4) { e *= 2; }
  return std::move(v4);
});
auto&& [v4] = this_thread::sync_wait(std::move(then4)).value();
// v4 contains {2, 4, 6, 8, 10}. No vectors were copied in this example.

4.20.3. execution::transfer_just

execution::sender auto transfer_just(
    execution::scheduler auto scheduler,
    auto ...&& values
);

Returns a sender whose value completion scheduler is the provided scheduler, which sends the provided values in the same manner as just.

execution::sender auto vals = execution::transfer_just(
    get_system_thread_pool().scheduler(),
    1, 2, 3
);
execution::sender auto snd = execution::then(vals, [](auto... args) {
    std::print(args...);
});
// when snd is executed, it will print "123"

This adaptor is included as it greatly simplifies lifting values into senders.

4.20.4. execution::just_error

execution::sender auto just_error(
    auto && error
);

Returns a sender with no completion schedulers, which completes with the specified error. If the provided error is an lvalue reference, a copy is made inside the returned sender and a non-const lvalue reference to the copy is sent to the receiver’s set_error. If the provided value is an rvalue reference, it is moved into the returned sender and an rvalue reference to it is sent to the receiver’s set_error.

4.20.5. execution::just_stopped

execution::sender auto just_stopped();

Returns a sender with no completion schedulers, which completes immediately by calling the receiver’s set_stopped.

4.20.6. execution::read

execution::sender auto read(auto tag);

execution::sender auto get_scheduler() {
  return read(execution::get_scheduler);
}
execution::sender auto get_delegatee_scheduler() {
  return read(execution::get_delegatee_scheduler);
}
execution::sender auto get_allocator() {
  return read(execution::get_allocator);
}
execution::sender auto get_stop_token() {
  return read(execution::get_stop_token);
}

Returns a sender that reaches into a receiver’s environment and pulls out the current value associated with the customization point denoted by Tag. It then sends the value read back to the receiver through the value channel. For instance, get_scheduler() (with no arguments) is a sender that asks the receiver for the currently suggested scheduler and passes it to the receiver’s set_value completion-signal.

This can be useful when scheduling nested dependent work. The following sender pulls the current schduler into the value channel and then schedules more work onto it.

execution::sender auto task =
  execution::get_scheduler()
    | execution::let_value([](auto sched) {
        return execution::on(sched, some nested work here);
    });

this_thread::sync_wait( std::move(task) ); // wait for it to finish

This code uses the fact that sync_wait associates a scheduler with the receiver that it connects with task. get_scheduler() reads that scheduler out of the receiver, and passes it to let_value's receiver’s set_value function, which in turn passes it to the lambda. That lambda returns a new sender that uses the scheduler to schedule some nested work onto sync_wait's scheduler.

4.21. User-facing sender adaptors

A sender adaptor is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and returns a sender, whose completion is related to the sender arguments it has received.

Sender adaptors are lazy, that is, they are never allowed to submit any work for execution prior to the returned sender being started later on, and are also guaranteed to not start any input senders passed into them. Sender consumers such as § 4.21.13 execution::ensure_started, § 4.22.1 execution::start_detached, and § 4.22.2 this_thread::sync_wait start senders.

For more implementer-centric description of starting senders, see § 5.5 Sender adaptors are lazy.

4.21.1. execution::transfer

execution::sender auto transfer(
    execution::sender auto input,
    execution::scheduler auto scheduler
);

Returns a sender describing the transition from the execution agent of the input sender to the execution agent of the target scheduler. See § 4.6 Execution context transitions are explicit.

execution::scheduler auto cpu_sched = get_system_thread_pool().scheduler();
execution::scheduler auto gpu_sched = cuda::scheduler();

execution::sender auto cpu_task = execution::schedule(cpu_sched);
// cpu_task describes the creation of a new task on the system thread pool

execution::sender auto gpu_task = execution::transfer(cpu_task, gpu_sched);
// gpu_task describes the transition of the task graph described by cpu_task to the gpu

4.21.2. execution::then

execution::sender auto then(
    execution::sender auto input,
    std::invocable<values-sent-by(input)...> function
);

then returns a sender describing the task graph described by the input sender, with an added node of invoking the provided function with the values sent by the input sender as arguments.

then is guaranteed to not begin executing function until the returned sender is started.

execution::sender auto input = get_input();
execution::sender auto snd = execution::then(input, [](auto... args) {
    std::print(args...);
});
// snd describes the work described by pred
// followed by printing all of the values sent by pred

This adaptor is included as it is necessary for writing any sender code that actually performs a useful function.

4.21.3. execution::upon_*

execution::sender auto upon_error(
    execution::sender auto input,
    std::invocable<errors-sent-by(input)...> function
);

execution::sender auto upon_stopped(
    execution::sender auto input,
    std::invocable auto function
);

upon_error and upon_stopped are similar to then, but where then works with values sent by the input sender, upon_error works with errors, and upon_stopped is invoked when the "stopped" signal is sent.

4.21.4. execution::let_*

execution::sender auto let_value(
    execution::sender auto input,
    std::invocable<values-sent-by(input)...> function
);

execution::sender auto let_error(
    execution::sender auto input,
    std::invocable<errors-sent-by(input)...> function
);

execution::sender auto let_stopped(
    execution::sender auto input,
    std::invocable auto function
);

let_value is very similar to then: when it is started, it invokes the provided function with the values sent by the input sender as arguments. However, where the sender returned from then sends exactly what that function ends up returning - let_value requires that the function return a sender, and the sender returned by let_value sends the values sent by the sender returned from the callback. This is similar to the notion of "future unwrapping" in future/promise-based frameworks.

let_value is guaranteed to not begin executing function until the returned sender is started.

let_error and let_stopped are similar to let_value, but where let_value works with values sent by the input sender, let_error works with errors, and let_stopped is invoked when the "stopped" signal is sent.

4.21.5. execution::on

execution::sender auto on(
    execution::scheduler auto sched,
    execution::sender auto snd
);

Returns a sender which, when started, will start the provided sender on an execution agent belonging to the execution context associated with the provided scheduler. This returned sender has no completion schedulers.

4.21.6. execution::into_variant

execution::sender auto into_variant(
    execution::sender auto snd
);

Returns a sender which sends a variant of tuples of all the possible sets of types sent by the input sender. Senders can send multiple sets of values depending on runtime conditions; this is a helper function that turns them into a single variant value.

4.21.7. execution::stopped_as_optional

execution::sender auto stopped_as_optional(
    single-sender auto snd
);

Returns a sender that maps the value channel from a T to an optional<decay_t<T>>, and maps the stopped channel to a value of an empty optional<decay_t<T>>.

4.21.8. execution::stopped_as_error

template<move_constructible Error>
execution::sender auto stopped_as_error(
    execution::sender auto snd,
    Error err
);

Returns a sender that maps the stopped channel to an error of err.

4.21.9. execution::bulk

execution::sender auto bulk(
    execution::sender auto input,
    std::integral auto size,
    invocable<decltype(size), values-sent-by(input)...> function
);

Returns a sender describing the task of invoking the provided function with every index in the provided shape along with the values sent by the input sender. The returned sender completes once all invocations have completed, or an error has occurred. If it completes by sending values, they are equivalent to those sent by the input sender.

No instance of function will begin executing until the returned sender is started. Each invocation of function runs in an execution agent whose forward progress guarantees are determined by the scheduler on which they are run. All agents created by a single use of bulk execute with the same guarantee. This allows, for instance, a scheduler to execute all invocations of the function in parallel.

The bulk operation is intended to be used at the point where the number of agents to be created is known and provided to bulk via its shape parameter. For some parallel computations, the number of agents to be created may be a function of the input data or dynamic conditions of the execution environment. In such cases, bulk can be combined with additional operations such as let_value to deliver dynamic shape information to the bulk operation.

In this proposal, only integral types are used to specify the shape of the bulk section. We expect that future papers may wish to explore extensions of the interface to explore additional kinds of shapes, such as multi-dimensional grids, that are commonly used for parallel computing tasks.

4.21.10. execution::split

execution::sender auto split(execution::sender auto sender);

If the provided sender is a multi-shot sender, returns that sender. Otherwise, returns a multi-shot sender which sends values equivalent to the values sent by the provided sender. See § 4.7 Senders can be either multi-shot or single-shot.

4.21.11. execution::when_all

execution::sender auto when_all(
    execution::sender auto ...inputs
);

execution::sender auto when_all_with_variant(
    execution::sender auto ...inputs
);

when_all returns a sender that completes once all of the input senders have completed. It is constrained to only accept senders that can complete with a single set of values (_i.e._, it only calls one overload of set_value on its receiver). The values sent by this sender are the values sent by each of the input senders, in order of the arguments passed to when_all. It completes inline on the execution context on which the last input sender completes, unless stop is requested before when_all is started, in which case it completes inline within the call to start.

when_all_with_variant does the same, but it adapts all the input senders using into_variant, and so it does not constrain the input arguments as when_all does.

The returned sender has no completion schedulers.

See § 4.9 Senders are joinable.

execution::scheduler auto sched = thread_pool.scheduler();

execution::sender auto sends_1 = ...;
execution::sender auto sends_abc = ...;

execution::sender auto both = execution::when_all(sched,
    sends_1,
    sends_abc
);

execution::sender auto final = execution::then(both, [](auto... args){
    std::cout << std::format("the two args: {}, {}", args...);
});
// when final executes, it will print "the two args: 1, abc"

4.21.12. execution::transfer_when_all

execution::sender auto transfer_when_all(
    execution::scheduler auto sched,
    execution::sender auto ...inputs
);

execution::sender auto transfer_when_all_with_variant(
    execution::scheduler auto sched,
    execution::sender auto ...inputs
);

Similar to § 4.21.11 execution::when_all, but returns a sender whose value completion scheduler is the provided scheduler.

See § 4.9 Senders are joinable.

4.21.13. execution::ensure_started

execution::sender auto ensure_started(
    execution::sender auto sender
);

Once ensure_started returns, it is known that the provided sender has been connected and start has been called on the resulting operation state (see § 5.2 Operation states represent work); in other words, the work described by the provided sender has been submitted for execution on the appropriate execution contexts. Returns a sender which completes when the provided sender completes and sends values equivalent to those of the provided sender.

If the returned sender is destroyed before execution::connect() is called, or if execution::connect() is called but the returned operation-state is destroyed before execution::start() is called, then a stop-request is sent to the eagerly launched operation and the operation is detached and will run to completion in the background. Its result will be discarded when it eventually completes.

Note that the application will need to make sure that resources are kept alive in the case that the operation detaches. e.g. by holding a std::shared_ptr to those resources or otherwise having some out-of-band way to signal completion of the operation so that resource release can be sequenced after the completion.

4.22. User-facing sender consumers

A sender consumer is an algorithm that takes one or more senders, which it may execution::connect, as parameters, and does not return a sender.

4.22.1. execution::start_detached

void start_detached(
    execution::sender auto sender
);

Like ensure_started, but does not return a value; if the provided sender sends an error instead of a value, std::terminate is called.

4.22.2. this_thread::sync_wait

auto sync_wait(
    execution::sender auto sender
) requires (always-sends-same-values(sender))
    -> std::optional<std::tuple<values-sent-by(sender)>>;

this_thread::sync_wait is a sender consumer that submits the work described by the provided sender for execution, similarly to ensure_started, except that it blocks the current std::thread or thread of main until the work is completed, and returns an optional tuple of values that were sent by the provided sender on its completion of work. Where § 4.20.1 execution::schedule and § 4.20.3 execution::transfer_just are meant to enter the domain of senders, sync_wait is meant to exit the domain of senders, retrieving the result of the task graph.

If the provided sender sends an error instead of values, sync_wait throws that error as an exception, or rethrows the original exception if the error is of type std::exception_ptr.

If the provided sender sends the "stopped" signal instead of values, sync_wait returns an empty optional.

For an explanation of the requires clause, see § 5.8 All senders are typed. That clause also explains another sender consumer, built on top of sync_wait: sync_wait_with_variant.

Note: This function is specified inside std::this_thread, and not inside execution. This is because sync_wait has to block the current execution agent, but determining what the current execution agent is is not reliable. Since the standard does not specify any functions on the current execution agent other than those in std::this_thread, this is the flavor of this function that is being proposed. If C++ ever obtains fibers, for instance, we expect that a variant of this function called std::this_fiber::sync_wait would be provided. We also expect that runtimes with execution agents that use different synchronization mechanisms than std::thread's will provide their own flavors of sync_wait as well (assuming their execution agents have the means to block in a non-deadlock manner).

4.23. execution::execute

In addition to the three categories of functions presented above, we also propose to include a convenience function for fire-and-forget eager one-way submission of an invocable to a scheduler, to fulfil the role of one-way executors from P0443.

void execution::execute(
    execution::schedule auto sched,
    std::invocable auto fn
);

Submits the provided function for execution on the provided scheduler, as-if by:

auto snd = execution::schedule(sched);
auto work = execution::then(snd, fn);
execution::start_detached(work);

5. Design - implementer side

5.1. Receivers serve as glue between senders

A receiver is a callback that supports more than one channel. In fact, it supports three of them:

Exactly one of these channels must be successfully (i.e. without an exception being thrown) invoked on a receiver before it is destroyed; if a call to set_value failed with an exception, either set_error or set_stopped must be invoked on the same receiver. These requirements are know as the receiver contract.

While the receiver interface may look novel, it is in fact very similar to the interface of std::promise, which provides the first two signals as set_value and set_error, and it’s possible to emulate the third channel with lifetime management of the promise.

Receivers are not a part of the end-user-facing API of this proposal; they are necessary to allow unrelated senders communicate with each other, but the only users who will interact with receivers directly are authors of senders.

Receivers are what is passed as the second argument to § 5.3 execution::connect.

5.2. Operation states represent work

An operation state is an object that represents work. Unlike senders, it is not a chaining mechanism; instead, it is a concrete object that packages the work described by a full sender chain, ready to be executed. An operation state is neither movable nor copyable, and its interface consists of a single algorithm: start, which serves as the submission point of the work represented by a given operation state.

Operation states are not a part of the user-facing API of this proposal; they are necessary for implementing sender consumers like execution::ensure_started and this_thread::sync_wait, and the knowledge of them is necessary to implement senders, so the only users who will interact with operation states directly are authors of senders and authors of sender algorithms.

The return value of § 5.3 execution::connect must satisfy the operation state concept.

5.3. execution::connect

execution::connect is a customization point which connects senders with receivers, resulting in an operation state that will ensure that the receiver contract of the receiver passed to connect will be fulfilled.

execution::sender auto snd = some input sender;
execution::receiver auto rcv = some receiver;
execution::operation_state auto state = execution::connect(snd, rcv);

execution::start(state);
// at this point, it is guaranteed that the work represented by state has been submitted
// to an execution context, and that execution context will eventually fulfill the
// receiver contract of rcv

// operation states are not movable, and therefore this operation state object must be
// kept alive until the operation finishes

5.4. Sender algorithms are customizable

Senders being able to advertise what their completion schedulers are fulfills one of the promises of senders: that of being able to customize an implementation of a sender algorithm based on what scheduler any work it depends on will complete on.

The simple way to provide customizations for functions like then, that is for sender adaptors and sender consumers, is to follow the customization scheme that has been adopted for C++20 ranges library; to do that, we would define the expression execution::then(sender, invocable) to be equivalent to:

  1. sender.then(invocable), if that expression is well formed; otherwise

  2. then(sender, invocable), performed in a context where this call always performs ADL, if that expression is well formed; otherwise

  3. a default implementation of then, which returns a sender adaptor, and then define the exact semantics of said adaptor.

However, this definition is problematic. Imagine another sender adaptor, bulk, which is a structured abstraction for a loop over an index space. Its default implementation is just a for loop. However, for accelerator runtimes like CUDA, we would like sender algorithms like bulk to have specialized behavior, which invokes a kernel of more than one thread (with its size defined by the call to bulk); therefore, we would like to customize bulk for CUDA senders to achieve this. However, there’s no reason for CUDA kernels to necessarily customize the then sender adaptor, as the generic implementation is perfectly sufficient. This creates a problem, though; consider the following snippet:

execution::scheduler auto cuda_sch = cuda_scheduler{};

execution::sender auto initial = execution::schedule(cuda_sch);
// the type of initial is a type defined by the cuda_scheduler
// let’s call it cuda::schedule_sender<>

execution::sender auto next = execution::then(cuda_sch, []{ return 1; });
// the type of next is a standard-library implementation-defined sender adaptor
// that wraps the cuda sender
// let’s call it execution::then_sender_adaptor<cuda::schedule_sender<>>

execution::sender auto kernel_sender = execution::bulk(next, shape, [](int i){ ... });

How can we specialize the bulk sender adaptor for our wrapped schedule_sender? Well, here’s one possible approach, taking advantage of ADL (and the fact that the definition of "associated namespace" also recursively enumerates the associated namespaces of all template parameters of a type):

namespace cuda::for_adl_purposes {
template<typename... SentValues>
class schedule_sender {
    execution::operation_state auto connect(execution::receiver auto rcv);
    execution::scheduler auto get_completion_scheduler() const;
};

execution::sender auto bulk(
    execution::sender auto && input,
    execution::shape auto && shape,
    invocable<sender-values(input)> auto && fn)
{
    // return a cuda sender representing a bulk kernel launch
}
} // namespace cuda::for_adl_purposes

However, if the input sender is not just a then_sender_adaptor like in the example above, but another sender that overrides bulk by itself, as a member function, because its author believes they know an optimization for bulk - the specialization above will no longer be selected, because a member function of the first argument is a better match than the ADL-found overload.

This means that well-meant specialization of sender algorithms that are entirely scheduler-agnostic can have negative consequences. The scheduler-specific specialization - which is essential for good performance on platforms providing specialized ways to launch certain sender algorithms - would not be selected in such cases. But it’s really the scheduler that should control the behavior of sender algorithms when a non-default implementation exists, not the sender. Senders merely describe work; schedulers, however, are the handle to the runtime that will eventually execute said work, and should thus have the final say in how the work is going to be executed.

Therefore, we are proposing the following customization scheme (also modified to take § 5.9 Ranges-style CPOs vs tag_invoke into account): the expression execution::<sender-algorithm>(sender, args...), for any given sender algorithm that accepts a sender as its first argument, should be equivalent to:

  1. tag_invoke(<sender-algorithm>, get_completion_scheduler<Signal>(sender), sender, args...), if that expression is well-formed; otherwise

  2. tag_invoke(<sender-algorithm>, sender, args...), if that expression is well-formed; otherwise

  3. a default implementation, if there exists a default implementation of the given sender algorithm.

where Signal is one of set_value, set_error, or set_stopped; for most sender algorithms, the completion scheduler for set_value would be used, but for some (like upon_error or let_stopped), one of the others would be used.

For sender algorithms which accept concepts other than sender as their first argument, we propose that the customization scheme remains as it has been in [P0443R14] so far, except it should also use tag_invoke.

5.5. Sender adaptors are lazy

Contrary to early revisions of this paper, we propose to make all sender adaptors perform strictly lazy submission, unless specified otherwise (the one notable exception in this paper is § 4.21.13 execution::ensure_started, whose sole purpose is to start an input sender).

Strictly lazy submission means that there is a guarantee that no work is submitted to an execution context before a receiver is connected to a sender, and execution::start is called on the resulting operation state.

5.6. Lazy senders provide optimization opportunities

Because lazy senders fundamentally describe work, instead of describing or representing the submission of said work to an execution context, and thanks to the flexibility of the customization of most sender algorithms, they provide an opportunity for fusing multiple algorithms in a sender chain together, into a single function that can later be submitted for execution by an execution context. There are two ways this can happen.

The first (and most common) way for such optimizations to happen is thanks to the structure of the implementation: because all the work is done within callbacks invoked on the completion of an earlier sender, recursively up to the original source of computation, the compiler is able to see a chain of work described using senders as a tree of tail calls, allowing for inlining and removal of most of the sender machinery. In fact, when work is not submitted to execution contexts outside of the current thread of execution, compilers are capable of removing the senders abstraction entirely, while still allowing for composition of functions across different parts of a program.

The second way for this to occur is when a sender algorithm is specialized for a specific set of arguments. For instance, we expect that, for senders which are known to have been started already, § 4.21.13 execution::ensure_started will be an identity transformation, because the sender algorithm will be specialized for such senders. Similarly, an implementation could recognize two subsequent § 4.21.9 execution::bulks of compatible shapes, and merge them together into a single submission of a GPU kernel.

5.7. Execution context transitions are two-step

Because execution::transfer takes a sender as its first argument, it is not actually directly customizable by the target scheduler. This is by design: the target scheduler may not know how to transition from a scheduler such as a CUDA scheduler; transitioning away from a GPU in an efficient manner requires making runtime calls that are specific to the GPU in question, and the same is usually true for other kinds of accelerators too (or for scheduler running on remote systems). To avoid this problem, specialized schedulers like the ones mentioned here can still hook into the transition mechanism, and inject a sender which will perform a transition to the regular CPU execution context, so that any sender can be attached to it.

This, however, is a problem: because customization of sender algorithms must be controlled by the scheduler they will run on (see § 5.4 Sender algorithms are customizable), the type of the sender returned from transfer must be controllable by the target scheduler. Besides, the target scheduler may itself represent a specialized execution context, which requires additional work to be performed to transition to it. GPUs and remote node schedulers are once again good examples of such schedulers: executing code on their execution contexts requires making runtime API calls for work submission, and quite possibly for the data movement of the values being sent by the input sender passed into transfer.

To allow for such customization from both ends, we propose the inclusion of a secondary transitioning sender adaptor, called schedule_from. This adaptor is a form of schedule, but takes an additional, second argument: the input sender. This adaptor is not meant to be invoked manually by the end users; they are always supposed to invoke transfer, to ensure that both schedulers have a say in how the transitions are made. Any scheduler that specializes transfer(snd, sch) shall ensure that the return value of their customization is equivalent to schedule_from(sch, snd2), where snd2 is a successor of snd that sends values equivalent to those sent by snd.

The default implementation of transfer(snd, sched) is schedule_from(sched, snd).

5.8. All senders are typed

All senders must advertise the types they will send when they complete. This is necessary for a number of features, and writing code in a way that’s agnostic of whether an input sender is typed or not in common sender adaptors such as execution::then is hard.

The mechanism for this advertisement is similar to the one in [P0443R14]; the way to query the types is through completion_signatures_of_t<S, [Env]>::value_types<tuple_like, variant_like>.

completion_signatures_of_t::value_types is a template that takes two arguments: one is a tuple-like template, the other is a variant-like template. The tuple-like argument is required to represent senders sending more than one value (such as when_all). The variant-like argument is required to represent senders that choose which specific values to send at runtime.

There’s a choice made in the specification of § 4.22.2 this_thread::sync_wait: it returns a tuple of values sent by the sender passed to it, wrapped in std::optional to handle the set_stopped signal. However, this assumes that those values can be represented as a tuple, like here:

execution::sender auto sends_1 = ...;
execution::sender auto sends_2 = ...;
execution::sender auto sends_3 = ...;

auto [a, b, c] = this_thread::sync_wait(
    execution::transfer_when_all(
        execution::get_completion_scheduler<execution::set_value_t>(sends_1),
        sends_1,
        sends_2,
        sends_3
    )).value();
// a == 1
// b == 2
// c == 3

This works well for senders that always send the same set of arguments. If we ignore the possibility of having a sender that sends different sets of arguments into a receiver, we can specify the "canonical" (i.e. required to be followed by all senders) form of value_types of a sender which sends Types... to be as follows:

template<template<typename ...> typename TupleLike>
using value_types = TupleLike;

If senders could only ever send one specific set of values, this would probably need to be the required form of value_types for all senders; defining it otherwise would cause very weird results and should be considered a bug.

This matter is somewhat complicated by the fact that (1) set_value for receivers can be overloaded and accept different sets of arguments, and (2) senders are allowed to send multiple different sets of values, depending on runtime conditions, the data they consumed, and so on. To accomodate this, [P0443R14] also includes a second template parameter to value_types, one that represents a variant-like type. If we permit such senders, we would almost certainly need to require that the canonical form of value_types for all senders (to ensure consistency in how they are handled, and to avoid accidentally interpreting a user-provided variant as a sender-provided one) sending the different sets of arguments Types1..., Types2..., ..., TypesN... to be as follows:

template<
    template<typename ...> typename TupleLike,
    template<typename ...> typename VariantLike
>
using value_types = VariantLike<
    TupleLike<Types1...>,
    TupleLike<Types2...>,
    ...,
    TupleLike<Types3...>
>;

This, however, introduces a couple of complications:

  1. A just(1) sender would also need to follow this structure, so the correct type for storing the value sent by it would be std::variant<std::tuple<int>> or some such. This introduces a lot of compile time overhead for the simplest senders, and this overhead effectively exists in all places in the code where value_types is queried, regardless of the tuple-like and variant-like templates passed to it. Such overhead does exist if only the tuple-like parameter exists, but is made much worse by adding this second wrapping layer.

  2. As a consequence of (1): because sync_wait needs to store the above type, it can no longer return just a std::tuple<int> for just(1); it has to return std::variant<std::tuple<int>>. C++ currently does not have an easy way to destructure this; it may get less awkward with pattern matching, but even then it seems extremely heavyweight to involve variants in this API, and for the purpose of generic code, the kind of the return type of sync_wait must be the same across all sender types.

One possible solution to (2) above is to place a requirement on sync_wait that it can only accept senders which send only a single set of values, therefore removing the need for std::variant to appear in its API; because of this, we propose to expose both sync_wait, which is a simple, user-friendly version of the sender consumer, but requires that value_types have only one possible variant, and sync_wait_with_variant, which accepts any sender, but returns an optional whose value type is the variant of all the possible tuples sent by the input sender:

auto sync_wait_with_variant(
    execution::sender auto sender
) -> std::optional<std::variant<
        std::tuple<values0-sent-by(sender)>,
        std::tuple<values1-sent-by(sender)>,
        ...,
        std::tuple<valuesn-sent-by(sender)>
    >>;

auto sync_wait(
    execution::sender auto sender
) requires (always-sends-same-values(sender))
    -> std::optional<std::tuple<values-sent-by(sender)>>;

5.9. Ranges-style CPOs vs tag_invoke

The contemporary technique for customization in the Standard Library is customization point objects. A customization point object, will it look for member functions and then for nonmember functions with the same name as the customization point, and calls those if they match. This is the technique used by the C++20 ranges library, and previous executors proposals ([P0443R14] and [P1897R3]) intended to use it as well. However, it has several unfortunate consequences:

  1. It does not allow for easy propagation of customization points unknown to the adaptor to a wrapped object, which makes writing universal adapter types much harder - and this proposal uses quite a lot of those.

  2. It effectively reserves names globally. Because neither member names nor ADL-found functions can be qualified with a namespace, every customization point object that uses the ranges scheme reserves the name for all types in all namespaces. This is unfortunate due to the sheer number of customization points already in the paper, but also ones that we are envisioning in the future. It’s also a big problem for one of the operations being proposed already: sync_wait. We imagine that if, in the future, C++ was to gain fibers support, we would want to also have std::this_fiber::sync_wait, in addition to std::this_thread::sync_wait. However, because we would want the names to be the same in both cases, we would need to make the names of the customizations not match the names of the customization points. This is undesirable.

This paper proposes to instead use the mechanism described in [P1895R0]: tag_invoke; the wording for tag_invoke has been incorporated into the proposed specification in this paper.

In short, instead of using globally reserved names, tag_invoke uses the type of the customization point object itself as the mechanism to find customizations. It globally reserves only a single name - tag_invoke - which itself is used the same way that ranges-style customization points are used. All other customization points are defined in terms of tag_invoke. For example, the customization for std::this_thread::sync_wait(s) will call tag_invoke(std::this_thread::sync_wait, s), instead of attempting to invoke s.sync_wait(), and then sync_wait(s) if the member call is not valid.

Using tag_invoke has the following benefits:

  1. It reserves only a single global name, instead of reserving a global name for every customization point object we define.

  2. It is possible to propagate customizations to a subobject, because the information of which customization point is being resolved is in the type of an argument, and not in the name of the function:

    // forward most customizations to a subobject
    template<typename Tag, typename ...Args>
    friend auto tag_invoke(Tag && tag, wrapper & self, Args &&... args) {
        return std::forward<Tag>(tag)(self.subobject, std::forward<Args>(args)...);
    }
    
    // but override one of them with a specific value
    friend auto tag_invoke(specific_customization_point_t, wrapper & self) {
        return self.some_value;
    }
    
  3. It is possible to pass those as template arguments to types, because the information of which customization point is being resolved is in the type. Similarly to how [P0443R14] defines a polymorphic executor wrapper which accepts a list of properties it supports, we can imagine scheduler and sender wrappers that accept a list of queries and operations they support. That list can contain the types of the customization point objects, and the polymorphic wrappers can then specialize those customization points on themselves using tag_invoke, dispatching to manually constructed vtables containing pointers to specialized implementations for the wrapped objects. For an example of such a polymorphic wrapper, see unifex::any_unique (example).

6. Specification

Much of this wording follows the wording of [P0443R14].

§ 7 Library introduction [library] is meant to be a diff relative to the wording of the [library] clause of [N4885].

§ 8 General utilities library [utilities] is meant to be a diff relative to the wording of the [utilities] clause of [N4885]. This diff applies changes from [P1895R0].

§ 9 Thread support library [thread] is meant to be a diff relative to the wording of the [thread] clause of [N4885]. This diff applies changes from [P2175R0].

§ 10 Execution control library [exec] is meant to be added as a new library clause to the working draft of C++.

7. Library introduction [library]

[Editorial: Add the header <execution> to Table 23: C++ library headers [tab:headers.cpp]]

In subclause [conforming], after [lib.types.movedfrom], add the following new subclause with suggested stable name [lib.tmpl-heads].

16.4.6.17 Class template-heads
  1. If a class template’s template-head is marked with "arguments are not associated entities"", any template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity. In such a case, the class template may be implemented as an alias template referring to a templated class, or as a class template where the template arguments themselves are templated classes.

  2. [Example:

    template<class T> // arguments are not associated entities
    struct S {};
    
    namespace N {
      int f(auto);
      struct A {};
    }
    
    int x = f(S<N::A>{});  // error: N::f not a candidate
    

    The template S specified above may be implemented as

    template<class T>
    struct s-impl {
      struct type { };
    };
    
    template<class T>
    using S = typename s-impl<T>::type;
    

    or as

    template<class T>
    struct hidden {
      using type = struct _ {
        using type = T;
      };
    };
    
    template<class HiddenT>
    struct s-impl {
      using T = typename HiddenT::type;
    };
    
    template<class T>
    using S = s-impl<typename hidden<T>::type>;
    

    -- end example]

8. General utilities library [utilities]

8.1. Function objects [function.objects]

8.1.1. Header <functional> synopsis [functional.syn]

At the end of this subclause, insert the following declarations into the synopsis within namespace std:

// Expositon-only:
template <class Fn, class... Args>
  concept callable =
    requires (Fn&& fn, Args&&... args) {
      std::forward<Fn>(fn)(std::forward<Args>(args)...);
    };
template <class Fn, class... Args>
  concept nothrow-callable =
    callable<Fn, Args...> &&
    requires (Fn&& fn, Args&&... args) {
      { std::forward<Fn>(fn)(std::forward<Args>(args)...) } noexcept;
    };
template <class Fn, class... Args>
  using call-result-t = decltype(declval<Fn>()(declval<Args>()...));

// [func.tag_invoke], tag_invoke
namespace tag-invoke { // exposition only
  void tag_invoke();

  template <class Tag, class... Args>
    concept tag_invocable =
      requires (Tag&& tag, Args&&... args) {
        tag_invoke(std::forward<Tag>(tag), std::forward<Args>(args)...);
      };

  template <class Tag, class... Args>
    concept nothrow_tag_invocable =
      tag_invocable<Tag, Args...> &&
      requires (Tag&& tag, Args&&... args) {
        { tag_invoke(std::forward<Tag>(tag), std::forward<Args>(args)...) } noexcept;
      };

  template <class Tag, class... Args>
    using tag_invoke_result_t =
      decltype(tag_invoke(declval<Tag>(), declval<Args>()...));

  template <class Tag, class... Args>
    struct tag_invoke_result {};

  template <class Tag, class... Args>
      requires tag_invocable<Tag, Args...>
    struct tag_invoke_result<Tag, Args...> {
      using type = tag_invoke_result_t<Tag, Args...>;
    };

  struct tag; // exposition only  
}
inline constexpr tag-invoke::tag tag_invoke {};
using tag-invoke::tag_invocable;
using tag-invoke::nothrow_tag_invocable;
using tag-invoke::tag_invoke_result_t;
using tag-invoke::tag_invoke_result;

template<auto& Tag>
  using tag_t = decay_t<decltype(Tag)>;

8.1.2. tag_invoke [func.tag_invoke]

Insert this section as a new subclause, between Searchers [func.search] and Class template hash [unord.hash].

  1. The name std::tag_invoke denotes a customization point object [customization.point.object]. Given subexpressions T and A..., the expression std::tag_invoke(T, A...) is expression-equivalent [defns.expression-equivalent] to tag_invoke(T, A...) if it is a well-formed expression with overload resolution performed in a context in which unqualified lookup for tag_invoke finds only the declaration

    void tag_invoke();
    

    Otherwise, std::tag_invoke(T, A...) is ill-formed.

  2. [Note: Diagnosable ill-formed cases above result in substitution failure when std::tag_invoke(T, A...) appears in the immediate context of a template instantiation. —end note]

9. Thread support library [thread]

9.1. Stop tokens [thread.stoptoken]

9.1.1. Header <stop_token> synopsis [thread.stoptoken.syn]

At the beginning of this subclause, insert the following declarations into the synopsis within namespace std:

template<template<typename> class>
  struct check-type-alias-exists; // exposition-only

template<typename T>
  concept stoppable_token = see-below;

template<typename T, typename CB, typename Initializer = CB>
  concept stoppable_token_for = see-below;

template<typename T>
  concept unstoppable_token = see-below;

At the end of this subclause, insert the following declarations into the synopsis of within namespace std:

// [stoptoken.never], class never_stop_token
class never_stop_token;

// [stoptoken.inplace], class in_place_stop_token
class in_place_stop_token;

// [stopsource.inplace], class in_place_stop_source
class in_place_stop_source;

// [stopcallback.inplace], class template in_place_stop_callback
template<typename Callback>
class in_place_stop_callback;

9.1.2. Stop token concepts [thread.stoptoken.concepts]

Insert this section as a new subclause between Header <stop_token> synopsis [thread.stoptoken.syn] and Class stop_token [stoptoken].

  1. The stoppable_token concept checks for the basic interface of a “stop token” which is copyable and allows polling to see if stop has been requested and also whether a stop request is possible. It also requires an associated nested template-type-alias, T::callback_type<CB>, that identifies the stop-callback type to use to register a callback to be executed if a stop-request is ever made on a stoppable_token of type, T. The stoppable_token_for concept checks for a stop token type compatible with a given callback type. The unstoppable_token concept checks for a stop token type that does not allow stopping.

template<typename T>
  concept stoppable_token =
    copy_constructible<T> &&
    move_constructible<T> &&
    is_nothrow_copy_constructible_v<T> &&
    is_nothrow_move_constructible_v<T> &&
    equality_comparable<T> &&
    requires (const T& token) {
      { token.stop_requested() } noexcept -> boolean-testable;
      { token.stop_possible() } noexcept -> boolean-testable;
      typename check-type-alias-exists<T::template callback_type>;
    };

template<typename T, typename CB, typename Initializer = CB>
  concept stoppable_token_for =
    stoppable_token<T> &&
    invocable<CB> &&
    requires {
      typename T::template callback_type<CB>;
    } &&
    constructible_from<CB, Initializer> &&
    constructible_from<typename T::template callback_type<CB>, T, Initializer> &&
    constructible_from<typename T::template callback_type<CB>, T&, Initializer> &&
    constructible_from<typename T::template callback_type<CB>, const T, Initializer> &&
    constructible_from<typename T::template callback_type<CB>, const T&, Initializer>;

template<typename T>
  concept unstoppable_token =
    stoppable_token<T> &&
    requires {
      { T::stop_possible() } -> boolean-testable;
    } &&
    (!T::stop_possible());
  1. Let t and u be distinct object of type T. The type T models stoppable_token only if:

    1. All copies of a stoppable_token reference the same logical shared stop state and shall report values consistent with each other.

    2. If t.stop_possible() evaluates to false then, if u, references the same logical shared stop state, u.stop_possible() shall also subsequently evaluate to false and u.stop_requested() shall also subsequently evaluate to false.

    3. If t.stop_requested() evaluates to true then, if u, references the same logical shared stop state, u.stop_requested() shall also subsequently evaluate to true and u.stop_possible() shall also subsequently evaluate to true.

    4. Given a callback-type, CB, and a callback-initializer argument, init, of type Initializer then constructing an instance, cb, of type T::callback_type<CB>, passing t as the first argument and init as the second argument to the constructor, shall, if t.stop_possible() is true, construct an instance, callback, of type CB, direct-initialized with init, and register callback with t’s shared stop state such that callback will be invoked with an empty argument list if a stop request is made on the shared stop state.

      1. If t.stop_requested() is true at the time callback is registered then callback may be invoked immediately inline inside the call to cb’s constructor.

      2. If callback is invoked then, if u references the same shared stop state as t, an evaluation of u.stop_requested() will be true if the beginning of the invocation of callback strongly-happens-before the evaluation of u.stop_requested().

      3. If t.stop_possible() evaluates to false then the construction of cb is not required to construct and initialize callback.

    5. Construction of a T::callback_type<CB> instance shall only throw exceptions thrown by the initialization of the CB instance from the value of type Initializer.

    6. Destruction of the T::callback_type<CB> object, cb, removes callback from the shared stop state such that callback will not be invoked after the destructor returns.

      1. If callback is currently being invoked on another thread then the destructor of cb will block until the invocation of callback returns such that the return from the invocation of callback strongly-happens-before the destruction of callback.

      2. Destruction of a callback cb shall not block on the completion of the invocation of some other callback registered with the same shared stop state.

9.1.3. Class stop_token [stoptoken]

9.1.3.1. General [stoptoken.general]

Modify the synopsis of class stop_token in section General [stoptoken.general] as follows:

namespace std {
  class stop_token {
  public:
    template<class T>
    using callback_type = stop_callback<T>;

    // [stoptoken.cons], constructors, copy, and assignment
    stop_token() noexcept;

    // ...

9.1.4. Class never_stop_token [stoptoken.never]

Insert a new subclause, Class never_stop_token [stoptoken.never], after section Class template stop_callback [stopcallback], as a new subclause of Stop tokens [thread.stoptoken].

9.1.4.1. General [stoptoken.never.general]
  1. The class never_stop_token provides an implementation of the unstoppable_token concept. It provides a stop token interface, but also provides static information that a stop is never possible nor requested.

namespace std
{
  class never_stop_token {
    // exposition only
    struct callback {
      explicit callback(never_stop_token, auto&&) noexcept {}
    };
  public:
    template<class>
    using callback_type = callback;

    static constexpr bool stop_requested() noexcept { return false; }
    static constexpr bool stop_possible() noexcept { return false; }
  };
}

9.1.5. Class in_place_stop_token [stoptoken.inplace]

Insert a new subclause, Class in_place_stop_token [stoptoken.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].

9.1.5.1. General [stoptoken.inplace.general]
  1. The class in_place_stop_token provides an interface for querying whether a stop request has been made (stop_requested) or can ever be made (stop_possible) using an associated in_place_stop_source object ([stopsource.inplace]). An in_place_stop_token can also be passed to an in_place_stop_callback ([stopcallback.inplace]) constructor to register a callback to be called when a stop request has been made from an associated in_place_stop_source.

namespace std {
  class in_place_stop_token {
  public:
    template<class CB>
    using callback_type = in_place_stop_callback<CB>;

    // [stoptoken.inplace.cons], constructors, copy, and assignment
    in_place_stop_token() noexcept;
    ~in_place_stop_token();
    void swap(in_place_stop_token&) noexcept;

    // [stoptoken.inplace.mem], stop handling
    [[nodiscard]] bool stop_requested() const noexcept;
    [[nodiscard]] bool stop_possible() const noexcept;

    [[nodiscard]] bool operator==(const in_place_stop_token&) const noexcept = default;
    friend void swap(in_place_stop_token& lhs, in_place_stop_token& rhs) noexcept;

  private:
    friend class in_place_stop_source;
    const in_place_stop_source* source_; // exposition only
    explicit in_place_stop_token(const in_place_stop_source* source) noexcept;
  };
}
9.1.5.2. Constructors, copy, and assignment [stoptoken.inplace.cons]
in_place_stop_token() noexcept;
  1. Effects: initializes source_ with nullptr.

explicit in_place_stop_token(const in_place_stop_source* source) noexcept;
  1. Effects: initializes source_ with source.

void swap(stop_token& rhs) noexcept;
  1. Effects: Exchanges the values of source_ and rhs.source_.

9.1.5.3. Members [stoptoken.inplace.mem]
[[nodiscard]] bool stop_requested() const noexcept;
  1. Returns: source_ != nullptr && source_->stop_requested().

  2. Remarks: If source_ != nullptr, then any calls to this function must strongly happen before the beginning of invocation of the destructor of *source_.

[[nodiscard]] bool stop_possible() const noexcept;
  1. Returns: source_ != nullptr && source_->stop_possible().

  2. Remarks: If source_ != nullptr, then any calls to this function must strongly happen before the beginning of invocation of the destructor of *source_.

9.1.5.4. Non-member functions [stoptoken.inplace.nonmembers]
friend void swap(in_place_stop_token& x, in_place_stop_token& y) noexcept;
  1. Effects: Equivalent to: x.swap(y).

9.1.6. Class in_place_stop_source [stopsource.inplace]

Insert a new subclause, Class in_place_stop_source [stopsource.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].

9.1.6.1. General [stopsource.inplace.general]
  1. The class in_place_stop_source implements the semantics of making a stop request, without the need for a dynamic allocation of a shared state. A stop request made on a in_place_stop_source object is visible to all associated in_place_stop_token ([stoptoken.inplace]) objects. Once a stop request has been made it cannot be withdrawn (a subsequent stop request has no effect). All uses of in_place_stop_token objects associated with a given in_place_stop_source object must happen before the invocation of the destructor of that in_place_stop_token object.

namespace std {
  class in_place_stop_source {
  public:
    // [stopsource.inplace.cons], constructors, copy, and assignment
    in_place_stop_source() noexcept;

    in_place_stop_source(in_place_stop_source&&) noexcept = delete;
    ~in_place_stop_source();

    //[stopsource.inplace.mem], stop handling
    [[nodiscard]] in_place_stop_token get_token() const noexcept;
    [[nodiscard]] bool stop_possible() const noexcept;
    [[nodiscard]] bool stop_requested() const noexcept;
    bool request_stop() noexcept;
  };
}
9.1.6.2. Constructors, copy, and assignment [stopsource.inplace.cons]
in_place_stop_source() noexcept;
  1. Effects: Initializes a new stop state inside *this.

  2. Postconditions: stop_possible() is true and stop_requested() is false.

9.1.6.3. Members [stopsource.inplace.mem]
[[nodiscard]] in_place_stop_token get_token() const noexcept;
  1. Returns: in_place_stop_token{this}

[[nodiscard]] bool stop_possible() const noexcept;
  1. Returns: true if the stop state inside *this has not yet received a stop request; otherwise, false.

[[nodiscard]] bool stop_requested() const noexcept;
  1. Returns: true if the stop state inside *this has received a stop request; otherwise, false.

bool request_stop() noexcept;
  1. Effects: Atomically determines whether the stop state inside *this has received a stop request, and if not, makes a stop request. The determination and making of the stop request are an atomic read-modify-write operation ([intro.races]). If the request was made, the callbacks registered by associated in_place_stop_callback objects are synchronously called. If an invocation of a callback exits via an exception then terminate is invoked ([except.terminate]).

  2. Postconditions: stop_possible() is false and stop_requested() is true.

  3. Returns: true if this call made a stop request; otherwise false.

9.1.7. Class template in_place_stop_callback [stopcallback.inplace]

Insert a new subclause, Class template in_place_stop_callback [stopcallback.inplace], after the section added above, as a new subclause of Stop tokens [thread.stoptoken].

9.1.7.1. General [stopcallback.inplace.general]
  1. namespace std {
      template<class Callback>
      class in_place_stop_callback {
      public:
        using callback_type = Callback;
    
        // [stopcallback.inplace.cons], constructors and destructor
        template<class C>
          explicit in_place_stop_callback(in_place_stop_token st, C&& cb)
            noexcept(is_nothrow_constructible_v<Callback, C>);
        ~in_place_stop_callback();
    
        in_place_stop_callback(in_place_stop_callback&&) = delete;
    
      private:
        Callback callback_;      // exposition only
      };
    
      template<class Callback>
        in_place_stop_callback(in_place_stop_token, Callback)
          -> in_place_stop_callback<Callback>;
    }
    
  2. Mandates: in_place_stop_callback is instantiated with an argument for the template parameter Callback that satisfies both invocable and destructible.

  3. Preconditions: in_place_stop_callback is instantiated with an argument for the template parameter Callback that models both invocable and destructible.

  4. Recommended practice: Implementation should use the storage of the in_place_stop_callback objects to store the state necessary for their association with an in_place_stop_source object.

9.1.7.2. Constructors and destructor [stopcallback.inplace.cons]
template
explicit in_place_stop_callback(in_place_stop_token st, C&& cb)
  noexcept(is_nothrow_constructible_v<Callback, C>);
  1. Constraints: Callback and C satisfy constructible_from<Callback, C>.

  2. Preconditions: Callback and C model constructible_from<Callback, C>.

  3. Effects: Initializes callback_ with std::forward<C>(cb). If st.stop_requested() is true, then std::forward<Callback>(callback_)() is evaluated in the current thread before the constructor returns. Otherwise, if st has an associated in_place_stop_source object, registers the callback with the stop state of the in_place_stop_source that st is associated with such that std::forward<Callback>(callback_)() is evaluated by the first call to request_stop() on an associated in_place_stop_source. The in_place_stop_callback object being initialized becomes associated with the in_place_stop_source object that st is associated with, if any.

  4. Throws: Any exception thrown by the initialization of callback_.

  5. Remarks: If evaluating std::forward<Callback>(callback_)() exits via an exception, then terminate is invoked ([except.terminate]).

~in_place_stop_callback();
  1. Effects: Unregisters the callback from the stop state of the associated in_place_stop_source object, if any. The destructor does not block waiting for the execution of another callback registered by an associated stop_­callback. If callback_ is concurrently executing on another thread, then the return from the invocation of callback_ strongly happens before ([intro.races]) callback_ is destroyed. If callback_ is executing on the current thread, then the destructor does not block ([defns.block]) waiting for the return from the invocation of callback_.

  2. Remarks: A program has undefined behavior if the invocation of this function does not strongly happen before the beginning of the invocation of the destructor of the associated in_place_stop_source object, if any.

10. Execution control library [exec]

  1. This Clause describes components supporting execution of function objects [function.objects].

  2. The following subclauses describe the requirements, concepts, and components for execution control primitives as summarized in Table 1.

Table 1: Execution control library summary [tab:execution.summary]
Subclause Header
[exec.sched] Schedulers <execution>
[exec.recv] Receivers
[exec.op_state] Operation states
[exec.snd] Senders
[exec.execute] One-way execution
  1. [Note: A large number of execution control primitives are customization point objects. For an object one might define multiple types of customization point objects, for which different rules apply.**** Table 2 shows the types of customization point objects used in the execution control library:

Table 2: Types of customization point objects in the execution control library [tab:execution.cpos]
Customization point object type Purpose Examples
core provide core execution functionality, and connection between core components connect, start, execute
completion signals called by senders to announce the completion of the work (success, error, or cancellation) set_value, set_error, set_stopped
senders allow the specialization of the provided sender algorithms
  • sender factories (just, just_error, just_stopped, ...)
  • sender adaptors (transfer, then, let_value, ...)
  • sender consumers (start_detached, sync_wait)
general queries allow querying different properties of execution objects get_scheduler, get_delegatee_scheduler, get_allocator, get_stop_token
scheduler queries allow querying schedulers properties get_forward_progress_guarantee, execute_may_block_caller
sender queries allow querying senders properties get_completion_scheduler

-- end note]

10.1. Header <execution> synopsis [exec.syn]

namespace std::execution {
  // [exec.helpers], helper concepts
  template<class T>
    concept movable-value = see-below; // exposition only

  template<class From, class To>
    concept decays-to = same_as<decay_t<From>, To>; // exposition only

  template<class T>
    concept class-type = decays-to<T, T> && is_class_v<T>;  // exposition only

  // [exec.queries], general queries
  namespace general-queries { // exposition only
    struct get_scheduler_t;
    struct get_delegatee_scheduler_t;
    struct get_allocator_t;
    struct get_stop_token_t;
  }
  using general-queries::get_scheduler_t;
  using general-queries::get_delegatee_scheduler_t;
  using general-queries::get_allocator_t;
  using general-queries::get_stop_token_t;
  inline constexpr get_scheduler_t get_scheduler{};
  inline constexpr get_delegatee_scheduler_t get_delegatee_scheduler{};
  inline constexpr get_allocator_t get_allocator{};
  inline constexpr get_stop_token_t get_stop_token{};

  template <class T>
    using stop_token_of_t =
      remove_cvref_t<decltype(get_stop_token(declval<T>()))>;

  // [exec.env], execution environments
  namespace exec-envs { // exposition only
    struct no_env;
    struct empty-env {}; // exposition only
    struct get_env_t;
    struct forwarding_env_query_t;
  }
  using exec-envs::no_env;
  using exec-envs::empty-env;
  using exec-envs::get_env_t;
  using exec-envs::forwarding_env_query_t;
  inline constexpr get_env_t get_env {};
  inline constexpr forwarding_env_query_t forwarding_env_query{};
  template <class T>
    concept forwarding-env-query = // exposition only
      forwarding_env_query(T{});

  template <class T>
    using env_of_t = decltype(get_env(declval<T>()));

  // [exec.sched], schedulers
  template<class S>
    concept scheduler = see-below;

  // [exec.sched_queries], scheduler queries
  enum class forward_progress_guarantee;
  namespace schedulers-queries { // exposition only
    struct forwarding_scheduler_query_t;
    struct get_forward_progress_guarantee_t;
  }
  using schedulers-queries::forwarding_scheduler_query_t;
  using schedulers-queries::get_forward_progress_guarantee_t;
  inline constexpr forwarding_scheduler_query_t forwarding_scheduler_query{};
  inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{};
}

namespace std::this_thread {
  namespace this-thread-queries { // exposition only
    struct execute_may_block_caller_t;
  }
  using this-thread-queries::execute_may_block_caller_t;
  inline constexpr execute_may_block_caller_t execute_may_block_caller{};
}

namespace std::execution {
  // [exec.recv], receivers
  template <class T, class E = exception_ptr>
    concept receiver = see-below;

  template <class T, class... An>
    concept receiver_of = see-below;

  namespace receivers { // exposition only
    struct set_value_t;
    struct set_error_t;
    struct set_stopped_t;
  }
  using receivers::set_value_t;
  using receivers::set_error_t;
  using receivers::set_stopped_t;
  inline constexpr set_value_t set_value{};
  inline constexpr set_error_t set_error{};
  inline constexpr set_stopped_t set_stopped{};

  // [exec.recv_queries], receiver queries
  namespace receivers-queries { // exposition only
    struct forwarding_receiver_query_t;
  }
  using receivers-queries::forwarding_receiver_query_t;
  inline constexpr forwarding_receiver_query_t forwarding_receiver_query{};
  template <class T>
    concept forwarding-receiver-query = // exposition only
      forwarding_receiver_query(T{});

  // [exec.op_state], operation states
  template<class O>
    concept operation_state = see-below;

  namespace op-state { // exposition only
    struct start_t;
  }
  using op-state::start_t;
  inline constexpr start_t start{};

  template<class S>
    concept has-sender-types = see-below; // exposition only

  // [exec.snd], senders
  template<class S, class E = no_env>
    concept sender = see-below;

  template<class S, class R>
    concept sender_to = see-below;

  template<class S, class E = no_env, class... Ts>
    concept sender_of = see below;

  template<class... Ts>
    struct type-list; // exposition only

  template<class S, class E = no_env>
    using single-sender-value-type = see below; // exposition only

  template <class S, class E = no_env>
    concept single-sender = see below; // exposition only

  // [exec.sndtraits], completion signatures
  namespace completion-signatures { // exposition only
    struct get_completion_signatures_t;
  }
  using completion-signatures::get_completion_signatures_t;
  inline constexpr get_completion_signatures_t get_completion_signatures {};

  template <class S, class E = no_env>
    using completion_signatures_of_t = see below;

  template <class E>
    struct dependent_completion_signatures;

  template <class... Ts>
    using decayed-tuple = tuple<decay_t<Ts>...>; // exposition only

  template <class... Ts>
    using variant-or-empty = see below; // exposition only

  template<class S,
           class E = no_env,
           template <class...> class Tuple = decayed-tuple,
           template <class...> class Variant = variant-or-empty>
      requires sender<S, E>
    using value_types_of_t =
      typename completion_signatures_of_t<S, E>::template value_types<Tuple, Variant>;

  template<class S,
           class E = no_env,
           template <class...> class Variant = variant-or-empty>
      requires sender<S, E>
    using error_types_of_t =
      typename completion_signatures_of_t<S, E>::template error_types<Variant>;

  // [exec.connect], the connect sender algorithm
  namespace senders-connect { // exposition only
    struct connect_t;
  }
  using senders-connect::connect_t;
  inline constexpr connect_t connect{};

  template <class S, class R>
    using connect_result_t = decltype(connect(declval<S>(), declval<R>()));

  // [exec.snd_queries], sender queries
  namespace senders-queries { // exposition only
    struct forwarding_sender_query_t;

    template <class T>
      concept forwarding-sender-query = // exposition only
        forwarding_sender_query(T{});

    template <class CPO>
    struct get_completion_scheduler_t;
  }
  using senders-queries::forwarding_sender_query_t;
  using senders-queries::get_completion_scheduler_t;
  inline constexpr forwarding_sender_query_t forwarding_sender_query{};
  template <class CPO>
  inline constexpr get_completion_scheduler_t<CPO> get_completion_scheduler{};

  // [exec.factories], sender factories
  namespace senders-factories { // exposition only
    template<movable-value... Ts>
      unspecified just(Ts &&...) noexcept(see below);
    template<movable-value Error>
      unspecified just_error(Error &&) noexcept(see below);
    unspecified just_stopped() noexcept;

    struct schedule_t;
    struct transfer_just_t;
  }
  using senders-factories::just;
  using senders-factories::just_error;
  using senders-factories::just_stopped;
  using senders-factories::schedule_t;
  using senders-factories::transfer_just_t;
  inline constexpr schedule_t schedule{};
  inline constexpr transfer_just_t transfer_just{};
  inline constexpr unspecified read{};

  template <scheduler S>
    using schedule_result_t = decltype(schedule(declval<S>()));

  // [exec.adapt], sender adaptors
  namespace sender-adaptor-closure { // exposition only
    template<class-type D>
      struct sender_adaptor_closure { };
  }
  using sender-adaptor-closure::sender_adaptor_closure;

  namespace sender-adaptors { // exposition only
    struct on_t;
    struct transfer_t;
    struct schedule_from_t;
    struct then_t;
    struct upon_error_t;
    struct upon_stopped_t;
    struct let_value_t;
    struct let_error_t;
    struct let_stopped_t;
    struct bulk_t;
    struct split_t;
    struct when_all_t;
    struct when_all_with_variant_t;
    struct transfer_when_all_t;
    struct transfer_when_all_with_variant_t;
    struct stopped_as_optional_t;
    struct stopped_as_error_t;
    struct ensure_started_t;
  }
  using sender-adaptors::on_t;
  using sender-adaptors::transfer_t;
  using sender-adaptors::schedule_from_t;
  using sender-adaptors::then_t;
  using sender-adaptors::upon_error_t;
  using sender-adaptors::upon_stopped_t;
  using sender-adaptors::let_value_t;
  using sender-adaptors::let_error_t;
  using sender-adaptors::let_stopped_t;
  using sender-adaptors::bulk_t;
  using sender-adaptors::split_t;
  using sender-adaptors::when_all_t;
  using sender-adaptors::when_all_with_variant_t;
  using sender-adaptors::transfer_when_all_t;
  using sender-adaptors::transfer_when_all_with_variant_t;
  using sender-adaptors::stopped_as_optional_t;
  using sender-adaptors::stopped_as_error_t;
  using sender-adaptors::ensure_started_t;

  inline constexpr on_t on{};
  inline constexpr transfer_t transfer{};
  inline constexpr schedule_from_t schedule_from{};

  inline constexpr then_t then{};
  inline constexpr upon_error_t upon_error{};
  inline constexpr upon_stopped_t upon_stopped{};

  inline constexpr let_value_t let_value{};
  inline constexpr let_error_t let_error{};
  inline constexpr let_stopped_t let_stopped{};

  inline constexpr bulk_t bulk{};

  inline constexpr split_t split{};
  inline constexpr when_all_t when_all{};
  inline constexpr when_all_with_variant_t when_all_with_variant{};
  inline constexpr transfer_when_all_t transfer_when_all{};
  inline constexpr transfer_when_all_with_variant_t
    transfer_when_all_with_variant{};

  namespace sender-adaptor-into-variant { // exposition only
    template<class S, class E>
        requires sender<S, E>
      using into-variant-type = see-below; // exposition-only
    template<sender S>
      see-below into_variant(S &&);
  }
  using sender-adaptor-into-variant::into_variant;

  inline constexpr stopped_as_optional_t stopped_as_optional;

  inline constexpr stopped_as_error_t stopped_as_error;

  inline constexpr ensure_started_t ensure_started{};

  // [exec.consumers], sender consumers
  namespace sender-consumers { // exposition only
    struct start_detached_t;
  }
  using sender-consumers::start_detached_t;
  inline constexpr start_detached_t start_detached{};

  // [exec.utils], sender and receiver utilities
  // [exec.utils.rcvr_adptr]
  template<
      class-type Derived,
      receiver Base = unspecified> // arguments are not associated entities ([lib.tmpl-heads])
    class receiver_adaptor;

  template <class Fn>
    concept completion-signature = // exposition only
      see below;

  // [exec.utils.cmplsigs]
  template <completion-signature... Fns> // arguments are not associated entities ([lib.tmpl-heads])
    struct completion_signatures;

  template <class... Args> // exposition only
    using default-set-value = set_value_t(Args...);

  template <class Err> // exposition only
    using default-set-error = set_error_t(Err);

  // [exec.utils.mkcmplsigs]
  template <
    sender Sndr,
    class Env = no_env,
    class AddlSigs = completion_signatures<>,
    template <class...> class SetValue = /* see below */,
    template <class> class SetError = /* see below */,
    bool SendsStopped = completion_signatures_of_t<Sndr, Env>::sends_stopped>
      requires sender
  using make_completion_signatures = completion_signatures</* see below */>;

  // [exec.ctx], execution contexts
  class run_loop;
}

namespace std::this_thread {
  namespace this-thread { // exposition only
    struct sync-wait-env; // exposition only
    template<class S>
        requires sender<S, sync-wait-env>
      using sync-wait-type = see-below; // exposition-only
    template<class S>
      using sync-wait-with-variant-type = see-below; // exposition-only

    struct sync_wait_t;
    struct sync_wait_with_variant_t;
  }
  using this-thread::sync_wait_t;
  using this-thread::sync_wait_with_variant_t;
  inline constexpr sync_wait_t sync_wait{};
  inline constexpr sync_wait_with_variant_t sync_wait_with_variant{};
}

namespace std::execution {
  // [exec.execute], one-way execution
  namespace execute { // exposition only
    struct execute_t;
  }
  using execute::execute_t;
  inline constexpr execute_t execute{};

  // [exec.as_awaitable]
  namespace coro-utils { // exposition only
    struct as_awaitable_t;
  }
  using coro-utils::as_awaitable_t;
  inline constexpr as_awaitable_t as_awaitable;

  // [exec.with_awaitable_senders]
  template <class-type Promise>
    struct with_awaitable_senders;
}

10.2. Helper concepts [exec.helpers]

template<class T>
concept movable-value = // exposition only
  move_constructible<decay_t<T>> &&
  constructible_from<decay_t<T>, T>;

10.3. General queries [exec.queries]

10.3.1. execution::get_scheduler [exec.queries.get_scheduler]

  1. execution::get_scheduler is used to ask an object for its associated scheduler.

  2. The name execution::get_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:

    1. tag_invoke(execution::get_scheduler, as_const(r)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially-throwing and its type satisfies execution::scheduler.

    2. Otherwise, execution::get_scheduler(r) is ill-formed.

  3. execution::get_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_scheduler).

10.3.2. execution::get_delegatee_scheduler [exec.queries.get_delegatee_scheduler]

  1. execution::get_delegatee_scheduler is used to ask an object for a scheduler that may be used to delegate work to for the purpose of forward progress delegation.

  2. The name execution::get_delegatee_scheduler denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_delegatee_scheduler(r) is ill-formed. Otherwise, it is expression equivalent to:

    1. tag_invoke(execution::get_delegatee_scheduler, as_const(r)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially-throwing and its type satisfies execution::scheduler.

    2. Otherwise, execution::get_delegatee_scheduler(r) is ill-formed.

  3. execution::get_delegatee_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_delegatee_scheduler).

10.3.3. execution::get_allocator [exec.queries.get_allocator]

  1. execution::get_allocator is used to ask an object for its associated allocator.

  2. The name execution::get_allocator denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_allocator(r) is ill-formed. Otherwise, it is expression equivalent to:

    1. tag_invoke(execution::get_allocator, as_const(r)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially-throwing and its type satisfies Allocator.

    2. Otherwise, execution::get_allocator(r) is ill-formed.

  3. execution::get_allocator() (with no arguments) is expression-equivalent to execution::read(execution::get_allocator).

10.3.4. execution::get_stop_token [exec.queries.get_stop_token]

  1. execution::get_stop_token is used to ask an object for an associated stop token.

  2. The name execution::get_stop_token denotes a customization point object. For some subexpression r, if the type of r is (possibly cv-qualified) no_env, then execution::get_stop_token(r) is ill-formed. Otherwise, it is expression equivalent to:

    1. tag_invoke(execution::get_stop_token, as_const(r)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially-throwing and its type satisfies stoppable_token.

    2. Otherwise, never_stop_token{}.

  3. execution::get_stop_token() (with no arguments) is expression-equivalent to execution::read(execution::get_stop_token).

10.4. Execution environments [exec.env]

  1. An execution environment contains state associated with the completion of an asynchronous operation. Every receiver has an associated execution environment, accessible with the get_env receiver query. The state of an execution environment is accessed with customization point objects. An execution environment may respond to any number of these environment queries.

  2. An environment query is a customization point object that accepts as its first argument an execution environment. For an environment query EQ and an object e of type no_env, the expression EQ(e) shall be ill-formed.

10.4.1. execution::no_env [exec.no_env]

namespace exec-envs { // exposition only
  struct no_env {
    friend void tag_invoke(auto, same_as<no_env> auto, auto&&...) = delete;
  };
}
  1. no_env is a special environment used by the sender concept and by the get_completion_signatures customization point when the user has specified no environment argument. [Note: A user may choose to not specify an environment in order to see if a sender knows its completion signatures independent of any particular execution environment. -- end note]

10.4.2. execution::get_env [exec.get_env]

namespace exec-envs { // exposition only
  struct get_env_t;
}
inline constexpr exec-envs::get_env_t get_env {};
  1. get_env is a customization point object. For some subexpression r, get_env(r) is expression-equivalent to

    1. tag_invoke(execution::get_env, r) if that expression is well-formed.

    2. Otherwise, empty-env{}.

  2. If get_env(r) is an lvalue, the object it refers to shall be valid while r is valid.

10.4.3. execution::forwarding_env_query [exec.fwd_env]

  1. execution::forwarding_env_query is used to ask a customization point object whether it is a environment query that should be forwarded through environment adaptors.

  2. The name execution::forwarding_env_query denotes a customization point object. For some subexpression t, execution::forwarding_env_query(t) is expression equivalent to:

    1. tag_invoke(execution::forwarding_env_query, t), contextually converted to bool, if the tag_invoke expression is well formed.

      • Mandates: The tag_invoke expression is indeed contextually convertible to bool, that expression and the contextual conversion are not potentially-throwing and are core constant expressions if t is a core constant expression.

    2. Otherwise, true.

10.5. Schedulers [exec.sched]

  1. The scheduler concept defines the requirements of a type that allows for scheduling of work on its associated execution context.

    template<class S>
      concept scheduler =
        copy_constructible<remove_cvref_t<S>> &&
        equality_comparable<remove_cvref_t<S>> &&
        requires(S&& s, const get_completion_scheduler_t<set_value_t> tag) {
          { execution::schedule((S&&) s) } -> sender;
          { tag_invoke(tag, execution::schedule((S&&) s)) } -> same_as<remove_cvref_t<S>>;
        };
    
  2. Let S be the type of a scheduler and let E be the type of an execution environment for which sender<schedule_result_t<S>, E> is true. Then sender_of<schedule_result_t<S>, E> shall be true.

  3. None of a scheduler’s copy constructor, destructor, equality comparison, or swap member functions shall exit via an exception.

  4. None of these member functions, nor a scheduler type’s schedule function, shall introduce data races as a result of concurrent invocations of those functions from different threads.

  5. For any two (possibly const) values s1 and s2 of some scheduler type S, s1 == s2 shall return true only if both s1 and s2 are handles to the same associated execution context.

  6. For a given scheduler expression s, the expression execution::get_completion_scheduler<set_value_t>(execution::schedule(s)) shall compare equal to s.

  7. A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule. [Note: The ability to wait for completion of submitted function objects may be provided by the associated execution context of the scheduler. —end note]

10.5.1. Scheduler queries [exec.sched_queries]

10.5.1.1. execution::forwarding_scheduler_query [exec.sched_queries.forwarding_scheduler_query]
  1. execution::forwarding_scheduler_query is used to ask a customization point object whether it is a scheduler query that should be forwarded through scheduler adaptors.

  2. The name execution::forwarding_scheduler_query denotes a customization point object. For some subexpression t, execution::forwarding_scheduler_query(t) is expression equivalent to:

    1. tag_invoke(execution::forwarding_scheduler_query, t), contextually converted to bool, if the tag_invoke expression is well formed.

      • Mandates: The tag_invoke expression is indeed contextually convertible to bool, that expression and the contextual conversion are not potentially-throwing and are core constant expressions if t is a core constant expression.

    2. Otherwise, false.

10.5.1.2. execution::get_forward_progress_guarantee [exec.sched_queries.get_forward_progress_guarantee]
enum class forward_progress_guarantee {
  concurrent,
  parallel,
  weakly_parallel
};
  1. execution::get_forward_progress_guarantee is used to ask a scheduler about the forward progress guarantees of execution agents created by that scheduler.

  2. The name execution::get_forward_progress_guarantee denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, execution::get_forward_progress_guarantee is ill-formed. Otherwise, execution::get_forward_progress_guarantee(s) is expression equivalent to:

    1. tag_invoke(execution::get_forward_progress_guarantee, as_const(s)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially throwing and its type is execution::forward_progress_guarantee.

    2. Otherwise, execution::forward_progress_guarantee::weakly_parallel.

  3. If execution::get_forward_progress_guarantee(s) for some scheduler s returns execution::forward_progress_guarantee::concurrent, all execution agents created by that scheduler shall provide the concurrent forward progress guarantee. If it returns execution::forward_progress_guarantee::parallel, all execution agents created by that scheduler shall provide at least the parallel forward progress guarantee.

10.5.1.3. this_thread::execute_may_block_caller [exec.sched_queries.execute_may_block_caller]
  1. this_thread::execute_may_block_caller is used to ask a scheduler s whether a call execution::execute(s, f) with any invocable f may block the thread where such a call occurs.

  2. The name this_thread::execute_may_block_caller denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::scheduler, this_thread::execute_may_block_caller is ill-formed. Otherwise, this_thread::execute_may_block_caller(s) is expression equivalent to:

    1. tag_invoke(this_thread::execute_may_block_caller, as_const(s)), if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially throwing and its type is bool.

    2. Otherwise, true.

  3. If this_thread::execute_may_block_caller(s) for some scheduler s returns false, no execution::execute(s, f) call with some invocable f shall block the calling thread.

10.6. Receivers [exec.recv]

  1. A receiver represents the continuation of an asynchronous operation. An asynchronous operation may complete with a (possibly empty) set of values, an error, or it may be cancelled. A receiver has three principal operations corresponding to the three ways an asynchronous operation may complete: set_value, set_error, and set_stopped. These are collectively known as a receiver’s completion-signal operations.

  2. The receiver concept defines the requirements for a receiver type with an unknown set of value types. The receiver_of concept defines the requirements for a receiver type with a known set of value types, whose error type is exception_ptr.

    template<class T, class E = exception_ptr>
    concept receiver =
      move_constructible<remove_cvref_t<T>> &&
      constructible_from<remove_cvref_t<T>, T> &&
      requires(remove_cvref_t<T>&& t, E&& e) {
        { execution::set_stopped(std::move(t)) } noexcept;
        { execution::set_error(std::move(t), (E&&) e) } noexcept;
      };
    
    template<class T, class... An>
    concept receiver_of =
      receiver<T> &&
      requires(remove_cvref_t<T>&& t, An&&... an) {
        execution::set_value(std::move(t), (An&&) an...);
      };
    
  3. The receiver’s completion-signal operations have semantic requirements that are collectively known as the receiver contract, described below:

    1. None of a receiver’s completion-signal operations shall be invoked before execution::start has been called on the operation state object that was returned by execution::connect to connect that receiver to a sender.

    2. Once execution::start has been called on the operation state object, exactly one of the receiver’s completion-signal operations shall complete non-exceptionally before the receiver is destroyed.

    3. If execution::set_value exits with an exception, it is still valid to call execution::set_error or execution::set_stopped on the receiver, but it is no longer valid to call execution::set_value on the receiver.

  4. Once one of a receiver’s completion-signal operations has completed non-exceptionally, the receiver contract has been satisfied.

  5. Receivers have an associated execution environment that is accessible by passing the receiver to execution::get_env. A sender can obtain information about the current execution environment by querying the environment of the receiver to which it is connected. The set of environment queries is extensible and includes:

    • execution::get_scheduler: used to obtain a suggested scheduler to be used when a sender needs to launch additional work. [Note: the presence of this query on a receiver’s environment does not bind a sender to use its result. --end note]

    • execution::get_delegatee_scheduler: used to obtain a delegatee scheduler on which an algorithm or scheduler may delegate work for the purpose of ensuring forward progress.

    • execution::get_allocator: used to obtain a suggested allocator to be used when a sender needs to allocate memory. [Note: the presence of this query on a receiver does not bind a sender to use its result. --end note]

    • execution::get_stop_token: used to obtain the associated stop token to be used by a sender to check whether a stop request has been made. [Note: such a stop token being signalled does not bind the sender to actually cancel any work. --end note]

  6. Let r be a receiver, s be a sender, and op_state be an operation state resulting from an execution::connect(s, r) call. Let token be a stop token resulting from an execution::get_stop_token(execution::get_env(r)) call. token must remain valid at least until a call to a receiver completion-signal function of r returns successfully. [Note: this means that, unless it knows about further guarantees provided by the receiver r, the implementation of op_state should not use token after it makes a call to a receiver completion-signal function of r. This also implies that any stop callbacks registered on token by the implementation of op_state or s must be destroyed before such a call to a receiver completion-signal function of r. --end note]

10.6.1. execution::set_value [exec.set_value]

  1. execution::set_value is used to send a value completion signal to a receiver.

  2. The name execution::set_value denotes a customization point object. The expression execution::set_value(R, Vs...) for some subexpressions R and Vs... is expression-equivalent to:

    1. tag_invoke(execution::set_value, R, Vs...), if that expression is valid. If the function selected by tag_invoke does not send the value(s) Vs... to the receiver R’s value channel, the program is ill-formed with no diagnostic required.

    2. Otherwise, execution::set_value(R, Vs...) is ill-formed.

10.6.2. execution::set_error [exec.set_error]

  1. execution::set_error is used to send a error signal to a receiver.

  2. The name execution::set_error denotes a customization point object. The expression execution::set_error(R, E) for some subexpressions R and E is expression-equivalent to:

    1. tag_invoke(execution::set_error, R, E), if that expression is valid. If the function selected by tag_invoke does not send the error E to the receiver R’s error channel, the program is ill-formed with no diagnostic required.

      • Mandates: The tag_invoke expression above is not potentially throwing.

    2. Otherwise, execution::set_error(R, E) is ill-formed.

10.6.3. execution::set_stopped [exec.set_stopped]

  1. execution::set_stopped is used to send a stopped signal to a receiver.

  2. The name execution::set_stopped denotes a customization point object. The expression execution::set_stopped(R) for some subexpression R is expression-equivalent to:

    1. tag_invoke(execution::set_stopped, R), if that expression is valid. If the function selected by tag_invoke does not signal the receiver R’s stopped channel, the program is ill-formed with no diagnostic required.

      • Mandates: The tag_invoke expression above is not potentially throwing.

    2. Otherwise, execution::set_stopped(R) is ill-formed.

10.6.4. Receiver queries [exec.recv_queries]

10.6.4.1. execution::forwarding_receiver_query [exec.recv_queries.forwarding_receiver_query]
  1. execution::forwarding_receiver_query is used to ask a customization point object whether it is a receiver query that should be forwarded through receiver adaptors.

  2. The name execution::forwarding_receiver_query denotes a customization point object. For some subexpression t, execution::forwarding_receiver_query(t) is expression equivalent to:

    1. tag_invoke(execution::forwarding_receiver_query, t), contextually converted to bool, if the tag_invoke expression is well formed.

      • Mandates: The tag_invoke expression is indeed contextually convertible to bool, that expression and the contextual conversion are not potentially-throwing and are core constant expressions if t is a core constant expression.

    2. Otherwise, false if the type of t is one of set_value_t, set_error_t, or set_stopped_t.

    3. Otherwise, true.

  3. [Note: Currently the only standard receiver query is execution::get_env -- end note]

10.7. Operation states [exec.op_state]

  1. The operation_state concept defines the requirements for an operation state type, which allows for starting the execution of work.

    template<class O>
      concept operation_state =
        destructible<O> &&
        is_object_v<O> &&
        requires (O& o) {
          { execution::start(o) } noexcept;
        };
    
  2. Any operation state types defined by the implementation are non-movable types.

10.7.1. execution::start [exec.op_state.start]

  1. execution::start is used to start work represented by an operation state object.

  2. The name execution::start denotes a customization point object. The expression execution::start(O) for some lvalue subexpression O is expression-equivalent to:

    1. tag_invoke(execution::start, O), if that expression is valid. If the function selected by tag_invoke does not start the work represented by the operation state O, the program is ill-formed with no diagnostic required.

      • Mandates: The tag_invoke expression above is not potentially throwing.

    2. Otherwise, execution::start(O) is ill-formed.

  3. The caller of execution::start(O) must guarantee that the lifetime of the operation state object O extends at least until one of the receiver completion-signal functions of a receiver R passed into the execution::connect call that produced O is ready to successfully return. [Note: this allows for the receiver to manage the lifetime of the operation state object, if destroying it is the last operation it performs in its completion-signal functions. --end note]

10.8. Senders [exec.snd]

  1. A sender describes a potentially asynchronous operation. A sender’s responsibility is to fulfill the receiver contract of a connected receiver by delivering one of the receiver completion-signals.

  2. The sender concept defines the requirements for a sender type. The sender_to concept defines the requirements for a sender type capable of being connected with a specific receiver type.

    template<template<template<class...> class, template<class...> class> class>
      struct has-value-types; // exposition only
    
    template<template<template<class...> class> class>
      struct has-error-types; // exposition only
    
    template<class S>
      concept has-sender-types = // exposition only
        requires {
          typename has-value-types<S::template value_types>;
          typename has-error-types<S::template error_types>;
          typename bool_constant<S::sends_stopped>;
        };
    
    template <class S, class E>
      concept sender-base = // exposition only
        requires { typename completion_signatures_of_t<S, E>; } &&
        has-sender-types<completion_signatures_of_t<S, E>>;
    
    template<class S, class E = no_env>
      concept sender =
        sender-base<S, E> &&
        sender-base<S, no_env> &&
        move_constructible<remove_cvref_t<S>>;
    
    template<class S, class R>
      concept sender_to =
        sender<S, env_of_t<R>> &&
        receiver<R> &&
        requires (S&& s, R&& r) {
          execution::connect(std::forward<S>(s), std::forward<R>(r));
        };
    
  3. The sender_of concept defines the requirements for a sender type that on successful completion sends the specified set of value types.

    template<class S, class E = no_env, class... Ts>
      concept sender_of =
        sender<S, E> &&
        same_as<
          type-list<Ts...>,
          typename completion_signatures_of_t<S, E>::template value_types<type-list, type_identity_t>
        >;
    

10.8.1. Completion signatures [exec.sndtraits]

  1. This clause makes use of the following implementation-defined entities:

    struct no-completion-signatures {};
    
10.8.1.1. execution::completion_signatures_of_t [exec.sndtraitst]
  1. The alias template completion_signatures_of_t is used to query a sender type for facts associated with the signals it sends.

  2. completion_signatures_of_t also recognizes awaitables as senders. For this clause ([exec]):

    1. An awaitable is an expression that would be well-formed as the operand of a co_await expression within a given context.

    2. For any type T, is-awaitable<T> is true if and only if an expression of that type is an awaitable as described above within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, is-awaitable<T, P> is true if and only if an expression of that type is an awaitable as described above within the context of a coroutine whose promise type is P.

    3. For an awaitable a such that decltype((a)) is type A, await-result-type<A> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type does not define a member await_transform. For a coroutine promise type P, await-result-type<A, P> is an alias for decltype(e), where e is a's await-resume expression ([expr.await]) within the context of a coroutine whose promise type is P.

  3. For types S and E, the type completion_signatures_of_t<S, E> is an alias for decltype(get_completion_signatures(declval<S>(), declval<E>())) if that expression is well-formed and names a type other than no-completion-signatures. Otherwise, it is ill-formed.

  4. execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an expression such that decltype((e)) is E. Then get_completion_signatures(s) is expression-equivalent to get_completion_signatures(s, no_env{}) and get_completion_signatures(s, e) is expression-equivalent to:

    1. tag_invoke_result_t<get_completion_signatures_t, S, E>{} if that expression is well-formed,

    2. Otherwise, if remove_cvref_t<S>::completion_signatures is well-formed and names a type, then a prvalue of remove_cvref_t<S>::completion_signatures,

    3. Otherwise, if is-awaitable<S> is true, then

      1. If await-result-type<S> is cv void then a prvalue of a type equivalent to:

        completion_signatures<
          set_value_t(),
          set_error_t(exception_ptr),
          set_stopped_t()>
        
      2. Otherwise, a prvalue of a type equivalent to:

        completion_signatures<
          set_value_t(await-result-type<S>),
          set_error_t(exception_ptr),
          set_stopped_t()>
        
    4. Otherwise, no-completion-signatures{}.

  5. The exposition-only type variant-or-empty<Ts...> is defined as follows:

    1. If sizeof...(Ts) is greater than zero, variant-or-empty<Ts...> names the type variant<Us...> where Us... is the pack decay_t<Ts>... with duplicate types removed.

    2. Otherwise, variant-or-empty<Ts...> names an implementation defined class type equivalent to the following:

      struct empty-variant {
        empty-variant() = delete;
      };
      
  6. Let r be an rvalue receiver of type R, and let S be the type of a sender. If value_types_of_t<S, env_of_t<R>, Tuple, Variant> is well formed, it shall name the type Variant<Tuple<Args0...>, Tuple<Args1...>, ..., Tuple<ArgsN...>>>, where the type packs Args0 through ArgsN are the packs of types the sender S passes as arguments to execution::set_value (besides the receiver object). If such sender S odr-uses ([basic.def.odr]) execution::set_value(r, args...), where decltype(args)... is not one of the type packs Args0... through ArgsN... (ignoring differences in rvalue-reference qualification), the program is ill-formed with no diagnostic required.

  7. Let r be an rvalue receiver of type R, and let S be the type of a sender. If error_types_of_t<S, env_of_t<R>, Variant> is well formed, it shall name the type Variant<E0, E1, ..., EN>, where the types E0 through EN are the types the sender S passes as arguments to execution::set_error (besides the receiver object). If such sender S odr-uses execution::set_error(r, e), where decltype(e) is not one of the types E0 through EN (ignoring differences in rvalue-reference qualification), the program is ill-formed with no diagnostic required.

  8. Let r be an rvalue receiver of type R, and let S be the type of a sender. If completion_signatures_of_t<S, env_of_t<R>>::sends_stopped is well formed and false, and such sender S odr-uses execution::set_stopped(r), the program is ill-formed with no diagnostic required.

  9. Let S be the type of a sender, let E be the type of an execution environment other than execution::no_env such that sender<S, E> is true. Let Tuple, Variant1, and Variant2 be variadic alias templates or class templates such that following types are well-formed:

    • value_types_of_t<S, no_env, Tuple, Variant1>

    • error_types_of_t<S, no_env, Variant2>

    then the following shall also be true:

    • value_types_of_t<S, E, Tuple, Variant1> shall also be well-formed and shall name the same type as value_types_of_t<S, no_env, Tuple, Variant1>,

    • error_types_of_t<S, E, Variant2> shall also be well-formed and shall name the same type as error_types_of_t<S, no_env, Variant2>, and

    • completion_signatures_of_t<S, E>::sends_stopped shall have the same value as completion_signatures_of_t<S, no_env>::sends_stopped.

10.8.1.2. dependent_completion_signatures [exec.depsndtraits]
template <class E>
  struct dependent_completion_signatures;
  1. dependent_completion_signatures is a placeholder completion signatures descriptor that can be used to report that a type might be a sender within a particular execution environment, but it isn’t a sender in an arbitrary execution environment.

  2. If decay_t<E> is no_env, dependent_completion_signatures<E> is equivalent to:

    template <>
      struct dependent_completion_signatures<no_env> {
        template <template <class...> class, template <class...> class>
            requires false
          using value_types = /* unspecified */;
    
        template <template <class...> class>
            requires false
          using error_types = /* unspecified */;
    
        static constexpr bool sends_stopped = /* unspecified */;
      };
    

    Otherwise, dependent_completion_signatures<E> is an empty struct.

10.8.2. execution::connect [exec.connect]

  1. execution::connect is used to connect a sender with a receiver, producing an operation state object that represents the work that needs to be performed to satisfy the receiver contract of the receiver with values that are the result of the operations described by the sender.

  2. The name execution::connect denotes a customization point object. For some subexpressions s and r, let S be decltype((s)) and R be decltype((r)), and let S' and R' be the decayed types of S and R, respectively. If R does not satisfy execution::receiver, execution::connect(s, r) is ill-formed. Otherwise, the expression execution::connect(s, r) is expression-equivalent to:

    1. tag_invoke(execution::connect, s, r), if that expression is valid and S satisfies execution::sender. If the function selected by tag_invoke does not return an operation state for which execution::start starts work described by s, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies operation_state.

    2. Otherwise, connect-awaitable(s, r) if is-awaitable<S, connect-awaitable-promise> is true and that expression is valid, where connect-awaitable is a coroutine equivalent to the following:

      operation-state-task connect-awaitable(S' s, R' r) requires see-below {
        exception_ptr ep;
        try {
          set-value-expr
        } catch(...) {
          ep = current_exception();
        }
        set-error-expr
      }
      

      where connect-awaitable-promise is the promise type of connect-awaitable, and where connect-awaitable suspends at the initial suspends point ([dcl.fct.def.coroutine]), and:

      1. set-value-expr first evaluates co_await (S&&) s, then suspends the coroutine and evaluates execution::set_value((R&&) r) if await-result-type<S, connect-awaitable-promise> is cv void; otherwise, it evaluates auto&& res = co_await (S&&) s, then suspends the coroutine and evaluates execution::set_value((R&&) r, (decltype(res)) res).

        If the call to execution::set_value exits with an exception, the coroutine is resumed and the exception is immediately propagated in the context of the coroutine.

        [Note: If the call to execution::set_value exits normally, then the connect-awaitable coroutine is never resumed. --end note]

      2. set-error-expr first suspends the coroutine and then executes execution::set_error((R&&) r, std::move(ep)).

        [Note: The connect-awaitable coroutine is never resumed after the call to execution::set_error. --end note]

      3. operation-state-task is a type that models operation_state. Its execution::start resumes the connect-awaitable coroutine, advancing it past the initial suspend point.

      4. Let p be an lvalue reference to the promise of the connect-awaitable coroutine, let b be a const lvalue reference to the receiver r. Then tag_invoke(tag, p, as...) is expression-equivalent to tag(b, as...) for any set of arguments as... and for any tag whose type satisfies forwarding-receiver-query.

      5. The expression p.unhandled_stopped() is expression-equivalent to (execution::set_stopped((R&&) r), noop_coroutine()).

      6. For some expression e, the expression p.await_transform(e) is expression-equivalent to tag_invoke(as_awaitable, e, p) if that expression is well-formed; otherwise, it is expression-equivalent to e.

      The operand of the requires-clause of connect-awaitable is equivalent to receiver_of<R> if await-result-type<S, connect-awaitable-promise> is cv void; otherwise, it is receiver_of<R, await-result-type<S, connect-awaitable-promise>>.

    3. Otherwise, execution::connect(s, r) is ill-formed.

  3. Standard sender types shall always expose an rvalue-qualified overload of a customization of execution::connect. Standard sender types shall only expose an lvalue-qualified overload of a customization of execution::connect if they are copyable.

10.8.3. Sender queries [exec.snd_queries]

10.8.3.1. execution::forwarding_sender_query [exec.snd_queries.forwarding_sender_query]
  1. execution::forwarding_sender_query is used to ask a customization point object whether it is a sender query that should be forwarded through sender adaptors.

  2. The name execution::forwarding_sender_query denotes a customization point object. For some subexpression t, execution::forwarding_sender_query(t) is expression equivalent to:

    1. tag_invoke(execution::forwarding_sender_query, t) contextually converted to bool, if the tag_invoke expression is well formed.

      • Mandates: The tag_invoke expression is indeed contextually convertible to bool, that expression and the contextual conversion are not potentially-throwing and are core constant expressions if t is a core constant expression.

    2. Otherwise, false.

10.8.3.2. execution::get_completion_scheduler [exec.snd_queries.get_completion_scheduler]
  1. execution::get_completion_scheduler is used to ask a sender object for the completion scheduler for one of its signals.

  2. The name execution::get_completion_scheduler denotes a customization point object template. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::get_completion_scheduler<CPO>(s) is ill-formed for all template arguments CPO. If the template argument CPO in execution::get_completion_scheduler<CPO> is not one of execution::set_value_t, execution::set_error_t, or execution::set_stopped_t, execution::get_completion_scheduler<CPO> is ill-formed. Otherwise, execution::get_completion_scheduler<CPO>(s) is expression-equivalent to:

    1. tag_invoke(execution::get_completion_scheduler<CPO>, as_const(s)) if this expression is well formed.

      • Mandates: The tag_invoke expression above is not potentially throwing and its type satisfies execution::scheduler.

    2. Otherwise, execution::get_completion_scheduler<CPO>(s) is ill-formed.

  3. If, for some sender s and customization point object CPO, execution::get_completion_scheduler<decltype(CPO)>(s) is well-formed and results in a scheduler sch, and the sender s invokes CPO(r, args...), for some receiver r which has been connected to s, with additional arguments args..., on an execution agent which does not belong to the associated execution context of sch, the behavior is undefined.

10.8.4. Sender factories [exec.factories]

10.8.4.1. General [exec.factories.general]
  1. Subclause [exec.factories] defines sender factories, which are utilities that return senders without accepting senders as arguments.

10.8.4.2. execution::schedule [exec.schedule]
  1. execution::schedule is used to obtain a sender associated with a scheduler, which can be used to describe work to be started on that scheduler’s associated execution context.

  2. The name execution::schedule denotes a customization point object. For some subexpression s, the expression execution::schedule(s) is expression-equivalent to:

    1. tag_invoke(execution::schedule, s), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, execution::schedule(s) is ill-formed.

10.8.4.3. execution::just [exec.just]
  1. execution::just is used to create a sender that propagates a set of values to a connected receiver.

    template<class... Ts>
    struct just-sender // exposition only
      : completion_signatures<set_value_t(Ts...), set_error_t(exception_ptr)>
    {
      tuple<Ts...> vs_;
    
      template<class R>
      struct operation_state {
        tuple<Ts...> vs_;
        R r_;
    
        friend void tag_invoke(start_t, operation_state& s) noexcept {
          try {
            apply([&s](Ts &... values_) {
              set_value(std::move(s.r_), std::move(values_)...);
            }, s.vs_);
          }
          catch (...) {
            set_error(std::move(s.r_), current_exception());
          }
        }
      };
    
      template<receiver R>
        requires receiver_of<R, Ts...> && (copy_constructible<Ts> &&...)
      friend operation_state<decay_t> tag_invoke(connect_t, const just-sender& j, R && r) {
        return { j.vs_, std::forward<R>(r) };
      }
    
      template<receiver R>
        requires receiver_of<R, Ts...>
      friend operation_state<decay_t> tag_invoke(connect_t, just-sender&& j, R && r) {
        return { std::move(j.vs_), std::forward<R>(r) };
      }
    };
    
    template<movable-value... Ts>
      just-sender<decay_t<Ts>...> just(Ts &&... ts) noexcept(see-below);
    
  2. Effects: Initializes vs_ with decayed-tuple<Ts...>(std::forward<Ts>(ts)...).

  3. Remarks: The expression in the noexcept-specifier is equivalent to

    (is_nothrow_constructible_v<decay_t<Ts>, Ts> && ...)
    
10.8.4.4. execution::transfer_just [exec.transfer_just]
  1. execution::transfer_just is used to create a sender that propagates a set of values to a connected receiver on an execution agent belonging to the associated execution context of a specified scheduler.

  2. The name execution::transfer_just denotes a customization point object. For some subexpressions s and vs..., let S be decltype((s)) and Vs... be decltype((vs)). If S does not satisfy execution::scheduler, or any type V in Vs does not satisfy movable-value, execution::transfer_just(s, vs...) is ill-formed. Otherwise, execution::transfer_just(s, vs...) is expression-equivalent to:

    1. tag_invoke(execution::transfer_just, s, vs...), if that expression is valid. If the function selected by tag_invoke does not return a sender whose set_value completion scheduler is equivalent to s and sends values equivalent to auto(vs)... to a receiver connected to it, the program is ill-formed with no diagnostic required.

      • Mandates: execution::sender_of<R, no_env, decltype(auto(vs))...>, where R is the type of the tag_invoke expression above.

    2. Otherwise, execution::transfer(execution::just(vs...), s).

10.8.4.5. execution::just_error [exec.just_error]
  1. execution::just_error is used to create a sender that propagates an error to a connected receiver.

    template<class T>
    struct just-error-sender // exposition only
      : completion_signatures<set_error_t(T)>
    {
      T err_;
    
      template<class R>
      struct operation_state {
        T err_;
        R r_;
    
        friend void tag_invoke(start_t, operation_state& s) noexcept {
          set_error(std::move(s.r_), std::move(err_));
        }
      };
    
      template<receiver R>
        requires receiver<R, T> && copy_constructible<T>
      friend operation_state<decay_t<R>> tag_invoke(connect_t, const just-error-sender& j, R && r) {
        return { j.err_, std::forward<R>(r) };
      }
    
      template<receiver R>
        requires receiver<R, T>
      friend operation_state<decay_t<R>> tag_invoke(connect_t, just-error-sender&& j, R && r) {
        return { std::move(j.err_), std::forward<R>(r) };
      }
    };
    
    template<movable-value T>
      just-error-sender<decay_t<T>> just_error(T && t) noexcept(see-below);
    
  2. Effects: Returns a just-error-sender<decay_t<T>> with err_ direct initialized with static_cast<T&&>(t).

  3. Remarks: The expression in the noexcept-specifier is equivalent to

    is_nothrow_constructible_v<decay_t<T>, T>
    
10.8.4.6. execution::just_stopped [exec.just_stopped]
  1. execution::just_stopped is used to create a sender that propagates a stopped signal to a connected receiver.

    struct just-stopped-sender // exposition only
      : completion_signatures<set_stopped_t()>
    {
      template<class R>
      struct operation_state {
        R r_;
    
        friend void tag_invoke(start_t, operation_state& s) noexcept {
          set_stopped(std::move(s.r_));
        }
      };
    
      template<receiver R>
      friend operation_state<decay_t<R>> tag_invoke(connect_t, const just-stopped-sender& j, R && r) {
        return { std::forward<R>(r) };
      }
    };
    
    just-stopped-sender just_stopped() noexcept;
    
  2. Effects: Equivalent to just-stopped-sender{}.

10.8.4.7. execution::read [exec.read]
  1. execution::read is used to create a sender that retrieves a value from the receiver’s associated environment and sends it back to the receiver through the value channel.

  2. execution::read is a customization point object of an unspecified class type equivalent to:

    template <class Tag>
      struct read-sender; // exposition only
    
    struct read-t { // exposition only
      template <class Tag>
        read-sender<Tag> operator()(Tag) const noexcept { return {}; }
    };
    
  3. read-sender is an exposition only class template equivalent to:

    template <class Tag>
      struct read-sender { // exposition only
        template<class R>
          struct operation-state { // exposition only
            R r_;
    
            friend void tag_invoke(start_t, operation-state& s) noexcept try {
              auto value = Tag{}(get_env(s.r_));
              set_value(std::move(s.r_), std::move(value));
            } catch(...) {
              set_error(std::move(s.r_), current_exception());
            }
          };
    
        template<receiver R>
          requires callable<Tag, env_of_t<R>> &&
            receiver_of<R, call-result-t<Tag, env_of_t<R>>>
        friend operation-state<decay_t<R>> tag_invoke(connect_t, read-sender, R && r) {
          return { std::forward<R>(r) };
        }
    
        friend empty-env tag_invoke(get_completion_signatures_t, read-sender, auto);
    
        template<class Env>
          requires (!same_as<Env, no_env>) && callable<Tag, Env>
        friend auto tag_invoke(get_completion_signatures_t, read-sender, Env)
          -> completion_signatures<
              set_value_t(call-result-t<Tag, Env>), set_error_t(exception_ptr)>;
      };
    

10.8.5. Sender adaptors [exec.adapt]

10.8.5.1. General [exec.adapt.general]
  1. Subclause [exec.adapt] defines sender adaptors, which are utilities that transform one or more senders into a sender with custom behaviors. When they accept a single sender argument, they can be chained to create sender chains.

  2. The bitwise OR operator is overloaded for the purpose of creating sender chains. The adaptors also support function call syntax with equivalent semantics.

  3. Unless otherwise specified, a sender adaptor is required to not begin executing any functions which would observe or modify any of the arguments of the adaptor before the returned sender is connected with a receiver using execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor.

  4. A type T is a forwarding sender query if it is the type of a customization point object that models forwarding-sender-query. Unless otherwise specified, all sender adaptors that accept a single sender argument return sender objects that propagate forwarding sender queries to that single sender argument. This requirement applies to any function that is selected by the implementation of the sender adaptor.

  5. A type T is a forwarding receiver query if it is the type of a customization point object that models forwarding-receiver-query. Unless otherwise specified, whenever a sender adaptor constructs a receiver that it passes to another sender’s connect, that receiver shall propagate forwarding receiver queries to a receiver accepted as an argument of execution::connect. This requirements applies to any sender returned from a function that is selected by the implementation of such sender adaptor.

  6. For any sender type, receiver type, operation state type, execution environment type, or coroutine promise type that is part of the implementation of any sender adaptor in this subclause and that is a class template, the template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity.

    [Example:

    namespace sender-adaptors { // exposition only
      template <class Sch, class S> // arguments are not associated entities ([lib.tmpl-heads])
      class on-sender {
        // ...
      };
    
      struct on_t {
        template <scheduler Sch, sender S>
        on-sender<Sch, S> operator()(Sch&& sch, S&& s) const {
          // ...
        }
      };
    }
    inline constexpr sender-adaptors::on_t on{};
    

    -- end example]

10.8.5.2. Sender adaptor closure objects [exec.adapt.objects]
  1. A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender. For a sender adaptor closure object C and an expression S such that decltype((S)) models sender, the following expressions are equivalent and yield a sender:

    C(S)
    S | C
    

    Given an additional pipeable sender adaptor closure object D, the expression C | D produces another pipeable sender adaptor closure object E:

    E is a perfect forwarding call wrapper ([func.require]) with the following properties:

    • Its target object is an object d of type decay_t<decltype((D))> direct-non-list-initialized with D.

    • It has one bound argument entity, an object c of type decay_t<decltype((C))> direct-non-list-initialized with C.

    • Its call pattern is d(c(arg)), where arg is the argument used in a function call expression of E.

    The expression C | D is well-formed if and only if the initializations of the state entities of E are all well-formed.

  2. An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base classes of type sender_adaptor_closure<U> for any other type U, and T does not model sender.

  3. The template parameter D for sender_adaptor_closure may be an incomplete type. Before any expression of type cv D appears as an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>. The behavior of an expression involving an object of type cv D as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.

  4. A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a sender.

  5. If a pipeable sender adaptor object accepts only one argument, then it is a pipeable sender adaptor closure object.

  6. If a pipeable sender adaptor object adaptor accepts more than one argument, then let s be an expression such that decltype((s)) models sender, let args... be arguments such that adaptor(s, args...) is a well-formed expression as specified in the rest of this subclause ([exec.adapt.objects]), and let BoundArgs be a pack that denotes decay_t<decltype((args))>.... The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:

    • Its target object is a copy of adaptor.

    • Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std::forward<decltype((args))>(args)..., respectively.

    • Its call pattern is adaptor(r, bound_args...), where r is the argument used in a function call expression of f.

The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above, are all well-formed.

10.8.5.3. execution::on [exec.on]
  1. execution::on is used to adapt a sender in a sender that will start the input sender on an execution agent belonging to a specific execution context.

  2. The name execution::on denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::on is ill-formed. Otherwise, the expression execution::on(sch, s) is expression-equivalent to:

    1. tag_invoke(execution::on, sch, s), if that expression is valid. If the function selected above does not return a sender which starts s on an execution agent of the associated execution context of sch when started, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, constructs a sender s1. When s1 is connected with some receiver out_r, it:

      1. Constructs a receiver r such that:

        1. When execution::set_value(r) is called, it calls execution::connect(s, r2), where r2 is as specified below, which results in op_state3. It calls execution::start(op_state3). If any of these throws an exception, it calls execution::set_error on out_r, passing current_exception() as the second argument.

        2. execution::set_error(r, e) is expression-equivalent to execution::set_error(out_r, e).

        3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).

      2. Calls execution::schedule(sch), which results in s2. It then calls execution::connect(s2, r), resulting in op_state2.

      3. op_state2 is wrapped by a new operation state, op_state1, that is returned to the caller.

    3. r2 is a receiver that wraps a reference to out_r and forwards all receiver completion-signals to it. In addition, execution::get_env(r2) returns an object e such that execution::get_scheduler(e) returns a copy of sch, and tag_invoke(tag, e, args...) is expression-equivalent to tag(e, args...) for all arguments args... and for all tag whose type satisfies forwarding-env-query and is not execution::get_scheduler_t.

    4. When execution::start is called on op_state1, it calls execution::start on op_state2.

    5. The lifetime of op_state2, once constructed, lasts until either op_state3 is constructed or op_state1 is destroyed, whichever comes first. The lifetime of op_state3, once constructed, lasts until op_state1 is destroyed.

10.8.5.4. execution::transfer [exec.transfer]
  1. execution::transfer is used to adapt a sender into a sender with a different associated set_value completion scheduler. [Note: it results in a transition between different execution contexts when executed. --end note]

  2. The name execution::transfer denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::transfer is ill-formed. Otherwise, the expression execution::transfer(s, sch) is expression-equivalent to:

    1. tag_invoke(execution::transfer, get_completion_scheduler<set_value_t>(s), s, sch), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::transfer, s, sch), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, schedule_from(sch, s).

    If the function selected above does not return a sender which is a result of a call to execution::schedule_from(sch, s2), where s2 is a sender which sends equivalent to those sent by s, the program is ill-formed with no diagnostic required.

  3. Senders returned from execution::transfer shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.

10.8.5.5. execution::schedule_from [exec.schedule_from]
  1. execution::schedule_from is used to schedule work dependent on the completion of a sender onto a scheduler’s associated execution context. [Note: schedule_from is not meant to be used in user code; they are used in the implementation of transfer. -end note]

  2. The name execution::schedule_from denotes a customization point object. For some subexpressions sch and s, let Sch be decltype((sch)) and S be decltype((s)). If Sch does not satisfy execution::scheduler, or S does not satisfy execution::sender, execution::schedule_from is ill-formed. Otherwise, the expression execution::schedule_from(sch, s) is expression-equivalent to:

    1. tag_invoke(execution::schedule_from, sch, s), if that expression is valid. If the function selected by tag_invoke does not return a sender which completes on an execution agent belonging to the associated execution context of sch and sends signals equivalent to those sent by s, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:

      1. Constructs a receiver r such that when a receiver completion-signal Signal(r, args...) is called, it decay-copies args... into op_state (see below) as args'... and constructs a receiver r2 such that:

        1. When execution::set_value(r2) is called, it calls Signal(out_r, std::move(args')...).

        2. execution::set_error(r2, e) is expression-equivalent to execution::set_error(out_r, e).

        3. execution::set_stopped(r2) is expression-equivalent to execution::set_stopped(out_r).

        It then calls execution::schedule(sch), resulting in a sender s3. It then calls execution::connect(s3, r2), resulting in an operation state op_state3. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(out_r, current_exception()). If any of these expressions would be ill-formed, then Signal(r, args...) is ill-formed.

      2. Calls execution::connect(s, r) resulting in an operation state op_state2. If this expression would be ill-formed, execution::connect(s2, out_r) is ill-formed.

      3. Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2). The lifetime of op_state3 ends when op_state is destroyed.

    3. Given an expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Es... be the set of types in the type-list named by error_types_of_t<S, E, type-list>, and let Vs... be the set of unique types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, where set-value-signature is the alias template:

      template <class... Ts>
        using set-value-signature = set_value_t(Ts...);
      

      Let Bs... be the set of unique types in [Es..., exception_ptr]. If either completion_signatures_of_t<schedule_result_t<Sch>, E>::sends_stopped or completion_signatures_of_t<S, E>::sends_stopped is true, then the type of tag_invoke(get_completion_signatures, s2, e) is a class type equivalent to:

      completion_signatures<Vs..., set_error_t(Bs)..., set_stopped_t()>
      

      Otherwise:

      completion_signatures<Vs..., set_error_t(Bs)...>
      
  3. Senders returned from execution::schedule_from shall not propagate the sender queries get_completion_scheduler<CPO> to an input sender. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.

10.8.5.6. execution::then [exec.then]
  1. execution::then is used to attach an invocable as a continuation for the successful completion of the input sender.

  2. The name execution::then denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue refering to an object decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::then is ill-formed. Otherwise, the expression execution::then(s, f) is expression-equivalent to:

    1. tag_invoke(execution::then, get_completion_scheduler<set_value_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::then, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:

      1. Constructs a receiver r such that:

        1. When execution::set_value(r, args...) is called, let v be the expression invoke(f', args...). If decltype(v) is void, calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r, v). If any of these throw an exception, it catches it and calls execution::set_error(out_r, current_exception()). If any of these expressions would be ill-formed, the expression execution::set_value(r, args...) is ill-formed.

        2. execution::set_error(r, e) is expression-equivalent to execution::set_error(out_r, e).

        3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).

      2. Returns an expression equivalent to execution::connect(s, r).

      3. Given an expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let V be value_types_of_t<S, E, result-type, type-list>, where result-type is the alias template:

        template <class... Ts>
          using result-type = invoke_result_t<F, Ts...>;
        
        1. If V is ill-formed, type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        2. Otherwise, let As... be the unique set of types in the type-list named by V, and let Bs... be the unique set of types in the type-list named by error_types_of_t<S, E, type-list> with the addition of exception_ptr. If completion_signatures_of_t<S, E>::sends_stopped is true, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to:

          completion_signatures<
            unary-set-value-signature<As>..., set_error_t(Bs)..., set_stopped_t()>
          

          Otherwise:

          completion_signatures<
            unary-set-value-signature<As>..., set_error_t(Bs)...>
          

          where unary-set-value-signature<T> is an alias for set_value_t() if T is void; otherwise, set_value_t(T).

    If the function selected above does not return a sender that invokes f with the result of the set_value signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.7. execution::upon_error [exec.upon_error]
  1. execution::upon_error is used to attach an invocable as a continuation for the unsuccessful completion of the input sender.

  2. The name execution::upon_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue refering to an object decay-copied from f. If S does not satisfy execution::sender, or F does not model movable-value, execution::upon_error is ill-formed. Otherwise, the expression execution::upon_error(s, f) is expression-equivalent to:

    1. tag_invoke(execution::upon_error, get_completion_scheduler<set_error_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::upon_error, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:

      1. Constructs a receiver r such that:

        1. execution::set_value(r, args...) is expression-equivalent to execution::set_value(out_r, args...).

        2. When execution::set_error(r, e) is called, let v be the expression invoke(f', e). If decltype(v) is void, calls execution::set_value(out_r); otherwise, it calls execution::set_value(out_r, v). If any of these throw an exception, it catches it and calls execution::set_error(out_r, current_exception()). If any of these expressions would be ill-formed, the expression execution::set_error(r, e) is ill-formed.

        3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).

      2. Returns an expression equivalent to execution::connect(s, r).

      3. Given some expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let V be error_types_of_t<S, E, result-type-list>, where result-type-list is the alias template:

        template <class... Ts>
          using result-type-list = type-list<invoke_result_t<F, Ts>...>;
        
        1. If V is ill-formed, type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        2. Otherwise, let As... be the set of types in the type-list named by V, let Bs... be the set of types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, and let Cs... be the unique set of types in [unary-set-value-signature<As>..., Bs...], where unary-set-value-signature<T> is an alias for set_value_t() if T is void; otherwise, set_value_t(T), and set-value-signature is the alias template:

          template <class... Ts>
            using set-value-signature = set_value_t(Ts...);
          
        3. If completion_signatures_of_t<S, E>::sends_stopped is true, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to:

          completion_signatures<Cs..., set_error_t(exception_ptr), set_stopped_t()>
          

          Otherwise:

          completion_signatures<Cs..., set_error_t(exception_ptr)>
          

    If the function selected above does not return a sender which invokes f with the result of the set_error signal of s, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.8. execution::upon_stopped [exec.upon_stopped]
  1. execution::upon_stopped is used to attach an invocable as a continuation for the completion of the input sender using the "stopped" channel.

  2. The name execution::upon_stopped denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue refering to an object decay-copied from f. If S does not satisfy execution::sender, or F does not model both movable-value and invocable, execution::upon_stopped is ill-formed. Otherwise, the expression execution::upon_stopped(s, f) is expression-equivalent to:

    1. tag_invoke(execution::upon_stopped, get_completion_scheduler<set_stopped_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::upon_stopped, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:

      1. Constructs a receiver r such that:

        1. execution::set_value(r, args...) is expression-equivalent to execution::set_value(out_r, args...).

        2. execution::set_error(r, e) is expression-equivalent to execution::set_error(out_r, e).

        3. When execution::set_stopped(r) is called, let v be the expression invoke(f'). If v has type void, calls execution::set_value(out_r); otherwise, calls execution::set_value(out_r, v). If any of these throw an exception, it catches it and calls execution::set_error(out_r, current_exception()). If any of these expressions would be ill-formed, the expression execution::set_stopped(r) is ill-formed.

      2. Returns an expression equivalent to execution::connect(s, r).

      3. Given some expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be the set of types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list> and let Es... be the unique set of types in the type-list named by error_types_of_t<S, E, type-list> with the addition of exception_ptr, where set-value-signature is the alias template:

        template <class... Ts>
          using set-value-signature = set_value_t(Ts...);
        
      4. If invoke_result_t<F> is void, let B be set_value_t(); otherwise, let B be set_value_t(invoke_result_t<F>). Let As... be the unique set of types in [Vs..., B]. The type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to:

        completion_signatures<As..., set_error_t(Es)...>
        

    If the function selected above does not return a sender which invokes f when s completes by calling set_stopped, passing the return value as the value to any connected receivers, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.9. execution::let_value [exec.let_value]
  1. execution::let_value is used to insert continuations creating more work dependent on the results of their input senders into a sender chain.

  2. The name execution::let_value denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender, execution::let_value is ill-formed. Otherwise, the expression execution::let_value(s, f) is expression-equivalent to:

    1. tag_invoke(execution::let_value, get_completion_scheduler<set_value_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::let_value, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.

      1. Let r be an rvalue of a receiver type R such that:

        1. When execution::set_value(r, args...) is called, decay-copies args... into op_state2 as args'..., then calls invoke(f', args'...), resulting in a sender s3. It then calls execution::connect(s3, std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'), current_exception()). If any of these expressions would be ill-formed, execution::set_value(r, args...) is ill-formed.

        2. execution::set_error(r, e) is expression-equivalent to execution::set_error(std::move(out_r'), e).

        3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(std::move(out_r')).

      2. execution::let_value(s, f) returns a sender s2 such that:

        1. If the expression execution::connect(s, r) is ill-formed, execution::connect(s2, out_r) is ill-formed.

        2. Otherwise, let op_state2 be the result of execution::connect(s, r). execution::connect(s2, out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).

      3. Given some expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Xs... be the set of types in the type-list named by error_types_of_t<S, E, type-list>, and let V be value_types_of_t<S, E, result-type, type-list>, where result-type is the alias template:

        template <class... Ts>
          using result-type = invoke_result_t<F, decay_t<Ts>&...>;
        
        1. If V is ill-formed, type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        2. Otherwise, let S3s... be the unique set of types in the type-list named by V. If (sender<S3s, E> &&...) is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        3. Otherwise, for each type S3i in S3s..., let Vsi... be the list of types in the type-list named by value_types_of_t<S3i, E, set-value-signature, type-list>, let Esi... be the list of types in the type-list named by error_types_of_t<S3i, E, type-list>, and let Dsi... be the list of values completion_signatures_of_t<S3s, E>::sends_stopped..., where set-value-signature is the following alias template:

          template <class... Ts>
            using set-value-signature = set_value_t(Ts...);
          
        4. Let Us... be the set of unique types in [Vs0..., Vs1..., ... Vsn-1...] and let Ws... be the unique set of types in [exception_ptr, Xs..., Es0..., Es1..., ... Esn-1...], where n is sizeof...(S3s).

        5. If either completion_signatures_of_t<S, E>::sends_stopped or (Ds ||...) is true, the type of get_completion_signatures(s2, e) is equivalent to:

          completion_signatures <Us..., set_error_t(Ws)..., set_stopped_t()>
          

          Otherwise:

          completion_signatures <Us..., set_error_t(Ws)...>
          

    If execution::let_value(s, f) does not return a sender that invokes f when set_value is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.10. execution::let_error [exec.let_error]
  1. execution::let_error is used to insert continuations creating more work dependent on the error of its input senders into a sender chain.

  2. The name execution::let_error denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender, execution::let_error is ill-formed. Otherwise, the expression execution::let_error(s, f) is expression-equivalent to:

    1. tag_invoke(execution::let_error, get_completion_scheduler<set_value_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::let_error, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.

      1. Let r be an rvalue of a receiver type R such that:

        1. execution::set_value(r, args...) is expression-equivalent to execution::set_value(std::move(out_r'), args...).

        2. When execution::set_error(r, e) is called, decay-copies e into op_state2 as e', then calls invoke(f', e'), resulting in a sender s3. It then calls execution::connect(s3, std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'), current_exception()). If any of these expressions would be ill-formed, execution::set_error(r, e) is ill-formed.

        3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(std::move(out_r')).

      2. execution::let_error(s, f) returns a sender s2 such that:

        1. If the expression execution::connect(s, r) is ill-formed, execution::connect(s2, out_r) is ill-formed.

        2. Otherwise, let op_state2 be the result of execution::connect(s, r). execution::connect(s2, out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).

      3. Given some expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let As... be the set of types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, and let W be error_types_of_t<S, E, result-type-list>, where set-value-signature is the alias template:

        template <class... Ts>
          using set-value-signature = set_value_t(Ts...);
        

        and result-type-list is the alias template:

        template <class... Ts>
          using result-type-list =
            type-list<invoke_result_t<F, decay_t<Ts>&>...>;
        
        1. If W is ill-formed, type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        2. Otherwise, let S3s... be the unique set of types in the type-list named by W. If (sender<S3s, E> &&...) is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>.

        3. Otherwise, for each type S3i in S3s..., let Vsi... be the list of types in the type-list named by value_types_of_t<S3i, E, set-value-signature, type-list>, let Esi... be the list of types in the type-list named by error_types_of_t<S3i, E, type-list>, and let Dsi... be the list of values completion_signatures_of_t<S3s, E>::sends_stopped....

        4. Let Us... be the set of unique types in [As..., Vs0..., Vs1..., ... Vsn-1...] and let Ws... be the unique set of types in [exception_ptr, Es0..., Es1..., ... Esn-1...], where n is sizeof...(S3s).

        5. If either completion_signatures_of_t<S, E>::sends_stopped or (Ds ||...) is true, the type of get_completion_signatures(s2, e) is equivalent to:

          completion_signatures <Us..., set_error_t(Ws)..., set_stopped_t()>
          

          Otherwise:

          completion_signatures <Us..., set_error_t(Ws)...>
          

    If execution::let_error(s, f) does not return a sender that invokes f when set_error is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.11. execution::let_stopped [exec.let_stopped]
  1. execution::let_stopped is used to insert continuations creating more work dependent on the error its input senders into a sender chain.

  2. The name execution::let_stopped denotes a customization point object. For some subexpressions s and f, let S be decltype((s)), let F be the decayed type of f, and let f' be an xvalue that refers to an object decay-copied from f. If S does not satisfy execution::sender or if F does not satisfy invocable, execution::let_stopped is ill-formed. Otherwise, the expression execution::let_stopped(s, f) is expression-equivalent to:

    1. tag_invoke(execution::let_stopped, get_completion_scheduler<set_value_t>(s), s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::let_stopped, s, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, given a receiver out_r and an lvalue out_r' refering to an object decay-copied from out_r.

      1. Let r be an rvalue of a receiver type R such that:

        1. execution::set_value(r, args...) is expression-equivalent to execution::set_value(std::move(out_r'), args...).

        2. execution::set_error(r, e) is expression-equivalent to execution::set_value(std::move(out_r'), e).

        3. When execution::set_stopped(r) is called, calls invoke(f'), resulting in a sender s3. It then calls execution::connect(s3, std::move(out_r')), resulting in an operation state op_state3. op_state3 is saved as a part of op_state2. It then calls execution::start(op_state3). If any of these throws an exception, it catches it and calls execution::set_error(std::move(out_r'), current_exception()). If any of these expressions would be ill-formed, execution::set_stopped(r) is ill-formed.

      2. execution::let_stopped(s, f) returns a sender s2 such that:

        1. If the expression execution::connect(s, r) is ill-formed, execution::connect(s2, out_r) is ill-formed.

        2. Otherwise, let op_state2 be the result of execution::connect(s, r). execution::connect(s2, out_r) returns an operation state op_state that stores op_state2. execution::start(op_state) is expression-equivalent to execution::start(op_state2).

      3. Given some expression e, let E be decltype((e)), and let S3 be invoke_result_t<F>. If either sender<S, E> or sender<S3, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let As... be the set of types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, and let Ws... be the types in the type-list named by error_types_of_t<S, E, type-list>, where set-value-signature is the alias template:

        template <class... Ts>
          using set-value-signature = set_value_t(Ts...);
        
        1. Let Vs... be the list of types in the type-list named by value_types_of_t<S3, E, set-value-signature, type-list>, and let Es... be the list of types in the type-list named by error_types_of_t<S3, E, type-list>.

        2. Let Us... be the set of unique types in [As..., Vs...] and let Ws... be the unique set of types in [exception_ptr, Es...],.

        3. If completion_signatures_of_t<S3, E>::sends_stopped is true, the type of get_completion_signatures(s2, e) is equivalent to:

          completion_signatures <Us..., set_error_t(Ws)..., set_stopped_t()>
          

          Otherwise:

          completion_signatures <Us..., set_error_t(Ws)...>
          

    If execution::let_stopped(s, f) does not return a sender that invokes f when set_stopped is called, and making its completion dependent on the completion of a sender returned by f, and propagates the other completion-signals sent by s, the program is ill-formed with no diagnostic required.

10.8.5.12. execution::bulk [exec.bulk]
  1. execution::bulk is used to run a task repeatedly for every index in an index space.

  2. The name execution::bulk denotes a customization point object. For some subexpressions s, shape, and f, let S be decltype((s)), Shape be decltype((shape)), and F be decltype((f)). If S does not satisfy execution::sender or Shape does not satisfy integral, execution::bulk is ill-formed. Otherwise, the expression execution::bulk(s, shape, f) is expression-equivalent to:

    1. tag_invoke(execution::bulk, get_completion_scheduler<set_value_t>(s), s, shape, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::bulk, s, shape, f), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2. When s2 is connected with some receiver out_r, it:

      1. Constructs a receiver r:

        1. When execution::set_value(r, args...) is called, calls f(i, args...) for each i of type Shape from 0 to shape, then calls execution::set_value(out_r, args...). If any of these throws an exception, it catches it and calls execution::set_error(out_r, current_exception()).

        2. When execution::set_error(r, e) is called, calls execution::set_error(out_r, e).

        3. When execution::set_stopped(r) is called, calls execution::set_stopped(out_r, e).

      2. Calls execution::connect(s, r), which results in an operation state op_state2.

      3. Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).

    4. Given an expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Es... be the types in the type-list named by error_types_of_t<S, E, type-list>, and let Bs... be the set of unique types in [Es..., exception_ptr]. The type of tag_invoke(get_completion_signatures, s2, e) shall be a class type Tr such that, for variadic templates Tuple and Variant:

      1. Tr::value_types<Tuple, Variant> names the same type as value_types_of_t<S, E, Tuple, Variant>.

      2. Tr::error_types<Variant> names the type Variant<Bs...>.

      3. Tr::sends_stopped is a core constant expression of type bool and value completion_signatures_of_t<S, E>::sends_stopped.

    If the function selected above does not return a sender which invokes f(i, args...) for each i of type Shape from 0 to shape when the input sender sends values args..., or does not propagate the values of the signals sent by the input sender to a connected receiver, the program is ill-formed with no diagnostic required.

10.8.5.13. execution::split [exec.split]
  1. execution::split is used to adapt an arbitrary sender into a sender that can be connected multiple times.

  2. Let split-env be the type of an execution environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.

  3. The name execution::split denotes a customization point object. For some subexpression s, let S be decltype((s)). If execution::sender<S, split-env> is false, execution::split is ill-formed. Otherwise, the expression execution::split(s) is expression-equivalent to:

    1. tag_invoke(execution::split, get_completion_scheduler<set_value_t>(s), s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::split, s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2, which:

      1. Creates an object sh_state that contains a stop_source, a list of pointers to operation states awaiting the completion of s, and that also reserves space for storing:

        • the operation state that results from connecting s with r described below, and

        • the sets of values and errors with which s may complete, with the addition of exception_ptr.

      2. Constructs a receiver r such that:

        1. When execution::set_value(r, args...) is called, decay-copies the expressions args... into sh_state. It then notifies all the operation states in sh_state's list of operation states that the results are ready. If any exceptions are thrown, the exception is caught and execution::set_error(r, current_exception()) is called instead.

        2. When execution::set_error(r, e) is called, decay-copies e into sh_state. It then notifies the operation states in sh_state's list of operation states that the results are ready.

        3. When execution::set_stopped(r) is called, it then notifies the operation states in sh_state's list of operation states that the results are ready.

        4. get_env(r) is an expression e of type split-env such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on sh_state's stop source.

      3. Calls execution::connect(s, r), resulting in an operation state op_state2. op_state2 is saved in sh_state.

      4. When s2 is connected with a receiver out_r of type OutR, it returns an operation state object op_state that contains:

        • An object out_r' of type OutR decay-copied from out_r,

        • A reference to sh_state,

        • A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:

          struct stop-callback-fn {
            stop_source& stop_src_;
            void operator()() noexcept {
              stop_src_.request_stop();
            }
          };
          
      5. When execution::start(op_state) is called:

        • If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal was used to complete r’s receiver contract ([exec.recv]). Calls Signal(out_r', args2...), where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r, args...) and returns.

        • Otherwise, it emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.

        • Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r').

        • Otherwise, it adds a pointer to op_state to the list of operation states in sh_state and calls execution::start(op_state2) if this would be the first such invocation.

      6. When r completes it will notify op_state that the result are ready. Let Signal be whichever receiver completion-signal was used to complete r's receiver contract ([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'), args2...) is called, where args2... is a pack of lvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r, args...).

      7. Ownership of sh_state is shared by s2 and by every op_state that results from connecting s2 to a receiver.

    4. Given an expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_completion_signatures, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be the set of unique types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, and let Es... be the set of types in the type-list named by error_types_of_t<S, E, error-types>, where set-value-signature is the alias template:

      template <class... Ts>
        using set-value-signature = set_value_t(decay_t<Ts>&...);
      

      and error-types is the alias template:

      template <class... Es>
        using error-types = type-list<decay_t<Es>&...>;
      

      Let Bs... be the set of unique types in [Es..., exception_ptr&]. If completion_signatures_of_t<S, E>::sends_stopped is true, then the type of tag_invoke(get_completion_signatures, s2, e) is a class type equivalent to:

      completion_signatures<Vs..., set_error_t(Bs)..., set_stopped_t()>
      

      Otherwise:

      completion_signatures<Vs..., set_error_t(Bs)...>
      

    If the function selected above does not return a sender which sends references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.

10.8.5.14. execution::when_all [exec.when_all]
  1. execution::when_all is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders that only send a single set of values. execution::when_all_with_variant is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders, each of which may have one or more sets of sent values.

  2. The name execution::when_all denotes a customization point object. For some subexpressions si..., let Si... be decltype((si)).... The expression execution::when_all(si...) is ill-formed if any of the following is true:

    • If the number of subexpressions si... is 0, or

    • If any type Si does not satisfy execution::sender.

    Otherwise, the expression execution::when_all(si...) is expression-equivalent to:

    1. tag_invoke(execution::when_all, si...), if that expression is valid. If the function selected by tag_invoke does not return a sender that sends a concatenation of values sent by si... when they all complete with set_value, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, constructs a sender w of type W. When w is connected with some receiver out_r of type OutR, it returns an operation state op_state specified as below:

      1. For each sender si, constructs a receiver ri such that:

        1. If execution::set_value(ri, ti...) is called for every ri, op_state's associated stop callback optional is reset and execution::set_value(out_r, t0..., t1..., ..., tn-1...) is called, where n the number of subexpressions in si....

        2. Otherwise, execution::set_error or execution::set_stopped was called for at least one receiver ri. If the first such to complete did so with the call execution::set_error(ri, e), request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_error(out_r, e) is called.

        3. Otherwise, request_stop is called on op_state's associated stop source. When all child operations have completed, op_state's associated stop callback optional is reset and execution::set_stopped(out_r) is called.

        4. For each receiver ri, get_env(ri) is an expression e such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on op_state's associated stop source, and for which tag_invoke(tag, e, args...) is expression-equivalent to tag(get_env(out_r), args...) for all arguments args... and all tag whose type satisfies forwarding-env-query and is not get_stop_token_t.

      2. For each sender si, calls execution::connect(si, ri), resulting in operation states child_opi.

      3. Returns an operation state op_state that contains:

        • Each operation state child_opi,

        • A stop source of type in_place_stop_source,

        • A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:

          struct stop-callback-fn {
            in_place_stop_source& stop_src_;
            void operator()() noexcept {
              stop_src_.request_stop();
            }
          };
          
      4. When execution::start(op_state) is called it:

        • Emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r)) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of op_state.

        • Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r).

        • Otherwise, calls execution::start(child_opi) for each child_opi.

      5. Given some expression e, let E be decltype((e)) and let WE be a type such that stop_token_of_t<WE> is in_place_stop_token and tag_invoke_result_t<Tag, WE, As...> names the type, if any, of call-result-t<Tag, E, As...> for all types As... and all Tag besides get_stop_token_t.

      • If for any sender Si, sender<Si, WE> is false, the type of tag_invoke(get_completion_signatures, w, e) shall be equivalent to dependent_completion_signatures<E>.

      • Otherwise, if type value_types_of_t<Si, WE, decayed-tuple, zero-or-one> is ill-formed, the type of tag_invoke(get_completion_signatures, w, e) shall be equivalent to dependent_completion_signatures<E>, where zero-or-one is an alias template equivalent to the following:

        template <class... Ts>
            requires (sizeof...(Ts) <= 1)
          using zero-or-one = void;
        
      • Otherwise, it shall be a class type Tr such that:

        • Tr::value_types<Tuple, Variant> is:

          • Variant<> if for any type Si, the type value_types_of_t<Si, WE, Tuple, Variant> is Variant<>.

          • Otherwise, Variant<Tuple<V0..., V1,..., Vn-1...>> where n is the count of types in Si..., and where Vi... is a set of types such that for the type Si, value_types_of_t<Si, WE, Tuple, Variant> is an alias for Variant<Tuple<Vi...>>.

        • Tr::error_types<Variant> is Variant<Ui...>, where Ui... is the unique set of types in [exception_ptr, E0..., E1,..., En-1...], where Ei... is a set of types such that for the type Si, error_types_of_t<Si, WE, Variant> is an alias for Variant<Ei...>.

        • Tr::sends_stopped is true.

  3. The name execution::when_all_with_variant denotes a customization point object. For some subexpressions s..., let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::when_all_with_variant is ill-formed. Otherwise, the expression execution::when_all_with_variant(s...) is expression-equivalent to:

    1. tag_invoke(execution::when_all_with_variant, s...), if that expression is valid. If the function selected by tag_invoke does not return a sender that, when connected with a receiver of type R, sends the types into-variant-type<S, env_of_t<R>>... when they all complete with set_value, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, execution::when_all(execution::into_variant(s)...).

  4. Senders returned from adaptors defined in this subclause shall not expose the sender queries get_completion_scheduler<CPO>.

10.8.5.15. execution::transfer_when_all [exec.transfer_when_all]
  1. execution::transfer_when_all is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders that only send a single set of values each, while also making sure that they complete on the specified scheduler. execution::transfer_when_all_with_variant is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders, which may have one or more sets of sent values. [Note: this can allow for better customization of the adaptors. --end note]

  2. The name execution::transfer_when_all denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype(sch) and S be decltype((s)). If Sch does not satisfy scheduler, or any type Si in S... does not satisfy execution::sender, execution::transfer_when_all is ill-formed. Otherwise, the expression execution::transfer_when_all(sch, s...) is expression-equivalent to:

    1. tag_invoke(execution::transfer_when_all, sch, s...), if that expression is valid. If the function selected by tag_invoke does not return a sender that sends a concatenation of values sent by s... when they all complete with set_value, or does not send its completion signals, other than ones resulting from a scheduling error, on an execution agent belonging to the associated execution context of sch, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, execution::transfer(execution::when_all(s...), sch).

  3. The name execution::transfer_when_all_with_variant denotes a customization point object. For some subexpressions sch and s..., let Sch be decltype((sch)) and let S be decltype((s)). If any type Si in S... does not satisfy execution::sender, execution::transfer_when_all_with_variant is ill-formed. Otherwise, the expression execution::transfer_when_all_with_variant(sch, s...) is expression-equivalent to:

    1. tag_invoke(execution::transfer_when_all_with_variant, s...), if that expression is valid. If the function selected by tag_invoke does not return a sender that, when connected with a receiver of type R, sends the types into-variant-type<S, env_of_t<R>>... when they all complete with set_value, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, execution::transfer_when_all(sch, execution::into_variant(s)...).

  4. Senders returned from execution::transfer_when_all shall not propagate the sender queries get_completion_scheduler<CPO> to input senders. They will implement get_completion_scheduler<CPO>, where CPO is one of set_value_t and set_stopped_t; this query returns a scheduler equivalent to the sch argument from those queries. The get_completion_scheduler<set_error_t> is not implemented, as the scheduler cannot be guaranteed in case an error is thrown while trying to schedule work on the given scheduler object.

10.8.5.16. execution::into_variant [exec.into_variant]
  1. execution::into_variant can be used to turn a sender which sends multiple sets of values into a sender which sends a variant of all of those sets of values.

  2. The template into-variant-type is used to compute the type sent by a sender returned from execution::into_variant.

    template<class S, class E>
        requires sender<S, E>
      using into-variant-type =
        value_types_of_t<S, E>;
    
    template<sender S>
      see-below into_variant(S && s);
    
  3. Effects: Returns a sender s2. When s2 is connected with some receiver out_r, it:

    1. Constructs a receiver r:

      1. If execution::set_value(r, ts...) is called, calls execution::set_value(out_r, into-variant-type<S>(decayed-tuple<decltype(ts)...>(ts...))).

      2. execution::set_error(r, e) is expression-equivalent to execution::set_error(out_r, e).

      3. execution::set_stopped(r) is expression-equivalent to execution::set_stopped(out_r).

    2. Calls execution::connect(s, r), resulting in an operation state op_state2.

    3. Returns an operation state op_state that contains op_state2. When execution::start(op_state) is called, calls execution::start(op_state2).

10.8.5.17. execution::stopped_as_optional [exec.stopped_as_optional]
  1. execution::stopped_as_optional is used to handle a stopped signal by mapping it into the value channel as an empty optional. The value channel is also converted into an optional. The result is a sender that never completes with stopped, reporting cancellation by completing with an empty optional.

  2. The name execution::stopped_as_optional denotes a customization point object. For some subexpression s, let S be decltype((s)). Let get-env-sender be an expression such that, when it is connected with a receiver r, start on the resulting operation state completes immediately by calling execution::set_value(r, get_env(r)). The expression execution::stopped_as_optional(s) is expression-equivalent to:

    execution::let_value(
      get-env-sender,
      []<class E>(const E&) requires single-sender<S, E> {
        return execution::let_stopped(
          execution::then(s,
            []<class T>(T&& t) {
              return optional<decay_t<single-sender-value-type<S, E>>>{
                static_cast<T&&>(t)
              };
            }
          ),
          [] () noexcept {
            return execution::just(optional<decay_t<single-sender-value-type<S, E>>>{});
          }
        );
      }
    )
    
10.8.5.18. execution::stopped_as_error [exec.stopped_as_error]
  1. execution::stopped_as_error is used to handle a stopped signal by mapping it into the error channel as a custom exception type. The result is a sender that never completes with stopped, reporting cancellation by completing with an error.

  2. The name execution::stopped_as_error denotes a customization point object. For some subexpressions s and e, let S be decltype((s)) and let E be decltype((e)). If the type S does not satisfy sender or if the type E doesn’t satisfy movable-value, execution::stopped_as_error(s, e) is ill-formed. Otherwise, the expression execution::stopped_as_error(s, e) is expression-equivalent to:

    execution::let_stopped(s, [] { return execution::just_error(e); })
    
10.8.5.19. execution::ensure_started [exec.ensure_started]
  1. execution::ensure_started is used to eagerly start the execution of a sender, while also providing a way to attach further work to execute once it has completed.

  2. Let ensure-started-env be the type of an execution environment such that, given an instance e, the expression get_stop_token(e) is well formed and has type stop_token.

  3. The name execution::ensure_started denotes a customization point object. For some subexpression s, let S be decltype((s)). If execution::sender<S, ensure-started-env> is false, execution::ensure_started(s) is ill-formed. Otherwise, the expression execution::ensure_started(s) is expression-equivalent to:

    1. tag_invoke(execution::ensure_started, get_completion_scheduler<set_value_t>(s), s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    2. Otherwise, tag_invoke(execution::ensure_started, s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above satisfies execution::sender.

    3. Otherwise, constructs a sender s2, which:

      1. Creates an object sh_state that contains a stop_source, an initially null pointer to an operation state awaitaing completion, and that also reserves space for storing:

        • the operation state that results from connecting s with r described below, and

        • the sets of values and errors with which s may complete, with the addition of exception_ptr.

        s2 shares ownership of sh_state with r described below.

      2. Constructs a receiver r such that:

        1. When execution::set_value(r, args...) is called, decay-copies the expressions args... into sh_state. It then checks sh_state to see if there is an operation state awaiting completion; if so, it notifies the operation state that the results are ready. If any exceptions are thrown, the exception is caught and execution::set_error(r, current_exception()) is called instead.

        2. When execution::set_error(r, e) is called, decay-copies e into sh_state. If there is an operation state awaiting completion, it then notifies the operation states that the results are ready.

        3. When execution::set_stopped(r) is called, it then notifies any awaiting operation state that the results are ready.

        4. get_env(r) is an expression e of type ensure-started-env such that execution::get_stop_token(e) is well-formed and returns the results of calling get_token() on sh_state's stop source.

        5. r shares ownership of sh_state with s2. After r's receiver contract has been completed, it releases its ownership of sh_state.

      3. Calls execution::connect(s, r), resulting in an operation state op_state2. op_state2 is saved in sh_state. It then calls execution::start(op_state2).

      4. When s2 is connected with a receiver out_r of type OutR, it returns an operation state object op_state that contains:

        • An object out_r' of type OutR decay-copied from out_r,

        • A reference to sh_state,

        • A stop callback of type optional<stop_token_of_t<env_of_t<OutR>>::callback_type<stop-callback-fn>>, where stop-callback-fn is an implementation defined class type equivalent to the following:

          struct stop-callback-fn {
            stop_source& stop_src_;
            void operator()() noexcept {
              stop_src_.request_stop();
            }
          };
          

        s2 transfers its ownership of sh_state to op_state.

      5. When execution::start(op_state) is called:

        • If r's receiver contract has already been satisfied, then let Signal be whichever receiver completion-signal was used to complete r's receiver contract ([exec.recv]). Calls Signal(out_r', args2...), where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r, args...) and returns.

        • Otherwise, it emplace constructs the stop callback optional with the arguments execution::get_stop_token(get_env(out_r')) and stop-callback-fn{stop-src}, where stop-src refers to the stop source of sh_state.

        • Then, it checks to see if stop-src.stop_requested() is true. If so, it calls execution::set_stopped(out_r').

        • Otherwise, it sets sh_state operation state pointer to the address of op_state, registering itself as awaiting the result of the completion of r.

      6. When r completes it will notify op_state that the result are ready. Let Signal be whichever receiver completion-signal was used to complete r's receiver contract ([exec.recv]). op_state's stop callback optional is reset. Then Signal(std::move(out_r'), args2...) is called, where args2... is a pack of xvalues referencing the subobjects of sh_state that have been saved by the original call to Signal(r, args...).

      7. [Note: If sender s2 is destroyed without being connected to a receiver, or if it is connected but the operation state is destroyed without having been started, then when r's receiver contract completes and it releases its shared ownership of sh_state, sh_state will be destroyed and the results of the operation are discarded. -- end note]

    4. Given an expression e, let E be decltype((e)). If sender<S, E> is false, the type of tag_invoke(get_sender_traits, s2, e) shall be equivalent to dependent_completion_signatures<E>. Otherwise, let Vs... be the set of unique types in the type-list named by value_types_of_t<S, E, set-value-signature, type-list>, and let Es... be the set of types in the type-list named by error_types_of_t<S, E, error-types>, where set-value-signature is the alias template:

      template <class... Ts>
        using set-value-signature = set_value_t(decay_t<Ts>&&...);
      

      and error-types is the alias template:

      template <class... Es>
        using error-types = type-list<decay_t<Es>&&...>;
      

      Let Bs... be the set of unique types in [Es..., exception_ptr&&]. If sender_traits_t<S, E>::sends_stopped is true, then the type of tag_invoke(get_sender_traits, s2, e) is a class type equivalent to:

      completion_signatures<Vs..., set_error_t(Bs)..., set_stopped_t()>
      

      Otherwise:

      completion_signatures<Vs..., set_error_t(Bs)...>
      

    If the function selected above does not return a sender that sends xvalue references to values sent by s, propagating the other channels, the program is ill-formed with no diagnostic required.

10.8.6. Sender consumers [exec.consumers]

10.8.6.1. execution::start_detached [exec.start_detached]
  1. execution::start_detached is used to eagerly start a sender without the caller needing to manage the lifetimes of any objects.

  2. The name execution::start_detached denotes a customization point object. For some subexpression s, let S be decltype((s)). If S does not satisfy execution::sender, execution::start_detached is ill-formed. Otherwise, the expression execution::start_detached(s) is expression-equivalent to:

    1. tag_invoke(execution::start_detached, execution::get_completion_scheduler<execution::set_value_t>(s), s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above is void.

    2. Otherwise, tag_invoke(execution::start_detached, s), if that expression is valid.

      • Mandates: The type of the tag_invoke expression above is void.

    3. Otherwise:

      1. Constructs a receiver r:

        1. When set_value(r, ts...) is called, it does nothing.

        2. When set_error(r, e) is called, it calls std::terminate.

        3. When set_stopped(r) is called, it does nothing.

      2. Calls execution::connect(s, r), resulting in an operation state op_state, then calls execution::start(op_state). The lifetime of op_state lasts until one of the receiver completion-signals of r is called.

    If the function selected above does not eagerly start the sender s after connecting it with a receiver which ignores the set_value and set_stopped signals and calls std::terminate on the set_error signal, the program is ill-formed with no diagnostic required.

10.8.6.2. this_thread::sync_wait [exec.sync_wait]
  1. this_thread::sync_wait and this_thread::sync_wait_with_variant are used to block a current thread until a sender passed into it as an argument has completed, and to obtain the values (if any) it completed with.

  2. For any receiver r created by an implementation of sync_wait and sync_wait_with_variant, the expressions get_scheduler(get_env(r)) and get_delegatee_scheduler(get_env(r)) shall be well-formed. For a receiver created by the default implementation of this_thread::sync_wait, these expressions shall return a scheduler to the same thread-safe, first-in-first-out queue of work such that tasks scheduled to the queue execute on the thread of the caller of sync_wait. [Note: The scheduler for an instance of execution::run_loop that is a local variable within sync_wait is one valid implementation. -- end note]

  3. The templates sync-wait-type and sync-wait-with-variant-type are used to determine the return types of this_thread::sync_wait and this_thread::sync_wait_with_variant. Let sync-wait-env be the type of the expression get_env(r) where r is an instance of the receiver created by the default implementation of sync_wait.

    template<sender<sync-wait-env> S>
      using sync-wait-type =
        optional<execution::value_types_of_t<S, sync-wait-env, decayed-tuple, type_identity_t>>;
    
    template<sender<sync-wait-env> S>
      using sync-wait-with-variant-type = optional<execution::into-variant-type<S, sync-wait-env>>;
    
  4. The name this_thread::sync_wait denotes a customization point object. For some subexpression s, let S be decltype((s)). If execution::sender<S, sync-wait-env> is false, or the number of the arguments completion_signatures_of_t<S, sync-wait-env>::value_types passed into the Variant template parameter is not 1, this_thread::sync_wait is ill-formed. Otherwise, this_thread::sync_wait is expression-equivalent to:

    1. tag_invoke(this_thread::sync_wait, execution::get_completion_scheduler<execution::set_value_t>(s), s), if this expression is valid.

      • Mandates: The type of the tag_invoke expression above is sync-wait-type<S, sync-wait-env>.

    2. Otherwise, tag_invoke(this_thread::sync_wait, s), if this expression is valid and its type is.

      • Mandates: The type of the tag_invoke expression above is sync-wait-type<S, sync-wait-env>.

    3. Otherwise:

      1. Constructs a receiver r.

      2. Calls execution::connect(s, r), resulting in an operation state op_state, then calls execution::start(op_state).

      3. Blocks the current thread until a receiver completion-signal of r is called. When it is:

        1. If execution::set_value(r, ts...) has been called, returns sync-wait-type<S, sync-wait-env>{decayed-tuple<decltype(ts)...>{ts...}}.

        2. If execution::set_error(r, e) has been called, let E be the decayed type of e. If E is exception_ptr, calls std::rethrow_exception(e). Otherwise, if the E is error_code, throws system_error(e). Otherwise, throws e.

        3. If execution::set_stopped(r) has been called, returns sync-wait-type<S, sync-wait-env>{}.

  5. The name this_thread::sync_wait_with_variant denotes a customization point object. For some subexpression s, let S be the type of execution::into_variant(s). If execution::sender<S, sync-wait-env> is false, this_thread::sync_wait_with_variant is ill-formed. Otherwise, this_thread::sync_wait_with_variant is expression-equivalent to:

    1. tag_invoke(this_thread::sync_wait_with_variant, execution::get_completion_scheduler<execution::set_value_t>(s), s), if this expression is valid.

      • Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S, sync-wait-env>.

    2. Otherwise, tag_invoke(this_thread::sync_wait_with_variant, s), if this expression is valid.

      • Mandates: The type of the tag_invoke expression above is sync-wait-with-variant-type<S, sync-wait-env>.

    3. Otherwise, this_thread::sync_wait(execution::into_variant(s)).

10.9. execution::execute [exec.execute]

  1. execution::execute is used to create fire-and-forget tasks on a specified scheduler.

  2. The name execution::execute denotes a customization point object. For some subexpressions sch and f, let Sch be decltype((sch)) and F be decltype((f)). If Sch does not satisfy execution::scheduler or F does not satisfy invocable, execution::execute is ill-formed. Otherwise, execution::execute is expression-equivalent to:

    1. tag_invoke(execution::execute, sch, f), if that expression is valid. If the function selected by tag_invoke does not invoke the function f (or an object decay-copied from f) on an execution agent belonging to the associated execution context of sch, or if it does not call std::terminate if an error occurs after control is returned to the caller, the program is ill-formed with no diagnostic required.

      • Mandates: The type of the tag_invoke expression above is void.

    2. Otherwise, execution::start_detached(execution::then(execution::schedule(sch), f)).

10.10. Sender/receiver utilities [exec.utils]

  1. This section makes use of the following exposition-only entities:

    // [Editorial note: copy_cvref_t as in [[P1450R3]] -- end note]
    // Mandates: is_base_of_v<T, remove_reference_t<U>> is true
    template <class T, class U>
      copy_cvref_t<U&&, T> c-style-cast(U&& u) noexcept requires decays-to<T, T> {
        return (copy_cvref_t<U&&, T>) std::forward<U>(u);
      }
    
  2. [Note: The C-style cast in c-style-cast is to disable accessibility checks. -- end note]

10.10.1. execution::receiver_adaptor [exec.utils.rcvr_adptr]

template<
    class-type Derived,
    receiver Base = unspecified> // arguments are not associated entities ([lib.tmpl-heads])
  class receiver_adaptor;
  1. receiver_adaptor is used to simplify the implementation of one receiver type in terms of another. It defines tag_invoke overloads that forward to named members if they exist, and to the adapted receiver otherwise.

  2. This section makes use of the following exposition-only entities:

    template <class Receiver, class... As>
      concept has-set-value =
        requires(Receiver&& r, As&&... as) {
          static_cast<Receiver&&>(r).set_value(static_cast<As&&>(as)...);
        };
    
    template <class Receiver, class A>
      concept has-set-error =
        requires(Receiver&& r, A&& a) {
          static_cast<Receiver&&>(r).set_error(static_cast<A&&>(a));
        };
    
    template <class Receiver>
      concept has-set-stopped =
        requires(Receiver&& r) {
          static_cast<Receiver&&>(r).set_stopped();
        };
    
  3. If Base is an alias for the unspecified default template argument, then:

    • Let HAS-BASE be false, and

    • Let GET-BASE(d) be d.base().

    otherwise, let:

    • Let HAS-BASE be true, and

    • Let GET-BASE(d) be c-style-cast<receiver_adaptor<Derived, Base>>(d).base().

    Let BASE-TYPE(D) be the type of GET-BASE(declval<D>()).

  4. receiver_adaptor<Derived, Base> is equivalent to the following:

    template <
      class-type Derived,
      receiver Base = unspecified> // arguments are not associated entities ([lib.tmpl-heads])
    class receiver_adaptor {
      friend Derived;
     public:
      // Constructors
      receiver_adaptor() = default;
      template <class B>
          requires HAS-BASE && constructible_from<Base, B>
        explicit receiver_adaptor(B&& base) : base_(std::forward<B>(base)) {}
    
     private:
      using set_value = unspecified;
      using set_error = unspecified;
      using set_stopped = unspecified;
    
      // Member functions
      template <class Self>
        requires HAS-BASE
      copy_cvref_t<Self, Base>&& base(this Self&& self) noexcept {
        return static_cast<Self&&>(self).base_;
      }
    
      // [exec.utils.rcvr_adptr.nonmembers] Non-member functions
      template <class D = Derived, class... As>
        friend void tag_invoke(set_value_t, Derived&& self, As&&... as) noexcept(see below);
    
      template <class E, class D = Derived>
        friend void tag_invoke(set_error_t, Derived&& self, E&& e) noexcept;
    
      template <class D = Derived>
        friend void tag_invoke(set_stopped_t, Derived&& self) noexcept;
    
      template <forwarding-receiver-query Tag, class D = Derived, class... As>
          requires callable<Tag, BASE-TYPE(const D&), As...>
        friend auto tag_invoke(Tag tag, const Derived& self, As&&... as)
          noexcept(nothrow-callable<Tag, BASE-TYPE(const D&), As...>)
          -> call-result-t<Tag, BASE-TYPE(const D&), As...> {
          return std::move(tag)(GET-BASE(self), std::forward<As>(as)...);
        }
    
      [[no_unique_address]] Base base_; // present if and only if HAS-BASE is true
    };
    
  5. [Note: receiver_adaptor provides tag_invoke overloads on behalf of the derived class Derived, which is incomplete when receiver_adaptor is instantiated.]

  6. [Example:

    template <execution::receiver_of<int> R>
      class my_receiver : execution::receiver_adaptor<my_receiver<R>, R> {
        friend execution::receiver_adaptor<my_receiver, R>;
        void set_value() && {
          execution::set_value(std::move(*this).base(), 42);
        }
       public:
        using execution::receiver_adaptor<my_receiver, R>::receiver_adaptor;
      };
    

    -- end example]

10.10.1.1. Non-member functions [exec.utils.rcvr_adptr.nonmembers]
template <class D = Derived, class... As>
  friend void tag_invoke(set_value_t, Derived&& self, As&&... as) noexcept(see below);
  1. Constraints: Either has-set-value<D, As...> is true or requires {typename D::set_value;} && receiver_of<BASE-TYPE(D), As...> is true.

  2. If has-set-value<D, As...> is true:

    1. Effects: Equivalent to static_cast<Derived&&>(self).set_value(static_cast<As&&>(as)...)

    2. Remarks: The exception specification is noexcept(static_cast<Derived&&>(self).set_value(static_cast<As&&>(as)...)).

  3. Otherwise:

    1. Effects: Equivalent to execution::set_value(GET-BASE(static_cast<Derived&&>(self)), static_cast<As&&>(as)...)

    2. Remarks: The exception specification is noexcept(set_value(GET-BASE(static_cast<Derived&&>(self)), static_cast<As&&>(as)...))

template <class E, class D = Derived>
  friend void tag_invoke(set_error_t, Derived&& self, E&& e) noexcept;
  1. Constraints: Either has-set-error<D, E> is true or requires {typename D::set_error;} && receiver<BASE-TYPE(D), E> is true.

  2. Effects: Equivalent to:

    • static_cast<Derived&&>(self).set_error(static_cast<E&&>(e)) if has-set-error<D, E> is true,

      • Mandates: The member function call expression above is not potentially throwing.

    • Otherwise, execution::set_error(GET-BASE(static_cast<Derived&&>(self)), static_cast<E&&>(e))

template <class D = Derived>
  friend void tag_invoke(set_stopped_t, Derived&& self) noexcept;
  1. Constraints: Either has-set-stopped<D> is true or requires {typename D::set_stopped;} is true.

  2. Effects: Equivalent to:

    • static_cast<Derived&&>(self).set_stopped() if has-set-stopped<D> is true,

      • Mandates: The member function call expression above is not potentially throwing.

    • Otherwise, execution::set_stopped(GET-BASE(static_cast<Derived&&>(self)))

10.10.2. execution::completion_signatures [exec.utils.cmplsigs]

  1. completion_signatures is used to define a type that implements the nested value_types, error_types, and sends_stopped members that describe the ways a sender completes. Its arguments are a flat list of function types that describe the signatures of the receiver’s completion-signal operations that the sender invokes.

  2. [Example:

    class my_sender {
      using completion_signatures =
        execution::completion_signatures<
          execution::set_value_t(),
          execution::set_value_t(int, float),
          execution::set_error_t(exception_ptr),
          execution::set_error_t(error_code),
          execution::set_stopped_t()>;
    };
    
    // completion_signatures_of_t<my_sender>
    //      ::value_types<tuple, variant> names the type:
    //   variant<tuple<>, tuple<int, float>>
    //
    // completion_signatures_of_t<my_sender>
    //      ::error_types<variant> names the type:
    //   variant<exception_ptr, error_code>
    //
    // completion_signatures_of_t<my_sender>::sends_stopped is true
    

    -- end example]

  3. This section makes use of the following exposition-only concept:

    template <class Fn>
      concept completion-signature = see below;
    
    1. A type Fn satisfies completion-signature if it is a function type with one of the following forms:

      • set_value_t(Vs...), where Vs is an arbitrary parameter pack.

      • set_error_t(E), where E is an arbitrary type.

      • set_stopped_t()

    2. Otherwise, Fn does not satisfy completion-signature.

  4. template <completion-signature... Fns> // arguments are not associated entities ([lib.tmpl-heads])
      struct completion_signatures {
        template <template <class...> class Tuple, template <class...> class Variant>
          using value_types = see below;
    
        template <template <class...> class Variant>
          using error_types = see below;
    
        static constexpr bool sends_stopped = see below;
      };
    
    1. Let ValueFns be a template parameter pack of the function types in Fns whose return types are execution::set_value_t, and let Valuesn be a template parameter pack of the function argument types in the n-th type in ValueFns. Then, given two variadic templates Tuple and Variant, the type completion_signatures<Fns...>::value_types<Tuple, Variant> names the type Variant<Tuple<Values0...>, Tuple<Values1...>, ... Tuple<Valuesm-1...>>, where m is the size of the parameter pack ValueFns.

    2. Let ErrorFns be a template parameter pack of the function types in Fns whose return types are execution::set_error_t, and let Errorn be the function argument type in the n-th type in ErrorFns. Then, given a variadic template Variant, the type completion_signatures<Fns...>::error_types<Variant> names the type Variant<Error0, Error1, ... Errorm-1>, where m is the size of the parameter pack ErrorFns.

    3. completion_signatures<Fns...>::sends_stopped is true if at least one of the types in Fns is execution::set_stopped_t(); otherwise, false.

10.10.3. execution::make_completion_signatures [exec.utils.mkcmplsigs]

  1. make_completion_signatures is an alias template used to adapt the completion signatures of a sender. It takes a sender, and environment, and several other template arguments that apply modifications to the sender’s completion signatures to generate a new instantiation of execution::completion_signatures.

  2. [Example:

    // Given a sender S and an environment Env, adapt a S’s completion
    // signatures by lvalue-ref qualifying the values, adding an additional
    // exception_ptr error completion if its not already there, and leaving the
    // other signals alone.
    template <class... Args>
      using my_set_value_t = execution::set_value_t(add_lvalue_reference_t<Args>...);
    
    using my_completion_signals =
      execution::make_completion_signatures<
        S, Env,
        execution::completion_signatures<execution::set_error_t(exception_ptr)>,
        my_set_value_t>;
    

    -- end example]

  3. This section makes use of the following exposition-only entities:

    template <class... As>
      using default-set-value = execution::set_value_t(As...);
    
    template <class Err>
      using default-set-error = execution::set_error_t(Err);
    
  4. template <
      execution::sender Sndr,
      class Env = execution::no_env,
      class AddlSigs = execution::completion_signatures<>,
      template <class...> class SetValue = default-set-value,
      template <class> class SetError = default-set-error,
      bool SendsStopped = execution::completion_signatures_of_t<Sndr, Env>::sends_stopped>
        requires sender<Sndr, Env>
    using make_completion_signatures = execution::completion_signatures</* see below */>;
    
    1. AddlSigs shall name an instantiation of the execution::completion_signatures class template.

    2. SetValue shall name an alias template such that for any template parameter pack As..., SetValue<As...> is either ill-formed, void or an alias for a function type whose return type is execution::set_value_t.

    3. SetError shall name an alias template such that for any type Err, SetError<Err> is either ill-formed, void or an alias for a function type whose return type is execution::set_error_t.

    4. Let Vs... be a pack of the non-void types in the type-list named by value_types_of_t<Sndr, Env, SetValue, type-list>.

    5. Let Es... be a pack of the non-void types in the type-list named by error_types_of_t<Sndr, Env, error-list>, where error-list is an alias template such that error-list<Ts...> names type-list<SetError<Ts>...>.

    6. Let Ss... be an empty pack if SendsStopped is false; otherwise, a pack containing the single type execution::set_stopped_t().

    7. Let MoreSigs... be a pack of the template arguments of the execution::completion_signatures instantiation named by AddlSigs.

    8. If any of the above types are ill-formed, then make_completion_signatures<Sndr, Env, AddlSigs, SetValue, SetDone, SendsStopped> is an alias for dependent_completion_signatures<Env>.

    9. Otherwise, make_completion_signatures<Sndr, Env, AddlSigs, SetValue, SetDone, SendsStopped> names the type completion_signatures<Sigs...> where Sigs... is the unique set of types in [Vs..., Es..., Ss..., MoreSigs...].

10.11. Execution contexts [exec.ctx]

  1. This section specifies some execution contexts on which work can be scheduled.

10.11.1. run_loop [exec.run_loop]

  1. A run_loop is an execution context on which work can be scheduled. It maintains a simple, thread-safe first-in-first-out queue of work. Its run() member function removes elements from the queue and executes them in a loop on whatever thread of execution calls run().

  2. A run_loop instance has an associated count that corresponds to the number of work items that are in its queue. Additionally, a run_loop has an associated state that can be one of starting, running, or finishing.

  3. Concurrent invocations of the member functions of run_loop, other than run and its destructor, do not introduce data races. The member functions pop_front, push_back, and finish execute atomically.

  4. [Note: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. — end note]

    class run_loop {
      // [exec.run_loop.types] Associated types
      class run-loop-scheduler; // exposition only
      class run-loop-sender; // exposition only
      struct run-loop-opstate-base { // exposition only
        virtual void execute() = 0;
        run_loop* loop_;
        run-loop-opstate-base* next_;
      };
      template<receiver_of R>
        using run-loop-opstate = unspecified; // exposition only
    
      // [exec.run_loop.members] Member functions:
      run-loop-opstate-base* pop_front(); // exposition only
      void push_back(run-loop-opstate-base*); // exposition only
    
     public:
      // [exec.run_loop.ctor] construct/copy/destroy
      run_loop() noexcept;
      run_loop(run_loop&&) = delete;
      ~run_loop();
    
      // [exec.run_loop.members] Member functions:
      run-loop-scheduler get_scheduler();
      void run();
      void finish();
    };
    
10.11.1.1. Associated types [exec.run_loop.types]
class run-loop-scheduler;
  1. run-loop-scheduler is an implementation defined type that models the scheduler concept.

  2. Instances of run-loop-scheduler remain valid until the end of the lifetime of the run_loop instance from which they were obtained.

  3. Two instances of run-loop-scheduler compare equal if and only if they were obtained from the same run_loop instance.

  4. Let sch be an expression of type run-loop-scheduler. The expression execution::schedule(sch) is not potentially throwing and has type run-loop-sender.

class run-loop-sender;
  1. run-loop-sender is an implementation defined type that models the sender_of concept; i.e., sender_of<run-loop-sender> is true. Additionally, the types reported by its error_types associated type is exception_ptr, and the value of its sends_stopped trait is true.

  2. An instance of run-loop-sender remains valid until the end of the lifetime of its associated execution::run_loop instance.

  3. Let s be an expression of type run-loop-sender, let r be an expression such that decltype(r) models the receiver_of concept, and let C be either set_value_t or set_stopped_t. Then:

    • The expression execution::connect(s, r) has type run-loop-opstate<decay_t<decltype(r)>> and is potentially throwing if and only if the initialiation of decay_t<decltype(r)> from r is potentially throwing.

    • The expression get_completion_scheduler<C>(s) is not potentially throwing, has type run-loop-scheduler, and compares equal to the run-loop-scheduler instance from which s was obtained.

template<receiver_of R> // arguments are not associated entities ([lib.tmpl-heads])
  struct run-loop-opstate;
  1. run-loop-opstate<R> inherits unambiguously from run-loop-opstate-base.

  2. Let o be a non-const lvalue of type run-loop-opstate<R>, and let REC(o) be a non-const lvalue reference to an instance of type R that was initialized with the expression r passed to the invocation of execution::connect that returned o. Then:

    • The object to which REC(o) refers remains valid for the lifetime of the object to which o refers.

    • The type run-loop-opstate<R> overrides run-loop-opstate-base::execute() such that o.execute() is equivalent to the following:

      try {
        if (execution::get_stop_token(REC(o)).stop_requested()) {
          execution::set_stopped(std::move(REC(o)));
        } else {
          execution::set_value(std::move(REC(o)));
        }
      } catch(...) {
        execution::set_error(std::move(REC(o)), current_exception());
      }
      
    • The expression execution::start(o) is equivalent to the following:

      try {
        o.loop_->push_back(&o);
      } catch(...) {
        execution::set_error(std::move(REC(o)), current_exception());
      }
      
10.11.1.2. Constructor and destructor [exec.run_loop.ctor]
run_loop::run_loop() noexcept;
  1. Postconditions: count is 0 and state is starting.

run_loop::~run_loop();
  1. Effects: If count is not 0 or if state is running, invokes terminate(). Otherwise, has no effects.

10.11.1.3. Member functions [exec.run_loop.members]
run-loop-opstate-base* run_loop::pop_front();
  1. Effects: Blocks ([defns.block]) until one of the following conditions is true:

    • count is 0 and state is finishing, in which case pop_front returns nullptr; or

    • count is greater than 0, in which case an item is removed from the front of the queue, count is decremented by 1, and the removed item is returned.

void run_loop::push_back(run-loop-opstate-base* item);
  1. Effects: Adds item to the back of the queue and increments count by 1.

  2. Synchronization: This operation synchronizes with the pop_front operation that obtains item.

run-loop-scheduler run_loop::get_scheduler();
  1. Returns: an instance of run-loop-scheduler that can be used to schedule work onto this run_loop instance.

void run_loop::run();
  1. Effects: Equivalent to:

    while (auto* op = pop_front()) {
      op->execute();
    }
    
  2. Precondition: state is starting.

  3. Postcondition: state is finishing.

  4. Remarks: While the loop is executing, state is running. When state changes, it does so without introducing data races.

void run_loop::finish();
  1. Effects: Changes state to finishing.

  2. Synchronization: This operation synchronizes with all pop_front operations on this object.

10.12. Coroutine utilities [exec.coro_utils]

10.12.1. execution::as_awaitable [exec.as_awaitable]

  1. as_awaitable is used to transform an object into one that is awaitable within a particular coroutine. This section makes use of the following exposition-only entities:

    template<class S, class E>
      using single-sender-value-type = see below;
    
    template<class S, class E>
      concept single-sender =
        sender<S, E> &&
        requires { typename single-sender-value-type<S, E>; };
    
    template <class S, class P>
      concept awaitable-sender =
        single-sender<S, env_of_t<P>> &&
        sender_to<S, awaitable-receiver> && // see below
        requires (P& p) {
          { p.unhandled_stopped() } -> convertible_to<coroutine_handle<>>;
        };
    
    template <class S, class P>
      class sender-awaitable;
    
    1. Alias template single-sender-value-type is defined as follows:

      1. If value_types_of_t<S, E, Tuple, Variant> would have the form Variant<Tuple<T>>, then single-sender-value-type<S, E> is an alias for type T.

      2. Otherwise, if value_types_of_t<S, E, Tuple, Variant> would have the form Variant<Tuple<>> or Variant<>, then single-sender-value-type<S, E> is an alias for type void.

      3. Otherwise, single-sender-value-type<S, E> is ill-formed.

    2. The type sender-awaitable<S, P> is equivalent to the following:

      template <class S, class P> // arguments are not associated entities ([lib.tmpl-heads])
      class sender-awaitable {
        struct unit {};
        using value_t = single-sender-value-type<S, env_of_t<P>>;
        using result_t = conditional_t<is_void_v<value_t>, unit, value_t>;
        struct awaitable-receiver;
      
        variant<monostate, result_t, exception_ptr> result_{};
        connect_result_t<S, awaitable-receiver> state_;
      
       public:
        sender-awaitable(S&& s, P& p);
        bool await_ready() const noexcept { return false; }
        void await_suspend(coroutine_handle<P>) noexcept { start(state_); }
        value_t await_resume();
      };
      
      1. awaitable-receiver is equivalent to the following:

        struct awaitable-receiver {
          variant<monostate, result_t, exception_ptr>* result_ptr_;
          coroutine_handle<P> continuation_;
          // ... see below
        };
        

        Let r be an rvalue expression of type awaitable-receiver, let cr be a const lvalue that refers to r, let v be an expression of type result_t, and let err be an arbitrary expression of type Err. Then:

        1. If value_t is void, then execution::set_value(r) is expression-equivalent to (r.result_ptr_->emplace<1>(), r.continuation_.resume()); otherwise, execution::set_value(r, v) is expression-equivalent to (r.result_ptr_->emplace<1>(v), r.continuation_.resume()).

        2. execution::set_error(r, e) is expression-equivalent to (r.result_ptr_->emplace<2>(AS_EXCEPT_PTR(err)), r.continuation_.resume()), where AS_EXCEPT_PTR(err) is:

          1. err if decay_t<Err> names the same type as exception_ptr,

          2. Otherwise, make_exception_ptr(system_error(err)) if decay_t<Err> names the same type as error_code,

          3. Otherwise, make_exception_ptr(err).

        3. execution::set_stopped(r) is expression-equivalent to static_cast<coroutine_handle<>>(r.continuation_.promise().unhandled_stopped()).resume().

        4. tag_invoke(tag, cr, as...) is expression-equivalent to tag(as_const(cr.continuation_.promise()), as...) for any expression tag whose type satisfies forwarding-receiver-query and for any set of arguments as....

      2. sender-awaitable::sender-awaitable(S&& s, P& p)

        • Effects: initializes state_ with connect((S&&) s, awaitable-receiver{&result_, coroutine_handle<P>::from_promise(p)}).

      3. value_t sender-awaitable::await_resume()

        • Effects: equivalent to:

          if (result_.index()) == 2)
            rethrow_exception(get<2>(result_));
          if constexpr (!is_void_v<value_t>)
            return static_cast<value_t&&>(get<1>(result_));
          
  2. as_awaitable is a customization point object. For some subexpressions e and p where p is an lvalue, E names the type decltype((e)) and P names the type decltype((p)), as_awaitable(e, p) is expression-equivalent to the following:

    1. tag_invoke(as_awaitable, e, p) if that expression is well-formed.

      • Mandates: is-awaitable<A> is true, where A is the type of the tag_invoke expression above.

    2. Otherwise, e if is-awaitable<E> is true.

    3. Otherwise, sender-awaitable{e, p} if awaitable-sender<E, P> is true.

    4. Otherwise, e.

10.12.2. execution::with_awaitable_senders [exec.with_awaitable_senders]

  1. with_awaitable_senders, when used as the base class of a coroutine promise type, makes senders awaitable in that coroutine type.

    In addition, it provides a default implementation of unhandled_stopped() such that if a sender completes by calling execution::set_stopped, it is treated as if an uncatchable "stopped" exception were thrown from the await-expression. In practice, the coroutine is never resumed, and the unhandled_stopped of the coroutine caller’s promise type is called.

    template <class-type Promise>
      struct with_awaitable_senders {
        template<OtherPromise>
          requires (!same_as<OtherPromise, void>)
        void set_continuation(coroutine_handle<OtherPromise> h) noexcept;
    
        coroutine_handle<> continuation() const noexcept { return continuation_; }
    
        coroutine_handle<> unhandled_stopped() noexcept {
          return stopped_handler_(continuation_.address());
        }
    
        template<class Value>
        see-below await_transform(Value&& value);
    
       private:
        // exposition only
        [[noreturn]] static coroutine_handle<> default_unhandled_stopped(void*) noexcept {
          terminate();
        }
        coroutine_handle<> continuation_{}; // exposition only
        // exposition only
        coroutine_handle<> (*stopped_handler_)(void*) noexcept = &default_unhandled_stopped;
      };
    
  2. void set_continuation(coroutine_handle<OtherPromise> h) noexcept

    • Effects: equivalent to:

      continuation_ = h;
      if constexpr ( requires(OtherPromise& other) { other.unhandled_stopped(); } ) {
        stopped_handler_ = [](void* p) noexcept -> coroutine_handle<> {
          return coroutine_handle<OtherPromise>::from_address(p)
            .promise().unhandled_stopped();
        };
      } else {
        stopped_handler_ = default_unhandled_stopped;
      }
      
  3. call-result-t<as_awaitable_t, Value, Promise&> await_transform(Value&& value)

    • Effects: equivalent to:

      return as_awaitable(static_cast<Value&&>(value), static_cast<Promise&>(*this));
      

Index

Terms defined by this specification

References

Informative References

[HPX]
Hartmut Kaiser; et al. HPX - The C++ Standard Library for Parallelism and Concurrency. URL: https://doi.org/10.21105/joss.02352
[N4885]
Thomas Köppe. Working Draft, Standard for Programming Language C++. 17 March 2021. URL: https://wg21.link/n4885
[P0443R14]
Jared Hoberock, Michael Garland, Chris Kohlhoff, Chris Mysen, H. Carter Edwards, Gordon Brown, D. S. Hollman. A Unified Executors Proposal for C++. 15 September 2020. URL: https://wg21.link/p0443r14
[P0981R0]
Richard Smith, Gor Nishanov. Halo: coroutine Heap Allocation eLision Optimization: the joint response. 18 March 2018. URL: https://wg21.link/p0981r0
[P1056R1]
Lewis Baker, Gor Nishanov. Add lazy coroutine (coroutine task) type. 7 October 2018. URL: https://wg21.link/p1056r1
[P1895R0]
Lewis Baker, Eric Niebler, Kirk Shoop. tag_invoke: A general pattern for supporting customisable functions. 8 October 2019. URL: https://wg21.link/p1895r0
[P1897R3]
Lee Howes. Towards C++23 executors: A proposal for an initial set of algorithms. 16 May 2020. URL: https://wg21.link/p1897r3
[P2175R0]
Lewis Baker. Composable cancellation for sender-based async operations. 15 December 2020. URL: https://wg21.link/p2175r0