1. Introduction
This paper proposes a self-contained design for a Standard C++ framework for managing asynchronous execution on generic execution resources. It is based on the ideas in A Unified Executors Proposal for C++ 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
(
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
- 
     Be composable and generic, allowing users to write code that can be used with many different types of execution resources. 
- 
     Encapsulate common asynchronous patterns in customizable and reusable algorithms, so users don’t have to invent things themselves. 
- 
     Make it easy to be correct by construction. 
- 
     Support the diversity of execution resources and execution agents, because not all execution agents are created equal; some are less capable than others, but not less important. 
- 
     Allow everything to be customized by an execution resource, including transfer to other execution resources, but don’t require that execution resources customize everything. 
- 
     Care about all reasonable use cases, domains and platforms. 
- 
     Errors must be propagated, but error handling must not present a burden. 
- 
     Support cancellation, which is not an error. 
- 
     Have clear and concise answers for where things execute. 
- 
     Be able to manage and terminate the lifetimes of objects asynchronously. 
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.19 User-facing sender factories, § 4.20 User-facing sender adaptors, and § 4.21 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:
- 
     First we need to get a scheduler from somewhere, such as a thread pool. A scheduler is a lightweight handle to an execution resource. 
- 
     To start a chain of work on a scheduler, we call § 4.19.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. 
- 
     We use sender algorithms to produce senders and compose asynchronous work. § 4.20.2 execution::then is a sender adaptor that takes an input sender and a std :: invocable std :: invocable then schedule void std :: invocable int 
- 
     Now, we add another operation to the chain, again using § 4.20.2 execution::then. This time, we get sent a value - the int 42 
- 
     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.21.1 this_thread::sync_wait, which will either return a std :: optional < std :: tuple < ... >> std :: optional 
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 just ( std :: move ( partials )) // 5 | continues_on ( sch ) | 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:
- 
     It scans a sequence of double std :: span < const double > input double std :: span < double > output 
- 
     It takes a scheduler, which specifies what execution resource the scan should be launched on. 
- 
     It also takes a tile_count 
- 
     First we need to allocate temporary storage needed for the algorithm, which we’ll do with a std :: vector partials double 
- 
     Next we’ll create our initial sender with § 4.19.2 execution::just and § 4.20.1 execution::continues_on. These senders will send the temporary storage, which we’ve moved into the sender. The sender has a completion scheduler of sch sch 
- 
     Senders and sender adaptors support composition via operator | operator | tile_count 
- 
     Each agent will call a std :: invocable i [ 0 , tile_count ) 
- 
     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. 
- 
     Then we do a sequential std :: inclusive_scan partials 
- 
     After all computation in that initial § 4.20.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 
- 
     Now we need to scan all of the values in partials 
- 
     § 4.20.2 execution::then takes an input sender and an std :: invocable std :: invocable std :: invocable std :: inclusive_scan partials 
- 
     Then we return partials 
- 
     Finally we do another § 4.20.9 execution::bulk of the same shape as before. In this § 4.20.9 execution::bulk, we will use the scanned values in partials 
- 
     async_inclusive_scan std :: span < double > async_inclusive_scan 
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_writable_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:
- 
     async_read std :: span < std :: byte > std :: span 
- 
     async_read_array dynamic_buffer 
- 
     dynamic_buffer std :: unique_ptr < std :: byte [] > 
- 
     The first thing we do inside of async_read_array dynamic_array operator | 
- 
     We need the lifetime of this dynamic_array let_value std :: invocable let_value std :: invocable std :: invocable 
- 
     Inside of the let_value std :: invocable async_read std :: span buf . size 
- 
     We chain the async_read operator | 
- 
     Next, we pipe a std :: invocable async_read 
- 
     That std :: invocable 
- 
     We need to check that the number of bytes read is what we expected. 
- 
     Now that we have read the size of the data, we can allocate storage for it. 
- 
     We return a std :: span < std :: byte > std :: invocable 
- 
     And that recipient will be another async_read 
- 
     Once the data has been read, in another § 4.20.2 execution::then, we confirm that we read the right number of bytes. 
- 
     Finally, we move out of and return our dynamic_buffer async_read_array 
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 
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 < class Receiver > struct recv_op : operation_base { using operation_state_concept = std :: execution :: operation_state_t ; 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 ); } void start () & noexcept { // Avoid even calling WSARecv() if operation already cancelled auto st = std :: execution :: get_stop_token ( std :: execution :: get_env ( receiver )); if ( st . stop_requested ()) { std :: execution :: set_stopped ( std :: move ( receiver )); return ; } // Store and cache result here in case it changes during execution const bool stopPossible = st . stop_possible (); if ( ! stopPossible ) { ready . store ( true, std :: memory_order_relaxed ); } // Launch the operation DWORD bytesTransferred = 0 ; DWORD flags = 0 ; int result = WSARecv ( sock , & buffer , 1 , & bytesTransferred , & flags , static_cast < WSAOVERLAPPED *> ( this ), NULL); if ( result == SOCKET_ERROR ) { int errorCode = WSAGetLastError (); if ( errorCode != WSA_IO_PENDING ) { if ( errorCode == WSA_OPERATION_ABORTED ) { std :: execution :: set_stopped ( std :: move ( receiver )); } else { std :: execution :: set_error ( std :: move ( 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 ( 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 stopCallback . emplace ( std :: move ( st ), cancel_cb { * this }); // Mark as 'completed' if ( ready . load ( std :: memory_order_acquire ) || ready . exchange ( true, std :: memory_order_acq_rel )) { // Already completed on another thread stopCallback . reset (); BOOL ok = WSAGetOverlappedResult ( sock , ( WSAOVERLAPPED * ) this , & bytesTransferred , FALSE , & flags ); if ( ok ) { std :: execution :: set_value ( std :: move ( receiver ), bytesTransferred ); } else { int errorCode = WSAGetLastError (); std :: execution :: set_error ( std :: move ( 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 ( self . ready . load ( std :: memory_order_acquire ) || self . ready . exchange ( true, std :: memory_order_acq_rel )) { // Unsubscribe any stop callback so we know that CancelIoEx() is not accessing 'op' // any more self . stopCallback . reset (); if ( errorCode == 0 ) { std :: execution :: set_value ( std :: move ( self . receiver ), bytesTransferred ); } else { std :: execution :: set_error ( std :: move ( self . receiver ), std :: error_code ( errorCode , std :: system_category ())); } } } using stop_callback_t = stop_callback_of_t < stop_token_of_t < env_of_t < Receiver >> , cancel_cb > ; Receiver receiver ; SOCKET sock ; WSABUF buffer ; std :: optional < stop_callback_t > stopCallback ; std :: atomic < bool > ready { false}; }; struct recv_sender { using sender_concept = std :: execution :: sender_t ; SOCKET sock ; void * data ; size_t len ; template < class Receiver > recv_op < Receiver > connect ( Receiver r ) const { return recv_op < Receiver > { sock , data , 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:
- 
     Although it schedules asynchronous 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 
- 
     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 proposed networking APIs that use the sender/receiver abstraction (see P2762). He has 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, 
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 
1.5. Examples: Algorithms
In this section we show a few simple sender/receiver-based algorithm implementations.
1.5.1. then 
namespace stdexec = std :: execution ; template < class R , class F > class _then_receiver : public R { F f_ ; public : _then_receiver ( R r , F f ) : R ( std :: move ( r )), f_ ( std :: move ( f )) {} // Customize set_value by invoking the callable and passing the result to // the inner receiver template < class ... As > requires std :: invocable < F , As ... > void set_value ( As && ... as ) && noexcept { try { stdexec :: set_value ( std :: move ( * this ). base (), std :: invoke (( F && ) f_ , ( As && ) as ...)); } catch (...) { stdexec :: set_error ( std :: move ( * this ). base (), std :: current_exception ()); } } }; template < stdexec :: sender S , class F > struct _then_sender { using sender_concept = stdexec :: sender_t ; S s_ ; F f_ ; template < class ... Args > using _set_value_t = stdexec :: completion_signatures < stdexec :: set_value_t ( std :: invoke_result_t < F , Args ... > ) > ; using _except_ptr_sig = stdexec :: completion_signatures < stdexec :: set_error_t ( std :: exception_ptr ) > ; // Compute the completion signatures template < class Env > auto get_completion_signatures ( Env && env ) && noexcept -> stdexec :: transform_completion_signatures_of < S , Env , _except_ptr_sig , _set_value_t > { return {}; } // Connect: template < stdexec :: receiver R > auto connect ( R r ) && -> stdexec :: connect_result_t < S , _then_receiver < R , F >> { return stdexec :: connect ( ( S && ) s_ , _then_receiver {( R && ) r , ( F && ) f_ }); } decltype ( auto ) get_env () const noexcept { return get_env ( s_ ); } }; template < stdexec :: sender S , class F > stdexec :: sender auto then ( S s , F f ) { return _then_sender < S , F > {( S && ) s , ( F && ) f }; } 
This code builds a 
In detail, it does the following:
- 
     Defines a receiver in terms of receiver and an invocable that: - 
       Defines a constrained set_value 
- 
       Delegates set_error set_stopped 
 
- 
       
- 
     Defines a sender that aggregates another sender and the invocable, which defines a connect std :: execution :: connect get_completion_signatures 
1.5.2. retry 
using namespace std ; namespace stdexec = execution ; template < class From , class To > concept _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 > struct _conv { F f_ ; static_assert ( is_nothrow_move_constructible_v < F > ); explicit _conv ( F f ) noexcept : f_ (( F && ) f ) {} operator invoke_result_t < F > () && { return (( F && ) f_ )(); } }; template < class S , class R > struct _retry_op ; // pass through all customizations except set_error, which retries // the operation. template < class S , class R > struct _retry_receiver { _retry_op < S , R >* o_ ; void set_value ( auto && ... as ) && noexcept { stdexec :: set_value ( std :: move ( o_ -> r_ ), ( decltype ( as ) && ) as ...); } void set_error ( auto && ) && noexcept { o_ -> _retry (); // This causes the op to be retried } void set_stopped () && noexcept { stdexec :: set_stopped ( std :: move ( o_ -> r_ )); } decltype ( auto ) get_env () const noexcept { return get_env ( o_ -> r_ ); } }; // 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 _retry_op { using operation_state_concept = stdexec :: operation_state_t ; using _child_op_t = stdexec :: connect_result_t < S & , _retry_receiver < S , R >> ; S s_ ; R r_ ; optional < _child_op_t > o_ ; _op ( _op && ) = delete ; _op ( S s , R r ) : s_ ( std :: move ( s )), r_ ( std :: move ( r )), o_ { _connect ()} {} auto _connect () noexcept { return _conv {[ this ] { return stdexec :: connect ( s_ , _retry_receiver < S , R > { this }); }}; } void _retry () noexcept { try { o_ . emplace ( _connect ()); // potentially-throwing stdexec :: start ( * o_ ); } catch (...) { stdexec :: set_error ( std :: move ( r_ ), std :: current_exception ()); } } void start () & noexcept { stdexec :: start ( * o_ ); } }; // Helpers for computing the `then` sender's completion signatures: template < class ... Ts > using _value_t = stdexec :: completion_signatures < stdexec :: set_value_t ( Ts ...) > ; template < class > using _error_t = stdexec :: completion_signatures <> ; using _except_sig = stdexec :: completion_signatures < stdexec :: set_error_t ( std :: exception_ptr ) > ; template < class S > struct _retry_sender { using sender_concept = stdexec :: sender_t ; S s_ ; explicit _retry_sender ( S s ) : s_ ( std :: move ( s )) {} // Declare the signatures with which this sender can complete template < class Env > using _compl_sigs = stdexec :: transform_completion_signatures_of < S & , Env , _except_sig , _value_t , _error_t > ; template < class Env > auto get_completion_signatures ( Env && ) const noexcept -> _compl_sigs < Env > { return {}; } template < stdexec :: receiver R > requires stdexec :: sender_to < S & , _retry_receiver < S , R >> _retry_op < S , R > connect ( R r ) && { return { std :: move ( s_ ), std :: move ( r )}; } decltype ( auto ) get_env () const noexcept { return get_env ( s_ ); } }; template < stdexec :: sender S > stdexec :: sender auto retry ( S s ) { return _retry_sender { std :: move ( s )}; } 
The 
This example does the following:
- 
     Defines a _conv std :: optional 
- 
     Defines a _retry_receiver set_error _retry () 
- 
     Defines an operation state that aggregates the input sender and receiver, and declares storage for the nested operation state in an optional _retry_receiver 
- 
     Starting the operation state dispatches to start 
- 
     The _retry () 
- 
     After reinitializing the inner operation state, _retry () start 
- 
     Defines a _retry_sender connect 
- 
     _retry_sender get_completion_signatures 
1.6. Examples: Schedulers
In this section we look at some schedulers of varying complexity.
1.6.1. Inline scheduler
namespace stdexec = std :: execution ; class inline_scheduler { template < class R > struct _op { using operation_state_concept = operation_state_t ; R rec_ ; void start () & noexcept { stdexec :: set_value ( std :: move ( rec_ )); } }; struct _env { template < class Tag > inline_scheduler query ( stdexec :: get_completion_scheduler_t < Tag > ) const noexcept { return {}; } }; struct _sender { using sender_concept = stdexec :: sender_t ; using _compl_sigs = stdexec :: completion_signatures < stdexec :: set_value_t () > ; using completion_signatures = _compl_sigs ; template < stdexec :: receiver_of < _compl_sigs > R > _op < R > connect ( R rec ) noexcept ( std :: is_nothrow_move_constructible_v < R > ) { return { std :: move ( rec )}; } _env get_env () const noexcept { return {}; } }; public : inline_scheduler () = default ; _sender schedule () const noexcept { return {}; } 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 
Although not a particularly useful scheduler, it serves to illustrate the basics
of implementing one. The 
- 
     Customizes execution :: schedule _sender 
- 
     The _sender sender set_error set_stopped execution :: completion_signatures 
- 
     The _sender execution :: connect _op 
- 
     The operation state customizes std :: execution :: start std :: execution :: set_value 
1.6.2. Single thread scheduler
This example shows how to create a scheduler for an execution resource that
consists of a single thread. It is implemented in terms of a lower-level
execution resource called 
class single_thread_context { std :: execution :: run_loop loop_ ; std :: thread thread_ ; public : single_thread_context () : loop_ () , thread_ ([ this ] { loop_ . run (); }) {} single_thread_context ( single_thread_context && ) = delete ; ~ 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 
The interesting bits are in the 
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:
- 
     server application that processes images 
- 
     execution resources: - 
       1 dedicated thread for network I/O 
- 
       N worker threads used for CPU-intensive work 
- 
       M threads for auxiliary I/O 
- 
       optional GPU context that may be used on some types of servers 
 
- 
       
- 
     all parts of the applications can be asynchronous 
- 
     no locks shall be used in user code 
1.7.1. Composability with execution :: let_ * 
   Example context:
- 
     we are looking at the flow of processing an HTTP request and sending back the response. 
- 
     show how one can break the (slightly complex) flow into steps with execution :: let_ * 
- 
     different phases of processing HTTP requests are broken down into separate concerns. 
- 
     each part of the processing might use different execution resources (details not shown in this example). 
- 
     error handling is generic, regardless which component fails; we always send the right response to the clients. 
Goals:
- 
     show how one can break more complex flows into steps with let_* functions. 
- 
     exemplify the use of let_value let_error let_stopped just 
namespace stdexec = std :: execution ; // Returns a sender that yields an http_request object for an incoming request stdexec :: sender auto schedule_request_start ( read_requests_ctx ctx ) {...} // Sends a response back to the client; yields a void signal on success stdexec :: sender auto send_response ( const http_response & resp ) {...} // Validate that the HTTP request is well-formed; forwards the request on success stdexec :: sender auto validate_request ( const http_request & req ) {...} // Handle the request; main application logic stdexec :: sender auto handle_request ( const http_request & req ) { //... return stdexec :: just ( http_response { 200 , result_body }); } // Transforms server errors into responses to be sent to the client stdexec :: sender auto error_to_response ( std :: exception_ptr err ) { try { std :: rethrow_exception ( err ); } catch ( const std :: invalid_argument & e ) { return stdexec :: just ( http_response { 404 , e . what ()}); } catch ( const std :: exception & e ) { return stdexec :: just ( http_response { 500 , e . what ()}); } catch (...) { return stdexec :: just ( http_response { 500 , "Unknown server error" }); } } // Transforms cancellation of the server into responses to be sent to the client stdexec :: sender auto stopped_to_response () { return stdexec :: just ( http_response { 503 , "Service temporarily unavailable" }); } //... // The whole flow for transforming incoming requests into responses stdexec :: 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 | stdexec :: let_value ( validate_request ) // process the request in a function that may be using a different execution resource | stdexec :: let_value ( handle_request ) // If there are errors transform them into proper responses | stdexec :: let_error ( error_to_response ) // If the flow is cancelled, send back a proper response | stdexec :: let_stopped ( stopped_to_response ) // write the result back to the client | stdexec :: let_value ( send_response ) // done ; // execute the whole flow asynchronously stdexec :: 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 
All our functions return 
Also, because of using 
1.7.2. Moving between execution resources with execution :: starts_on execution :: continues_on 
   Example context:
- 
     reading data from the socket before processing the request 
- 
     reading of the data is done on the I/O context 
- 
     no processing of the data needs to be done on the I/O context 
Goals:
- 
     show how one can change the execution resource 
- 
     exemplify the use of starts_on continues_on 
namespace stdexec = 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 = stdexec :: just ( sock , buf , buf_len ) | stdexec :: then ( legacy_read_from_socket ); // The entire flow auto snd = // start by reading data on the I/O thread stdexec :: starts_on ( io_sched , std :: move ( snd_read )) // do the processing on the worker threads pool | stdexec :: continues_on ( work_sched ) // process the incoming data (on worker threads) | stdexec :: then ([ buf ]( int read_len ) { process_read_data ( buf , read_len ); }) // done ; // execute the whole flow asynchronously stdexec :: start_detached ( std :: move ( snd )); 
The example assume that we need to wrap some legacy code of reading sockets, and handle execution resource 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 resource (i.e., thread pool).
Calling 
The completion-signal will be issued in the I/O execution resource, so we have
to move it to the work thread pool. This is achieved with the help of the 
The reader should notice the difference between 
1.8. Design changes from P0443
- 
     The executor 
- 
     Properties are not included in this paper. We see them as a possible future extension, if the committee gets more comfortable with them. 
- 
     Senders now advertise what scheduler, if any, their evaluation will complete on. 
- 
     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. 
- 
     P0443 did not propose a suite of sender algorithms necessary for writing sender code; this paper does. See § 4.19 User-facing sender factories, § 4.20 User-facing sender adaptors, and § 4.21 User-facing sender consumers. 
- 
     P0443 did not specify the semantics of variously qualified connect 
- 
     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. 
- 
     Support for untyped senders is dropped; the typed_sender sender sender_traits completion_signatures_of_t 
- 
     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 Customization points. 
- 
     A specific thread pool implementation is omitted, as per LEWG direction. 
- 
     Some additional utilities are added: - 
       run_loop 
- 
       completion_signatures transform_completion_signatures 
 
- 
       
1.9. 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.9.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.9.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 Halo: coroutine Heap Allocation eLision Optimization: the joint response, 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 choice for a basis of all standard async.
1.9.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.10. Field experience
1.10.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 
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:
- 
     Timed schedulers, which permit scheduling work on an execution resource at a particular time or after a particular duration has elapsed. In addition, it provides time-based algorithms. 
- 
     File I/O schedulers, which permit filesystem I/O to be scheduled. 
- 
     Two complementary abstractions for streams (asynchronous ranges), and a set of stream-based algorithms. 
Libunifex has seen heavy production use at Meta. An employee summarizes it as follows:
As of June, 2023, Unifex is still used in production at Meta. It’s used to express the asynchrony in rsys, and is therefore serving video calling to billions of people every month on Meta’s social networking apps on iOS, Android, Windows, and macOS. It’s also serving the Virtual Desktop experience on Oculus Quest devices, and some internal uses that run on Linux.
One team at Meta has migrated from
tofolly :: Future and seen significant developer efficiency improvements. Coroutines are easier to understand than chained futures so the team was able to meet requirements for certain constrained environments that would have been too complicated to maintain with futures.unifex :: task In all the cases mentioned above, developers mix-and-match between the sender algorithms in Unifex and Unifex’s coroutine type,
. We also rely onunifex :: task ’s scheduler affinity to minimize surprise when programming with coroutines.unifex :: task 
1.10.2. stdexec
stdexec is the reference implementation of this proposal. It is a complete implementation, written from the specification in this paper, and is current with \R8.
The original purpose of stdexec was to help find specification bugs and to harden the wording of the proposal, but it has since become one of NVIDIA’s core C++ libraries for high-performance computing. In addition to the facilities proposed in this paper, stdexec has schedulers for CUDA, Intel TBB, and MacOS. Like libunifex, its scope has also expanded to include a streaming abstraction and stream algorithms, and time-based schedulers and algorithms.
The stdexec project has seen lots of community interest and contributions. At the time of writing (March, 2024), the GitHub repository has 1.2k stars, 130 forks, and 50 contributors.
stdexec is fit for broad use and for ultimate contribution to libc++.
1.10.3. 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.
- 
     HPX - The C++ Standard Library for Parallelism and Concurrency HPX is a general purpose C++ runtime system for parallel and distributed applications that has been under active development since 2007. HPX exposes a uniform, standards-oriented API, and keeps abreast of the latest standards and proposals. It is used in a wide variety of high-performance applications. The sender/receiver implementation in HPX has been under active development since May 2020. It is used to erase the overhead of futures and to make it possible to write efficient generic asynchronous algorithms that are agnostic to their execution resource. In HPX, algorithms can migrate execution between execution resources, even to GPUs and back, using a uniform standard interface with sender/receiver. Far and away, the HPX team has the greatest usage experience outside Facebook. Mikael Simberg summarizes the experience as follows: Summarizing, for us the major benefits of sender/receiver compared to the old model are: - 
        Proper hooks for transitioning between execution resources. 
- 
        The adaptors. Things like let_value 
- 
        Separation of the error channel from the value channel (also cancellation, but we don’t have much use for it at the moment). Even from a teaching perspective having to explain that the future f2 f1 . then ([]( future < T > f2 ) > {...}) 
- 
        For futures we have a thing called hpx :: dataflow when_all (...). then (...) when_all (...) | > then (...) 
 
- 
        
- 
     kuhllib by Dietmar Kuehl This is a prototype Standard Template Library with an implementation of sender/receiver that has been under development since May, 2021. It is significant mostly for its support for sender/receiver-based networking interfaces. Here, Dietmar Kuehl speaks about the perceived complexity of sender/receiver: ... and, also similar to STL: as I had tried to do things in that space before I recognize sender/receivers as being maybe complicated in one way but a huge simplification in another one: like with STL I think those who use it will benefit - if not from the algorithm from the clarity of abstraction: the separation of concerns of STL (the algorithm being detached from the details of the sequence representation) is a major leap. Here it is rather similar: the separation of the asynchronous algorithm from the details of execution. Sure, there is some glue to tie things back together but each of them is simpler than the combined result. Elsewhere, he said: ... to me it feels like sender/receivers are like iterators when STL emerged: they are different from what everybody did in that space. However, everything people are already doing in that space isn’t right. Kuehl also has experience teaching sender/receiver at Bloomberg. About that experience he says: When I asked [my students] specifically about how complex they consider the sender/receiver stuff the feedback was quite unanimous that the sender/receiver parts aren’t trivial but not what contributes to the complexity. 
- 
     C++ Bare Metal Senders and Receivers from Intel This is a prototype implementation of sender/receiver by Intel that has been under development since August, 2023. It is significant mostly for its support for bare metal (no operating system) and embedded systems, a domain for which senders are particularly well-suited due to their very low dynamic memory requirements. 
1.10.4. 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. R10
The changes since R9 are as follows:
Fixes:
- 
     Fixed connect get_completion_signatures transform_sender 
- 
     ensure_started start_detached execute execute_may_block_caller 
- 
     Fixed a logic error in the specification of split 
- 
     Fixed stopped_as_optional stopped_as_optional optional tuple 
- 
     The queryable stoppable_source stoppable_callback_for 
Enhancements:
- 
     The operation_state queryable 
- 
     The get_delegatee_scheduler get_delegation_scheduler 
- 
     The read read_env 
- 
     The nullary forms of the queries which returned instances of the read_env get_scheduler () read_env ( get_scheduler ) 
- 
     A feature test macro has been added: __cpp_lib_senders 
- 
     transfer continues_on on starts_on on starts_on continues_on 
- 
     An exposition-only simple - allocator get_allocator 
- 
     An exposition-only write - env on 
2.2. R9
The changes since R8 are as follows:
Fixes:
- 
     The tag_invoke 
- 
     Per guidance from LWG and LEWG, receiver_adaptor 
- 
     The receiver final receiver_adaptor tag_invoke 
- 
     std :: tag_t 
- 
     The types in_place_stop_token in_place_stop_source in_place_stop_callback inplace_stop_token inplace_stop_source inplace_stop_callback 
Enhancements:
- 
     The specification of the sync_wait 
- 
     The specification of all the stop token, source, and callback types have been re-expressed in terms of shared concepts. 
- 
     Declarations are shown in their proper namespaces. 
- 
     Editorial changes have been made to clarify what text is added, what is removed, and what is an editorial note. 
- 
     The section numbers of the proposed wording now match the section numbers in the working draft of the C++ standard. 
2.3. R8
The changes since R7 are as follows:
Fixes:
- 
     get_env ( obj ) 
- 
     get_env std :: execution std :: 
- 
     make_completion_signatures transform_completion_signatures_of transform_completion_signatures 
- 
     Add a requirement on queryable objects that if tag_invoke ( query , env , args ...) query ( env , args ...) 
- 
     The sender_in < Sndr , Env > E queryable 
- 
     Senders of more than one value are now co_await std :: tuple 
Enhancements:
- 
     The exposition-only class template basic - sender 
- 
     enable_sender enable_receiver sender_concept receiver_concept 
2.4. R7
The changes since R6 are as follows:
Fixes:
- 
     Make it valid to pass non-variadic templates to the exposition-only alias template gather - signatures value_types_of_t error_types_of_t sync - wait - result - type 
- 
     Removed the query forwarding from receiver_adaptor 
- 
     When adapting a sender to an awaitable with as_awaitable variant 
- 
     Correctly specify the completion signatures of the schedule_from 
- 
     The sender_of T T && 
- 
     The just just_error 
Enhancements:
- 
     The sender receiver enable_sender enable_receiver is_sender is_receiver 
- 
     get_attrs get_env 
- 
     The exposition-only type empty - env empty_env 
- 
     get_env empty_env {} tag_invoke 
- 
     get_env 
- 
     get_env empty_env env_of_t std :: 
- 
     Add a new subclause describing the async programming model of senders in abstract terms. See § 34.3 Asynchronous operations [async.ops]. 
2.5. R6
The changes since R5 are as follows:
Fixes:
- 
     Fix typo in the specification of in_place_stop_source 
- 
     get_completion_signatures connect 
- 
     A coroutine promise type is an environment provider (that is, it implements get_env () 
Enhancements:
- 
     Sender queries are moved into a separate queryable "attributes" object that is accessed by passing the sender to get_attrs () sender get_attrs () sender_in < Snd , Env > 
- 
     The placeholder types no_env dependent_completion_signatures <> 
- 
     ensure_started split get_attrs () 
- 
     Reorder constraints of the scheduler receiver 
- 
     Re-express the sender_of 
- 
     Make the specification of the alias templates value_types_of_t error_types_of_t sends_done gather - signatures 
2.5.1. Environments and attributes
In earlier revisions, receivers, senders, and schedulers all were directly
queryable. In R4, receiver queries were moved into a separate "environment"
object, obtainable from a receiver with a 
Schedulers, however, remain directly queryable. As lightweight handles that are required to be movable and copyable, there is little reason to want to dispose of a scheduler and yet persist the scheduler’s queries.
This revision also makes operation states directly queryable, even though there isn’t yet a use for such. Some early prototypes of cooperative bulk parallel sender algorithms done at NVIDIA suggest the utility of forwardable operation state queries. The authors chose to make opstates directly queryable since the opstate object is itself required to be kept alive for the duration of asynchronous operation.
2.6. R5
The changes since R4 are as follows:
Fixes:
- 
     start_detached void set_value 
Enhancements:
- 
     Receiver concepts refactored to no longer require an error channel for exception_ptr 
- 
     sender_of connect 
- 
     get_completion_signatures completion_signatures dependent_completion_signatures 
- 
     make_completion_signatures 
- 
     receiver_adaptor get_env set_ * receiver_adaptor get_env () get_env_t 
- 
     just just_error just_stopped into_variant 
2.7. R4
The changes since R3 are as follows:
Fixes:
- 
     Fix specification of get_completion_scheduler transfer schedule_from transfer_when_all set_error 
- 
     The value of sends_stopped falsetotrueto acknowledge the fact that some coroutine types are generally awaitable and may implement theunhandled_stopped () 
- 
     Fix the incorrect use of inline namespaces in the < execution > 
- 
     Shorten the stable names for the sections. 
- 
     sync_wait std :: error_code std :: system_error 
- 
     Fix how ADL isolation from class template arguments is specified so it doesn’t constrain implmentations. 
- 
     Properly expose the tag types in the header < execution > 
Enhancements:
- 
     Support for "dependently-typed" senders, where the completion signatures -- and thus the sender metadata -- depend on the type of the receiver connected to it. See the section dependently-typed senders below for more information. 
- 
     Add a read ( query ) 
- 
     Add completion_signatures 
- 
     Add make_completion_signatures 
- 
     Drop support for untyped senders and rename typed_sender sender 
- 
     set_done set_stopped done stopped 
- 
     Add customization points for controlling the forwarding of scheduler, sender, receiver, and environment queries through layers of adaptors; specify the behavior of the standard adaptors in terms of the new customization points. 
- 
     Add get_delegatee_scheduler 
- 
     Add schedule_result_t 
- 
     More precisely specify the sender algorithms, including precisely what their completion signatures are. 
- 
     stopped_as_error 
- 
     tag_invoke 
2.7.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 
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 
Example:
To get concrete, consider the case of the "
This causes knock-on problems since some important algorithms require a typed
sender, such as 
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 :: starts_on ( current_sched , nested work ... ); }); std :: this_thread :: sync_wait ( std :: move ( task )); 
The code above is attempting to schedule some work onto the 
Solution:
The solution is conceptually quite simple: extend the 
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 
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 
Impact:
This change, apart from increasing the expressive power of the sender/receiver abstraction, has the following impact:
- 
     Typed senders become moderately more challenging to write. (The new completion_signatures transform_completion_signatures 
- 
     Sender adaptor algorithms that previously constrained their sender arguments to satisfy the typed_sender connect 
- 
     Operation states that own receivers that add to or change the environment are typically larger by one pointer. It comes with the benefit of far fewer indirections to evaluate queries. 
"Has it been implemented?"
Yes, the reference implementation, which can be found at https://github.com/NVIDIA/stdexec, 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 
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 
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: 
Details:
Below are the salient parts of the new support for dependently-typed senders in R4:
- 
     Receiver queries have been moved from the receiver into a separate environment object. 
- 
     Receivers have an associated environment. The new get_env get_env 
- 
     sender_traits Env 
- 
     The primary sender_traits completion_signatures_of_t get_completion_signatures tag_invoke get_completion_signatures 
- 
     Support for untyped senders is dropped. The typed_sender sender 
- 
     The environment argument to the sender get_completion_signatures no_env no_env 
- 
     A type S sender < S > dependent_completion_signatures 
- 
     If a sender satisfies both sender < S > sender < S , Env > 
- 
     All of the algorithms and examples have been updated to work with dependently-typed senders. 
2.8. R3
The changes since R2 are as follows:
Fixes:
- 
     Fix specification of the starts_on get_scheduler 
- 
     Fix a memory safety bug in the implementation of connect - awaitable 
- 
     Fix recursive definition of the scheduler 
Enhancements:
- 
     Add run_loop 
- 
     Add receiver_adaptor 
- 
     Require a scheduler’s sender to model sender_of 
- 
     Specify the cancellation scope of the when_all 
- 
     Make as_awaitable 
- 
     Change connect as_awaitable 
- 
     Add value_types_of_t error_types_of_t stop_token_type_t stop_token_of_t 
- 
     Add a design rationale for the removal of the possibly eager algorithms. 
- 
     Expand the section on field experience. 
2.9. R2
The changes since R1 are as follows:
- 
     Remove the eagerly executing sender algorithms. 
- 
     Extend the execution :: connect sender_traits <> typed_sender 
- 
     Add utilities as_awaitable () with_awaitable_senders <> 
- 
     Add a section describing the design of the sender/awaitable interactions. 
- 
     Add a section describing the design of the cancellation support in sender/receiver. 
- 
     Add a section showing examples of simple sender adaptor algorithms. 
- 
     Add a section showing examples of simple schedulers. 
- 
     Add a few more examples: a sudoku solver, a parallel recursive file copy, and an echo server. 
- 
     Refined the forward progress guarantees on the bulk 
- 
     Add a section describing how to use a range of senders to represent async sequences. 
- 
     Add a section showing how to use senders to represent partial success. 
- 
     Add sender factories execution :: just_error execution :: just_stopped 
- 
     Add sender adaptors execution :: stopped_as_optional execution :: stopped_as_error 
- 
     Document more production uses of sender/receiver at scale. 
- 
     Various fixes of typos and bugs. 
2.10. R1
The changes since R0 are as follows:
- 
     Added a new concept, sender_of 
- 
     Added a new scheduler query, this_thread :: execute_may_block_caller 
- 
     Added a new scheduler query, get_forward_progress_guarantee 
- 
     Removed the unschedule 
- 
     Various fixes of typos and bugs. 
2.11. R0
Initial revision.
3. Design - introduction
The following three sections describe the entirety of the proposed design.
- 
     § 3 Design - introduction describes the conventions used through the rest of the design sections, as well as an example illustrating how we envision code will be written using this proposal. 
- 
     § 4 Design - user side describes all the functionality from the perspective we intend for users: it describes the various concepts they will interact with, and what their programming model is. 
- 
     § 5 Design - implementer side describes the machinery that allows for that programming model to function, and the information contained there is necessary for people implementing senders and sender algorithms (including the standard library ones) - but is not necessary to use senders productively. 
3.1. Conventions
The following conventions are used throughout the design section:
- 
     The namespace proposed in this paper is the same as in A Unified Executors Proposal for C++: std :: execution std :: execution :: foo std :: execution :: foo 
- 
     Universal references and explicit calls to std :: move std :: forward 
- 
     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 callable 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 callable 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 resources describe the place of execution
An execution resource 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 resources
A scheduler is a lightweight handle that represents a strategy for
scheduling work onto an execution resource. Since execution resources 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, 
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 resource associated with sch 
Note that a particular scheduler type may provide other kinds of scheduling
operations which are supported by its associated execution resource. It is not
limited to scheduling purely using the 
Future papers will propose additional scheduler concepts that extend 
- 
     A time_scheduler scheduler schedule_after ( sched , duration ) schedule_at ( sched , time_point ) now ( sched ) 
- 
     Concepts that extend scheduler 
- 
     Concepts that extend scheduler 
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.19 User-facing sender factories, § 4.20 User-facing sender adaptors, and § 4.21 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:
- 
     sender factories, algorithms that take no senders and return a sender. 
- 
     sender adaptors, algorithms that take (and potentially execution :: connect 
- 
     sender consumers, algorithms that take (and potentially execution :: connect 
4.5. Senders can propagate completion schedulers
One of the goals of executors is to support a diverse set of execution resources, including traditional thread pools, task and fiber frameworks (like HPX 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 resource 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.
A Unified Executors Proposal for C++ 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 (Towards C++23 executors: A proposal for an initial set of algorithms) 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:
- 
     trying to submit work to one execution resource (such as a CPU thread pool) from another execution resource (such as a GPU or a task framework), which assumes that all execution agents are as capable as a std :: thread 
- 
     forcibly interleaving two adjacent execution graph nodes that are both executing on one execution resource (such as a GPU) with glue code that runs on another execution resource (such as a CPU), which is prohibitively expensive for some execution resources (such as CUDA or SYCL). 
- 
     having to customise most or all sender algorithms to support an execution resource, 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 
4.5.1. execution :: get_completion_scheduler 
   
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 > ( get_env ( 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 > ( get_env ( snd1 )); // completion_sch1 is equivalent to cpu_sched execution :: sender auto snd2 = execution :: continues_on ( 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 > ( get_env ( snd3 )); // completion_sch3 is equivalent to gpu_sched 
4.6. Execution resource transitions are explicit
A Unified Executors Proposal for C++ does not contain any mechanisms for performing an execution
resource transition. The only sender algorithm that can create a sender that
will move execution to a specific execution resource is 
We propose that, for senders advertising their completion scheduler, all execution resource transitions must be explicit; running user code anywhere but where they defined it to run must be considered a bug.
The 
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 :: continues_on ( 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 
A single-shot sender can only be connected to a receiver
at most once. Its implementation of 
A multi-shot sender can be connected to multiple
receivers and can be launched multiple times. Multi-shot senders customise 
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 
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 
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 
Some multi-use sender algorithms may require that an input sender be
copy-constructible but will only call 
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 
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 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 
The ability to be able to cancel in-flight operations is fundamental to supporting some kinds of generic concurrency algorithms.
For example:
- 
     a when_all ( ops ...) 
- 
     a first_successful ( ops ...) 
- 
     a generic timeout ( src , duration ) src 
- 
     a stop_when ( src , trigger ) src trigger trigger src 
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" )), 5 s ), cancelButton . on_click ()); } 
In this example, if we take the operation returned by 
- 
     first_successful query_server_a ( query ) 
- 
     when_all load_file ( "some_file.jpg" ) 
- 
     timeout 
- 
     stop_when 
- 
     The parent operation consuming the composed_cancellation_example () 
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.9.1. Cancellation design summary
The design of cancellation described in this paper is built on top of and
extends the 
At a high-level, the facilities proposed by this paper for supporting cancellation include:
- 
     Add a std :: stoppable_token std :: stop_token 
- 
     Add std :: unstoppable_token stoppable_token 
- 
     Add std :: inplace_stop_token std :: inplace_stop_source std :: inplace_stop_callback < CB > 
- 
     Add std :: never_stop_token 
- 
     Add std :: execution :: get_stop_token () 
- 
     Add std :: execution :: stop_token_of_t < T > get_stop_token () 
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 
As the caller of 
4.9.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 false from the 
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 
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.9.3. Cancellation is inherently racy
Usually, an operation will attach a stop callback at some point inside the call
to 
A stop-request can be issued concurrently from another thread. This means the
implementation of 
An implementation of 
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 
For an example of an implementation strategy for solving these data-races see § 1.4 Asynchronous Windows socket recv.
4.9.4. Cancellation design status
This paper currently includes the design for cancellation as proposed in Composable cancellation for sender-based async operations - "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.10. 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 weighing 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 write an algorithm 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.10.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 
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:
- 
     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 ()) eager_op_1 () eager_op_2 () when_all when_all It then becomes the responsibility, not of the algorithm, but of the end user to handle the exception and ensure that eager_op_1 () 
- 
     Detach from the computation - let the operation continue in the background - like an implicit call to std :: thread :: detach () std :: shared_ptr 
- 
     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 threads 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:
- 
     A variation of (1): Call std :: terminate std :: thread 
- 
     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. 
- 
     A variation of (3): Request cancellation of the operation before blocking on its completion. This is the strategy that std :: jthread 
4.10.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 
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:
- 
     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. 
- 
     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.10.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 
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.10.4. Eager senders cannot access execution resource 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.11. Schedulers advertise their forward progress guarantees
To decide whether a scheduler (and its associated execution resource) 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:
- 
     concurrent, which requires that a thread makes progress eventually; 
- 
     parallel, which requires that a thread makes progress once it executes a step; and 
- 
     weakly parallel, which does not require that the thread makes progress. 
This paper introduces a scheduler query function, 
4.12. Most sender adaptors are pipeable
To facilitate an intuitive syntax for composition, most sender adaptors are pipeable; they can be composed (piped)
together with 
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) | 
 | 
| Function call (named temporaries) | 
 | 
| Pipe | 
 | 
Certain sender adaptors are not 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.
- 
     execution :: when_all execution :: when_all_with_variant 
- 
     execution :: starts_on continues_on 
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.13. 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 modeled 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 
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 
Now imagine that 
Far more interesting would be if 
4.14. 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.15. 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 
For an example, imagine a coroutine type called 
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.16. 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 
For example, consider the following trivial implementation of the sender-based 
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 
4.17. Cancellation of a sender can unwind a stack of coroutines
When looking at the sender-based 
When your task type’s promise inherits from 
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 
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 
Obviously, 
4.18. 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 
We will propose a customization point for combining schedulers with policies in order to provide control over where work will execute.
template < class ExecutionPolicy > unspecified executing_on ( execution :: scheduler auto scheduler , ExecutionPolicy && policy ); 
This function would return an object of an unspecified 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 
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 
In the future, we expect additional papers will propose asynchronous forms of
the parallel algorithms which (1) return senders rather than values or 
4.19. User-facing sender factories
A sender factory is an algorithm that takes no senders as parameters and returns a sender.
4.19.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 resources.
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.19.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 
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 ( snd2 , [] ( 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.19.3. 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 
4.19.4. execution :: just_stopped 
execution :: sender auto just_stopped (); 
Returns a sender with no completion schedulers, which
completes immediately by calling the receiver’s 
4.19.5. execution :: read_env 
execution :: sender auto read_env ( auto tag ); 
Returns a sender that reaches into a receiver’s environment and pulls out the
current value associated with the customization point denoted by 
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 :: read_env ( get_scheduler ) | execution :: let_value ([]( auto sched ) { return execution :: starts_on ( sched , some nested work here ); }); this_thread :: sync_wait ( std :: move ( task ) ); // wait for it to finish 
This code uses the fact that 
4.20. User-facing sender adaptors
A sender adaptor is an algorithm that takes one or more senders, which it
may 
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.1 this_thread::sync_wait start senders.
For more implementer-centric description of starting senders, see § 5.5 Sender adaptors are lazy.
4.20.1. execution :: continues_on 
execution :: sender auto continues_on ( 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 resource 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 :: continues_on ( cpu_task , gpu_sched ); // gpu_task describes the transition of the task graph described by cpu_task to the gpu 
4.20.2. execution :: then 
execution :: sender auto then ( execution :: sender auto input , std :: invocable < values - sent - by ( input ) ... > function ); 
execution :: sender auto input = get_input (); execution :: sender auto snd = execution :: then ( input , []( auto ... args ) { std :: ( 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.20.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 ); 
4.20.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 ); 
4.20.5. execution :: starts_on 
execution :: sender auto starts_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 resource associated with the provided scheduler. This returned sender has no completion schedulers.
4.20.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.20.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 
4.20.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 
4.20.9. execution :: bulk 
execution :: sender auto bulk ( execution :: sender auto input , std :: integral auto shape , 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 
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.20.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.20.11. execution :: when_all 
execution :: sender auto when_all ( execution :: sender auto ... inputs ); execution :: sender auto when_all_with_variant ( execution :: sender auto ... inputs ); 
The returned sender has no completion schedulers.
execution :: scheduler auto sched = thread_pool . scheduler (); execution :: sender auto sends_1 = ...; execution :: sender auto sends_abc = ...; execution :: sender auto both = execution :: when_all ( 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. User-facing sender consumers
A sender consumer is an algorithm that takes one or more senders, which it
may 
4.21.1. 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 ) >> ; 
If the provided sender sends an error instead of values, 
If the provided sender sends the "stopped" signal instead of values, 
For an explanation of the 
Note: This function is specified inside 
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:
- 
     set_value operator () 
- 
     set_error 
- 
     set_stopped set_value set_error 
Once an async operation has been started exactly one of these functions must be invoked on a receiver before it is destroyed.
While the receiver interface may look novel, it is in fact very similar to the
interface of 
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: 
Operation states are not a part of the user-facing API of this proposal; they
are necessary for implementing sender consumers like 
The return value of § 5.3 execution::connect must satisfy the operation state concept.
5.3. execution :: connect 
   
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 resource, and that execution resource will eventually call one of the // completion operations on 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 
- 
     sender . then ( invocable ) 
- 
     then ( sender , invocable ) 
- 
     a default implementation of then 
However, this definition is problematic. Imagine another sender adaptor, 
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 unspecified 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 
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 % lt ; 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 
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: the expression 
- 
     Create a sender that implements the default implementation of the sender algorithm. That sender is tuple-like; it can be destructured into its constituent parts: algorithm tag, data, and child sender(s). 
- 
     We query the child sender for its domain. A domain is a tag type associated with the scheduler that the child sender will complete on. If there are multiple child senders, we query all of them for their domains and require that they all be the same. 
- 
     We use the domain to dispatch to a transform_sender < sender - algorithm > 
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.
Strictly lazy submission means that there is a guarantee
that no work is submitted to an execution resource before a receiver is
connected to a sender, and 
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 resource, 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 resource. 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 resources 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, an implementation could recognize two subsequent § 4.20.9 execution::bulks of compatible shapes, and merge them together into a single submission of a GPU kernel.
5.7. Execution resource transitions are two-step
Because 
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 
To allow for such customization from both ends, we propose the inclusion of a
secondary transitioning sender adaptor, called 
The default implementation of 
5.8. All senders are typed
All senders must advertise the types they will send when they complete. There are many sender adaptors that need this information. Even just transitioning from one execution context to another requires temporarily storing the async result data so it can be propagated in the new execution context. Doing that efficiently requires knowing the type of the data.
The mechanism a sender uses to advertise its completions is the 
This proposal includes utilities for parsing and manipulating the list of a
sender’s completion signatures. For instance, 
5.9. Customization points
Earlier versions of this paper used a dispatching technique known as 
struct my_receiver { friend void tag_invoke ( execution :: set_value_t , my_receiver && self , int value ) noexcept { std :: cout << "received value: " << value ; } //... }; 
The 
struct my_receiver { using receiver_concept = execution :: receiver_t ; void set_value ( int value ) && noexcept { std :: cout << "received value: " << value ; } //... }; 
The only exception to this is the customization of queries. There is a need to
build queryable adaptors that can forward an open and unknowable set of queries
to some wrapped object. This is done by defining a member function named 
template < class Query , class Queryable , class ... Args > concept query_for = requires ( const Queryable & o , Args && ... args ) { o . query ( Query (), ( Args && ) args ...); }; template < class Allocator = std :: allocator <> , class Base = execution :: empty_env > struct with_allocator { Allocator alloc {}; Base base {}; // Forward unknown queries to the wrapped object: template < query_for < Base > Query > decltype ( auto ) query ( Query q ) const { return base . query ( q ); } // Specialize the query for the allocator: Allocator query ( execution :: get_allocator_t ) const { return alloc ; } }; 
Customization of sender algorithms such as 
6. Specification
Much of this wording follows the wording of A Unified Executors Proposal for C++.
§ 22 General utilities library [utilities] is meant to be a diff relative to the wording of the [utilities] clause of Working Draft, Standard for Programming Language C++.
§ 33 Concurrency support library [thread] is meant to be a diff relative to the wording of the [thread] clause of Working Draft, Standard for Programming Language C++. This diff applies changes from Composable cancellation for sender-based async operations.
§ 34 Execution control library [exec] is meant to be added as a new library clause to the working draft of C++.
14. Exception handling [except]
14.6. Special functions [except.special]
14.6.2. The std :: terminate 
    At the end of the bulleted list in the Note in paragraph 1, add a new bullet as follows:
- 
       when a call to a wait () wait_until () wait_for () 
- 
        when a callback invocation exits via an exception when requesting stop on a std :: stop_source std :: inplace_stop_source std :: stop_callback std :: inplace_stop_callback 
- 
        when a run_loop running 
- 
        when unhandled_stopped () with_awaitable_senders < T > unhandled_stopped () 
16. Library introduction [library]
At the end of [expos.only.entity], add the following:
- 
      The following are defined for exposition only to aid in the specification of the library: namespace std { // ...as before... } 
- 
       An object dst src dst decay_t < decltype (( src )) > dst src 
16.1.
16.2.
16.3.
16.4.
16.4.1.
16.4.2.
16.4.3.
16.4.4.
16.4.4.1.
16.4.4.2.
16.4.4.3.
16.4.4.4.
16.4.4.5.
16.4.4.6.
16.4.4.6.1. General [allocator.requirements.general]
At the end of [allocator.requirements.general], add the following new paragraph:
- 
      [Example 2: The following is an allocator class template supporting the minimal interface that meets the requirements of [allocator.requirements.general]: template < class T > struct SimpleAllocator { using value_type = T ; SimpleAllocator ( ctor args ); template < class U > SimpleAllocator ( const SimpleAllocator < U >& other ); T * allocate ( std :: size_t n ); void deallocate ( T * p , std :: size_t n ); template < class U > bool operator == ( const SimpleAllocator < U >& rhs ) const ; }; -- end example] 
- 
       The following exposition-only concept defines the minimal requirements on an Allocator type. template < class Alloc > concept simple - allocator = requires ( Alloc alloc , size_t n ) { { * alloc . allocate ( n ) } -> same_as < typename Alloc :: value_type &> ; { alloc . deallocate ( alloc . allocate ( n ), n ) }; } && copy_constructible < Alloc > && equality_comparable < Alloc > ; - 
         A type Alloc simple - allocator 
 
- 
         
17. Language support library [cpp]
17.3. Implementation properties [support.limits]
17.3.2. Header < version > 
    To the 
#define __cpp_lib_semaphore 201907L // also in <semaphore> #define __cpp_lib_senders 2024XXL // also in <execution> #define __cpp_lib_shared_mutex 201505L // also in <shared_mutex> 
22. General utilities library [utilities]
22.10. Function objects [function.objects]
22.10.2. Header < functional > 
    At the end of this subclause, insert the following
declarations into the synopsis within 
namespace std { // ...as before... namespace ranges { // 22.10.9, concept-constrained comparisons struct equal_to ; // freestanding struct not_equal_to ; // freestanding struct greater ; // freestanding struct less ; // freestanding struct greater_equal ; // freestanding struct less_equal ; // freestanding } template < class Fn , class ... Args > concept callable = // exposition only requires ( Fn && fn , Args && ... args ) { std :: forward < Fn > ( fn )( std :: forward < Args > ( args )...); }; template < class Fn , class ... Args > concept nothrow - callable = // exposition only callable < Fn , Args ... > && requires ( Fn && fn , Args && ... args ) { { std :: forward < Fn > ( fn )( std :: forward < Args > ( args )...) } noexcept ; }; // exposition only: template < class Fn , class ... Args > using call - result - t = decltype ( declval < Fn > ()( declval < Args > ()...)); template < const auto & Tag > using decayed - typeof = decltype ( auto ( Tag )); // exposition only } 
33. Concurrency support library [thread]
33.3. Stop tokens [thread.stoptoken]
33.3.1. Introduction [thread.stoptoken.intro]
- 
      Subclause [thread.stoptoken] describes components that can be used to asynchronously request that an operation stops execution in a timely manner, typically because the result is no longer required. Such a request is called a stop request. 
- 
      stop_source stop_token stop_callback stoppable - source stoppable_token stoppable - callback - for ownership ofaccess to a stop state.AnyAny object modelingstop_source stop_token stop_callback stop_source stop_token stop_callback stoppable - source stoppable_token stoppable - callback - for stoppable - source stoppable_token stoppable - callback - for The last remaining owner of the stop state automatically releases the resources associated with the stop state.
- 
      A n object of a type that models stop pable _token whichthat can either- 
         actively poll the token to check if there has been a stop request, or 
- 
         register a callback using thethat will be called in the event that a stop request is made.stop_callback 
 A stop request made via aan object whose type modelsstop_source stoppable - source stop pable _token stop_source stoppable - source 
- 
         
- 
      Callbacks registered via aan object whose type modelsstop_callback stoppable - callback - for stop_source stoppable - source 
The following paragraph is moved to the specification of
the new 
- 
       Calls to the functions request_stop stop_requested stop_possible request_stop truesynchronizes with a call tostop_requested stop_token stop_source true. Registration of a callback synchronizes with the invocation of that callback.
- 
       The types stop_source stop_token stop_callback 
- 
       An object of type inplace_stop_source inplace_stop_token inplace_stop_callback inplace_stop_source 
33.3.2. Header < stop_token > 
    In this subclause, insert the following
declarations into the 
namespace std { // [stoptoken.concepts], stop token concepts template < class CallbackFn , class Token , class Initializer = CallbackFn > concept stoppable - callback - for = see below ; // exposition only template < class Token > concept stoppable_token = see below ; template < class Token > concept unstoppable_token = see below ; template < class Source > concept stoppable - source = see below ; // exposition only // 33.3.3, class stop_token class stop_token ; // 33.3.4, class stop_source class stop_source ; // no-shared-stop-state indicator struct nostopstate_t { explicit nostopstate_t () = default ; }; inline constexpr nostopstate_t nostopstate {}; // 33.3.5, class template stop_callback template < class Callback Fn > class stop_callback ; // [stoptoken.never], class never_stop_token class never_stop_token ; // [stoptoken.inplace], class inplace_stop_token class inplace_stop_token ; // [stopsource.inplace], class inplace_stop_source class inplace_stop_source ; // [stopcallback.inplace], class template inplace_stop_callback template < class CallbackFn > class inplace_stop_callback ; template < class T , class CallbackFn > using stop_callback_for_t = T :: template callback_type < CallbackFn > ; } 
Insert the following subclause as a new subclause between
Header 
33.3.3. Stop token concepts [stoptoken.concepts]
- 
       The exposition-only stoppable - callback - for Token template < class CallbackFn , class Token , class Initializer = CallbackFn > concept stoppable - callback - for = // exposition only invocable < CallbackFn > && constructible_from < CallbackFn , Initializer > && requires { typename stop_callback_for_t < Token , CallbackFn > ; } && constructible_from < stop_callback_for_t < Token , CallbackFn > , const Token & , Initializer > ; 
- 
       Let t u Token init same_as < decltype ( init ), Initializer > true; and letSCB stop_callback_for_t < Token , CallbackFn > 
- 
       The concept stoppable - callback - for < CallbackFn , Token , Initializer > - 
         The following concepts are modeled: - 
           constructible_from < SCB , Token , Initializer > 
- 
           constructible_from < SCB , Token & , Initializer > 
- 
           constructible_from < SCB , const Token , Initializer > 
 
- 
           
- 
         An object of type SCB CallbackFn scb SCB callback_fn scb scb t init - 
           If t . stop_possible () true:- 
             callback_fn init 
- 
             Construction of scb callback_fn init 
- 
             The callback invocation std :: forward < CallbackFn > ( callback_fn )() t - 
               If t . stop_requested () falseat the time of registration, the callback invocation is added to the stop state’s list of callbacks such thatstd :: forward < CallbackFn > ( callback_fn )() 
- 
               Otherwise, std :: forward < CallbackFn > ( callback_fn )() scb 
 
- 
               
- 
             If the callback invocation was added to stop state’s list of callbacks, scb 
 
- 
             
- 
           If t . stop_possible () false, there is no requirement that the initialization ofscb callback_fn 
 
- 
           
- 
         Destruction of scb - 
           If the constructor of scb t callback_fn 
- 
           Otherwise, the invocation of callback_fn 
- 
           If callback_fn callback_fn callback_fn callback_fn 
- 
           If callback_fn callback_fn 
- 
           A stoppable callback deregistration shall not block on the completion of the invocation of some other callback registered with the same logical stop state. 
- 
           The stoppable callback deregistration shall destroy callback_fn 
 
- 
           
 
- 
         
- 
       The stoppable_token unstoppable_token stoppable_token template < template < class > class > struct check - type - alias - exists ; // exposition-only template < class Token > concept stoppable_token = requires ( const Token tok ) { typename check - type - alias - exists < Token :: template callback_type > ; { tok . stop_requested () } noexcept -> same_as < bool > ; { tok . stop_possible () } noexcept -> same_as < bool > ; { Token ( tok ) } noexcept ; // see implicit expression variations // ([concepts.equality]) } && copyable < Token > && equality_comparable < Token > && swappable < Token > ; template < class Token > concept unstoppable_token = stoppable_token < Token > && requires ( const Token tok ) { requires bool_constant < ( ! tok . stop_possible ()) >:: value ; }; 
- 
       An object whose type models stoppable_token stoppable_token 
- 
       Let SP t . stop_possible () false, and letSR t . stop_requested () true.
- 
       The type Token stoppable_token - 
         Any evaluation of u . stop_possible () u . stop_requested () SP false.
- 
         Any evaluation of u . stop_possible () u . stop_requested () SR true.
- 
         For any types CallbackFn Initializer stoppable - callback - for < CallbackFn , Token , Initializer > stoppable - callback - for < CallbackFn , Token , Initializer > 
- 
         If t t . stop_possible () t . stop_requested () false.
- 
         If t u t u t == u true; otherwise, it isfalse.
 
- 
         
- 
       An object whose type models the exposition-only stoppable - source stop_requested stop_possible get_token request_stop template < class Source > concept stoppable - source = // exposition only requires ( Source & src , const Source csrc ) { // see implicit expression variations // ([concepts.equality]) { csrc . get_token () } -> stoppable_token ; { csrc . stop_possible () } noexcept -> same_as < bool > ; { csrc . stop_requested () } noexcept -> same_as < bool > ; { src . request_stop () } -> same_as < bool > ; }; - 
         An object whose type models stoppable - source s stoppable - source s . stop_possible () s . stop_requested () false.
- 
         Let t stoppable - source t t . get_token () t 
 The following paragraph is moved from the introduction, with minor modifications (underlined in green). - 
         Calls to the member functions request_stop stop_requested stop_possible stoppable_token request_stop truesynchronizes with a call tostop_requested stop pable _token stop_source stoppable - source true. Registration of a callback synchronizes with the invocation of that callback.
 The following paragraph is taken from § 33.3.5.3 Member functions [stopsource.mem] and modified. - 
         If the stoppable - source request_stop false. Otherwise, it shall execute a stop request operation on the associated stop state. A stop request operation determines whether the stop state has received a stop request, and if not, makes a stop request. The determination and making of the stop request shall happen atomically, as-if by a read-modify-write operation ([intro.races]). If the request was made, the stop state’s registered callback invocations shall be synchronously executed. If an invocation of a callback exits via an exception thenterminate request_stop trueif a stop request was made, andfalseotherwise. After a call torequest_stop stop_possible falseor a call tostop_requested true.A stop request includes notifying all condition variables of type condition_variable_any 
 
- 
         
Modify subclause [stoptoken] as follows:
33.3.4. Class stop_token 
    33.3.4.1. General [stoptoken.general]
- 
      The classThe classstop_token stop_requested stop_possible stop_source stop_token stop_callback stop_source stop_token stoppable_token stop_source stop_token 
namespace std { class stop_token { public : template < class CallbackFn > using callback_type = stop_callback < CallbackFn > ; // [stoptoken.cons], constructors, copy, and assignment stop_token () noexcept = default ; stop_token ( const stop_token & ) noexcept ; stop_token ( stop_token && ) noexcept ; stop_token & operator = ( const stop_token & ) noexcept ; stop_token & operator = ( stop_token && ) noexcept ; ~ stop_token (); // [stoptoken.mem], Member functions void swap ( stop_token & ) noexcept ; // [stoptoken.mem], stop handling [[ nodiscard ]] bool stop_requested () const noexcept ; [[ nodiscard ]] bool stop_possible () const noexcept ; bool operator == ( const stop_token & rhs ) const noexcept = default ; [[ nodiscard ]] friend bool operator == ( const stop_token & lhs , const stop_token & rhs ) noexcept ; friend void swap ( stop_token & lhs , stop_token & rhs ) noexcept ; private : shared_ptr < unspecified > stop - state {}; // exposition only }; } 
- 
       stop - state stop_token stop_token stop - state 
33.3.4.2. Constructors, copy, and assignment [stoptoken.cons]
stop_token () noexcept ; 
- 
      Postconditions: stop_possible () falseandstop_requested () false.stop_token 
stop_token ( const stop_token & rhs ) noexcept ; 
- 
      Postconditions: * this == rhs true.* this rhs 
stop_token ( stop_token && rhs ) noexcept ; 
- 
      Postconditions: * this rhs rhs . stop_possible () false.
~ stop_token (); 
- 
      Effects: Releases ownership of the stop state, if any. 
stop_token & operator = ( const stop_token & rhs ) noexcept ; 
- 
      Effects: Equivalent to: stop_token ( rhs ). swap ( * this ) 
- 
      Returns: * this 
stop_token & operator = ( stop_token && rhs ) noexcept ; 
- 
      Effects: Equivalent to: stop_token ( std :: move ( rhs )). swap ( * this ) 
- 
      Returns: * this 
Move 
33.3.4.3. Member functions [stoptoken.mem]
void swap ( stop_token & rhs ) noexcept ; 
- 
     Effects: Exchanges the values ofEquivalent to:* this rhs stop - state . swap ( rhs . stop - state ) 
[[ nodiscard ]] bool stop_requested () const noexcept ; 
- 
     Returns: trueif* this stop - state false.
[[ nodiscard ]] bool stop_possible () const noexcept ; 
- 
     Returns: falseif:- 
        * this does not have ownership of a stop stateis disengaged , or
- 
        a stop request was not made and there are no associated stop_source true.
 
- 
        
The following are covered by the 
33.3.4.4. Non-member functions [stoptoken.nonmembers]
[[ nodiscard ]] bool operator == ( const stop_token & lhs , const stop_token & rhs ) noexcept ; 
- 
      Returns: trueiflhs rhs lhs rhs false.
friend void swap ( stop_token & x , stop_token & y ) noexcept ; 
- 
      Effects: Equivalent to: x . swap ( y ) 
33.3.5. Class stop_source 
    33.3.5.1. General [stopsource.general]
- 
      The classstop_source stop_source stop_source stop_token 
namespace std { The following definitions are already specified in the < stop_token > synopsis : // no-shared-stop-state indicator struct nostopstate_t { explicit nostopstate_t () = default ; }; inline constexpr nostopstate_t nostopstate {}; class stop_source { public : // 33.3.4.2, constructors, copy, and assignment stop_source (); explicit stop_source ( nostopstate_t ) noexcept ; {} stop_source ( const stop_source & ) noexcept ; stop_source ( stop_source && ) noexcept ; stop_source & operator = ( const stop_source & ) noexcept ; stop_source & operator = ( stop_source && ) noexcept ; ~ stop_source (); // [stopsource.mem], Member functions void swap ( stop_source & ) noexcept ; // 33.3.4.3, stop handling [[ nodiscard ]] stop_token get_token () const noexcept ; [[ nodiscard ]] bool stop_possible () const noexcept ; [[ nodiscard ]] bool stop_requested () const noexcept ; bool request_stop () noexcept ; bool operator == ( const stop_source & rhs ) const noexcept = default ; [[ nodiscard ]] friend bool operator == ( const stop_source & lhs , const stop_source & rhs ) noexcept ; friend void swap ( stop_source & lhs , stop_source & rhs ) noexcept ; private : shared_ptr < unspecified > stop - state {}; // exposition only }; } 
- 
       stop - state stop_source stop_source stop - state 
- 
       stop_source stoppable - source copyable equality_comparable swappable 
33.3.5.2. Constructors, copy, and assignment [stopsource.cons]
stop_source (); 
- 
      Effects: Initialises * this stop - state 
- 
      Postconditions: stop_possible () trueandstop_requested () false.
- 
      Throws: bad_alloc 
explicit stop_source ( nostopstate_t ) noexcept ; 
- 
       Postconditions: stop_possible () falseandstop_requested () false. No resources are allocated for the state.
stop_source ( const stop_source & rhs ) noexcept ; 
- 
       Postconditions: * this true.* this rhs 
stop_source ( stop_source && rhs ) noexcept ; 
- 
       Postconditions: * this rhs rhs . stop_possible () false.
~ stop_source (); 
- 
       Effects: Releases ownership of the stop state, if any. 
stop_source & operator = ( const stop_source & rhs ) noexcept ; 
- 
       Effects: Equivalent to: stop_source ( rhs ). swap ( * this ) 
- 
       Returns: * this 
stop_source & operator = ( stop_source && rhs ) noexcept ; 
- 
       Effects: Equivalent to: stop_source ( std :: move ( rhs )). swap ( * this ) 
- 
       Returns: * this 
Move 
33.3.5.3. Member functions [stopsource.mem]
void swap ( stop_source & rhs ) noexcept ; 
- 
      Effects: Exchanges the values ofEquivalent to:* this rhs stop - state . swap ( rhs . stop - state ) 
[[ nodiscard ]] stop_token get_token () const noexcept ; 
- 
      Returns: stop_token () stop_possible () false; otherwise a new associatedstop_token stop - state stop - state * this 
[[ nodiscard ]] bool stop_possible () const noexcept ; 
- 
      Returns: trueif* this falsestop - state != nullptr 
[[ nodiscard ]] bool stop_requested () const noexcept ; 
- 
      Returns: trueif* this stop - state false.
bool request_stop () noexcept ; 
- 
       Effects: Executes a stop request operation ([stoptoken.concepts]) on the associated stop state, if any. 
- 
       Effects: If * this false. Otherwise, atomically determines whether the owned stop state 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 associatedstop_callback terminate A stop request includes notifying all condition variables of type condition_variable_any 
- 
       Postconditions: stop_possible () falseorstop_requested () true.
- 
       Returns: trueif this call made a stop request; otherwisefalse.
33.3.5.4. Non-member functions [stopsource.nonmembers]
[[ nodiscard ]] friend bool operator == ( const stop_source & lhs , const stop_source & rhs ) noexcept ; 
- 
       Returns: trueiflhs rhs lhs rhs false.
friend void swap ( stop_source & x , stop_source & y ) noexcept ; 
- 
       Effects: Equivalent to: x . swap ( y ) 
33.3.6. Class template stop_callback 
    33.3.6.1. General [stopcallback.general]
namespace std { template < class Callback Fn > class stop_callback { public : using callback_type = Callback Fn ; // 33.3.5.2, constructors and destructor template < class C Initializer > explicit stop_callback ( const stop_token & st , C Initializer && cb init ) noexcept ( is_nothrow_constructible_v < Callback Fn , C Initializer > ); template < class C Initializer > explicit stop_callback ( stop_token && st , C Initializer && cb init ) noexcept ( is_nothrow_constructible_v < Callback Fn , C Initializer > ); ~ stop_callback (); stop_callback ( const stop_callback & ) = delete ; stop_callback ( stop_callback && ) = delete ; stop_callback & operator = ( const stop_callback & ) = delete ; stop_callback & operator = ( stop_callback && ) = delete ; private : Callback Fn callback callback - fn ; // exposition only }; template < class Callback Fn > stop_callback ( stop_token , Callback Fn ) -> stop_callback < Callback Fn > ; } 
- 
      Mandates: stop_callback Callback Fn invocable destructible 
- 
       Preconditions: stop_callback Callback invocable destructible 
- 
       Remarks: For a type Initializer stoppable - callback - for < CallbackFn , stop_token , Initializer > stoppable - callback - for < CallbackFn , stop_token , Initializer > callback - fn stop_callback < CallbackFn > 
33.3.6.2. Constructors and destructor [stopcallback.cons]
template < class C Initializer > explicit stop_callback ( const stop_token & st , C Initializer && cb init ) noexcept ( is_nothrow_constructible_v < Callback Fn , C Initializer > ); template < class C Initializer > explicit stop_callback ( stop_token && st , C Initializer && cb init ) noexcept ( is_nothrow_constructible_v < Callback Fn , C Initializer > ); 
- 
      Constraints: Callback Fn C Initializer constructible_from < Callback Fn , C Initializer > 
- 
       Preconditions: Callback C constructible_from < Callback , C > 
- 
      Effects: Initializes callback callback - fn std :: forward < C Initializer > ( cb init ) IfIf a callback is registered withst . stop_requested () true, thenstd :: forward & lt ; Callback > ( callback )() st std :: forward & lt ; Callback > ( callback )() request_stop () stop_source st * this 
- 
       Throws: Any exception thrown by the initialization of callback 
- 
       Remarks: If evaluating std :: forward < Callback > ( callback )() terminate 
~ stop_callback (); 
- 
      Effects: Unregisters the callback from the owned stop state, if any. The destructor does not block waiting for the execution of another callback registered by an associatedExecutes a stoppable callback deregistration ([stoptoken.concepts]) and releases ownership of the stop state, if any.stop_callback callback callback callback callback callback 
Insert a new subclause, Class 
33.3.7. Class never_stop_token 
     33.3.7.1. General [stoptoken.never.general]
- 
       The class never_stop_token unstoppable_token namespace std { class never_stop_token { struct callback - type { // exposition only explicit callback - type ( never_stop_token , auto && ) noexcept {} }; public : template < class > using callback_type = callback - type ; static constexpr bool stop_requested () noexcept { return false; } static constexpr bool stop_possible () noexcept { return false; } bool operator == ( const never_stop_token & ) const = default ; }; } 
Insert a new subclause, Class 
33.3.8. Class inplace_stop_token 
     33.3.8.1. General [stoptoken.inplace.general]
- 
       The class inplace_stop_token stoppable_token inplace_stop_source namespace std { class inplace_stop_token { public : template < class CallbackFn > using callback_type = inplace_stop_callback < CallbackFn > ; inplace_stop_token () = default ; bool operator == ( const inplace_stop_token & ) const = default ; // [stoptoken.inplace.mem], member functions bool stop_requested () const noexcept ; bool stop_possible () const noexcept ; void swap ( inplace_stop_token & ) noexcept ; private : const inplace_stop_source * stop - source = nullptr ; // exposition only }; } 
33.3.8.2. Member functions [stoptoken.inplace.members]
void swap ( inplace_stop_token & rhs ) noexcept ; 
- 
       Effects: Exchanges the values of stop - source rhs . stop - source 
bool stop_requested () const noexcept ; 
- 
       Effects: Equivalent to: return stop - source != nullptr && stop - source -> stop_requested (); 
- 
       As specified in [basic.life], the behavior of stop_requested () inplace_stop_source 
bool stop_possible () const noexcept ; 
- 
       Returns: stop - source != nullptr 
- 
       As specified in [basic.stc.general], the behavior of stop_possible () inplace_stop_source 
Insert a new subclause, Class 
33.3.9. Class inplace_stop_source 
     33.3.9.1. General [stopsource.inplace.general]
- 
       The class inplace_stop_source stoppable - source namespace std { class inplace_stop_source { public : // [stopsource.inplace.cons], constructors, copy, and assignment constexpr inplace_stop_source () noexcept ; inplace_stop_source ( inplace_stop_source && ) = delete ; inplace_stop_source ( const inplace_stop_source & ) = delete ; inplace_stop_source & operator = ( inplace_stop_source && ) = delete ; inplace_stop_source & operator = ( const inplace_stop_source & ) = delete ; ~ inplace_stop_source (); //[stopsource.inplace.mem], stop handling constexpr inplace_stop_token get_token () const noexcept ; static constexpr bool stop_possible () noexcept { return true; } bool stop_requested () const noexcept ; bool request_stop () noexcept ; }; } 
33.3.9.2. Constructors, copy, and assignment [stopsource.inplace.cons]
constexpr inplace_stop_source () noexcept ; 
- 
       Effects: Initializes a new stop state inside * this 
- 
       Postconditions: stop_requested () false.
33.3.9.3. Members [stopsource.inplace.mem]
constexpr inplace_stop_token get_token () const noexcept ; 
- 
       Returns: A new associated inplace_stop_token inplace_stop_token stop - source this 
bool stop_requested () const noexcept ; 
- 
       Returns: trueif the stop state inside* this false.
bool request_stop () noexcept ; 
- 
       Effects: Executes a stop request operation ([stoptoken.concepts]). 
- 
       Postconditions: stop_requested () true.
Insert a new subclause, Class template 
33.3.10. Class template inplace_stop_callback 
     33.3.10.1. General [stopcallback.inplace.general]
namespace std { template < class CallbackFn > class inplace_stop_callback { public : using callback_type = CallbackFn ; // [stopcallback.inplace.cons], constructors and destructor template < class Initializer > explicit inplace_stop_callback ( inplace_stop_token st , Initializer && init ) noexcept ( is_nothrow_constructible_v < CallbackFn , Initializer > ); ~ inplace_stop_callback (); inplace_stop_callback ( inplace_stop_callback && ) = delete ; inplace_stop_callback ( const inplace_stop_callback & ) = delete ; inplace_stop_callback & operator = ( inplace_stop_callback && ) = delete ; inplace_stop_callback & operator = ( const inplace_stop_callback & ) = delete ; private : CallbackFn callback - fn ; // exposition only }; template < class CallbackFn > inplace_stop_callback ( inplace_stop_token , CallbackFn ) -> inplace_stop_callback < CallbackFn > ; } 
- 
       Mandates: CallbackFn invocable destructible 
- 
       Remarks: For a type Initializer stoppable - callback - for < CallbackFn , inplace_stop_token , Initializer > stoppable - callback - for < CallbackFn , inplace_stop_token , Initializer > inplace_stop_callback < CallbackFn > callback - fn 
33.3.10.2. Constructors and destructor [stopcallback.inplace.cons]
template < class Initializer > explicit inplace_stop_callback ( inplace_stop_token st , Initializer && init ) noexcept ( is_nothrow_constructible_v < CallbackFn , Initializer > ); 
- 
       Constraints: constructible_from < CallbackFn , Initializer > 
- 
       Effects: Initializes callback - fn std :: forward < Initializer > ( init ) 
~ inplace_stop_callback (); 
- 
       Effects: Executes a stoppable callback deregistration ([stoptoken.concepts]). 
Insert a new top-level clause
34. Execution control library [exec]
34.1. General [exec.general]
- 
       This Clause describes components supporting execution of function objects [function.objects]. 
- 
       The following subclauses describe the requirements, concepts, and components for execution control primitives as summarized in Table 1. 
| Subclause | Header | 
- 
       Table 2 shows the types of customization point objects [customization.point.object] used in the execution control library: 
| Customization point object type | Purpose | Examples | 
|---|---|---|
| core | provide core execution functionality, and connection between core components | e.g., , | 
| completion functions | called by senders to announce the completion of the work (success, error, or cancellation) | ,, | 
| senders | allow the specialization of the provided sender algorithms | 
 | 
| queries | allow querying different properties of objects | 
 | 
- 
       This clause makes use of the following exposition-only entities: - 
         For a subexpression expr MANDATE - NOTHROW ( expr ) expr - 
           Mandates: noexcept ( expr ) true.
 
- 
           
- 
namespace std { template < class T > concept movable - value = // exposition only move_constructible < decay_t < T >> && constructible_from < decay_t < T > , T > && ( ! is_array_v < remove_reference_t < T >> ); } 
- 
         For function types F1 F2 R1 ( Args1 ...) R2 ( Args2 ...) MATCHING - SIG ( F1 , F2 ) trueif and only ifsame_as < R1 ( Args1 && ...), R2 ( Args2 && ...) > true.
- 
         For a subexpression err Err decltype (( err )) AS - EXCEPT - PTR ( err ) - 
           err decay_t < Err > exception_ptr - 
             Mandates: err != exception_ptr () true.
 
- 
             
- 
           Otherwise, make_exception_ptr ( system_error ( err )) decay_t < Err > error_code 
- 
           Otherwise, make_exception_ptr ( err ) 
 
- 
           
 
- 
         
34.2. Queries and queryables [exec.queryable]
34.2.1. General [exec.queryable.general]
- 
       A queryable object is a read-only collection of key/value pairs where each key is a customization point object known as a query object. A query is an invocation of a query object with a queryable object as its first argument and a (possibly empty) set of additional arguments. A query imposes syntactic and semantic requirements on its invocations. 
- 
       Let q args env o O cenv o decltype (( cenv )) const O & q ( env , args ...) q ( cenv , args ...) 
- 
       The type of a query expression can not be void 
- 
       The expression q ( env , args ...) 
- 
       If the expression env . query ( q , args ...) q ( env , args ...) 
- 
       Unless otherwise specified, the result of a query is valid as long as the queryable object is valid. 
34.2.2. queryable 
namespace std { template < class T > concept queryable = destructible < T > ; // exposition only } 
- 
       The exposition-only queryable 
- 
       Let env Env Env queryable q args requires { q ( env , args ...) } truethenq ( env , args ...) q 
34.3. Asynchronous operations [async.ops]
- 
       An execution resource is a program entity that manages a (possibly dynamic) set of execution agents ([thread.req.lockable.general]), which it uses to execute parallel work on behalf of callers. [Example 1: The currently active thread, a system-provided thread pool, and uses of an API associated with an external hardware accelerator are all examples of execution resources. -- end example] Execution resources execute asynchronous operations. An execution resource is either valid or invalid. 
- 
       An asynchronous operation is a distinct unit of program execution that: - 
         ... is explicitly created. 
- 
         ... can be explicitly started once at most. 
- 
         ... once started, eventually completes exactly once with a (possibly empty) set of result datums and in exactly one of three dispositions: success, failure, or cancellation. - 
           A successful completion, also known as a value completion, can have an arbitrary number of result datums. 
- 
           A failure completion, also known as an error completion, has a single result datum. 
- 
           A cancellation completion, also known as a stopped completion, has no result datum. 
 An asynchronous operation’s async result is its disposition and its (possibly empty) set of result datums. 
- 
           
- 
         ... can complete on a different execution resource than the execution resource on which it started. 
- 
         ... can create and start other asynchronous operations called child operations. A child operation is an asynchronous operation that is created by the parent operation and, if started, completes before the parent operation completes. A parent operation is the asynchronous operation that created a particular child operation. 
 An asynchronous operation can in fact execute synchronously; that is, it can complete during the execution of its start operation on the thread of execution that started it. 
- 
         
- 
       An asynchronous operation has associated state known as its operation state. 
- 
       An asynchronous operation has an associated environment. An environment is a queryable object ([exec.queryable]) representing the execution-time properties of the operation’s caller. The caller of an asynchronous operation is its parent operation or the function that created it. An asynchronous operation’s operation state owns the operation’s environment. 
- 
       An asynchronous operation has an associated receiver. A receiver is an aggregation of three handlers for the three asynchronous completion dispositions: a value completion handler for a value completion, an error completion handler for an error completion, and a stopped completion handler for a stopped completion. A receiver has an associated environment. An asynchronous operation’s operation state owns the operation’s receiver. The environment of an asynchronous operation is equal to its receiver’s environment. 
- 
       For each completion disposition, there is a completion function. A completion function is a customization point object ([customization.point.object]) that accepts an asynchronous operation’s receiver as the first argument and the result datums of the asynchronous operation as additional arguments. The value completion function invokes the receiver’s value completion handler with the value result datums; likewise for the error completion function and the stopped completion function. A completion function has an associated type known as its completion tag that is the unqualified type of the completion function. A valid invocation of a completion function is called a completion operation. 
- 
       The lifetime of an asynchronous operation, also known as the operation’s async lifetime, begins when its start operation begins executing and ends when its completion operation begins executing. If the lifetime of an asynchronous operation’s associated operation state ends before the lifetime of the asynchronous operation, the behavior is undefined. After an asynchronous operation executes a completion operation, its associated operation state is invalid. Accessing any part of an invalid operation state is undefined behavior. 
- 
       An asynchronous operation shall not execute a completion operation before its start operation has begun executing. After its start operation has begun executing, exactly one completion operation shall execute. The lifetime of an asynchronous operation’s operation state can end during the execution of the completion operation. 
- 
       A sender is a factory for one or more asynchronous operations. Connecting a sender and a receiver creates an asynchronous operation. The asynchronous operation’s associated receiver is equal to the receiver used to create it, and its associated environment is equal to the environment associated with the receiver used to create it. The lifetime of an asynchronous operation’s associated operation state does not depend on the lifetimes of either the sender or the receiver from which it was created. A sender is started when it is connected to a receiver and the resulting asynchronous operation is started. A sender’s async result is the async result of the asynchronous operation created by connecting it to a receiver. A sender sends its results by way of the asynchronous operation(s) it produces, and a receiver receives those results. A sender is either valid or invalid; it becomes invalid when its parent sender (see below) becomes invalid. 
- 
       A scheduler is an abstraction of an execution resource with a uniform, generic interface for scheduling work onto that resource. It is a factory for senders whose asynchronous operations execute value completion operations on an execution agent belonging to the scheduler’s associated execution resource. A schedule-expression obtains such a sender from a scheduler. A schedule sender is the result of a schedule expression. On success, an asynchronous operation produced by a schedule sender executes a value completion operation with an empty set of result datums. Multiple schedulers can refer to the same execution resource. A scheduler can be valid or invalid. A scheduler becomes invalid when the execution resource to which it refers becomes invalid, as do any schedule senders obtained from the scheduler, and any operation states obtained from those senders. 
- 
       An asynchronous operation has one or more associated completion schedulers for each of its possible dispositions. A completion scheduler is a scheduler whose associated execution resource is used to execute a completion operation for an asynchronous operation. A value completion scheduler is a scheduler on which an asynchronous operation’s value completion operation can execute. Likewise for error completion schedulers and stopped completion schedulers. 
- 
       A sender has an associated queryable object ([exec.queryable]) known as its attributes that describes various characteristics of the sender and of the asynchronous operation(s) it produces. For each disposition, there is a query object for reading the associated completion scheduler from a sender’s attributes; i.e., a value completion scheduler query object for reading a sender’s value completion scheduler, etc. If a completion scheduler query is well-formed, the returned completion scheduler is unique for that disposition for any asynchronous operation the sender creates. A schedule sender is required to have a value completion scheduler attribute whose value is equal to the scheduler that produced the schedule sender. 
- 
       A completion signature is a function type that describes a completion operation. An asynchronous operation has a finite set of possible completion signatures corresponding to the completion operations that the asynchronous operation potentially evaluates ([basic.def.odr]). For a completion function set rcvr args c set ( rcvr , args ...) F decltype ( auto ( set ))( decltype (( args ))...) Sig c MATCHING - SIG ( Sig , F ) true([exec.general]). Together, a sender type and an environment typeEnv Env 
- 
       A sender algorithm is a function that takes and/or returns a sender. There are three categories of sender algorithms: - 
         A sender factory is a function that takes non-senders as arguments and that returns a sender. 
- 
         A sender adaptor is a function that constructs and returns a parent sender from a set of one or more child senders and a (possibly empty) set of additional arguments. An asynchronous operation created by a parent sender is a parent operation to the child operations created by the child senders. 
- 
         A sender consumer is a function that takes one or more senders and a (possibly empty) set of additional arguments, and whose return type is not the type of a sender. 
 
- 
         
34.4. Header < execution > 
namespace std { // [exec.general], 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.queryable], queryable objects template < class T > concept queryable = see above ; // exposition only // [exec.queries], queries struct forwarding_query_t { see below }; struct get_allocator_t { see below }; struct get_stop_token_t { see below }; inline constexpr forwarding_query_t forwarding_query {}; 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 > ())) > ; template < class T > concept forwarding - query = // exposition only forwarding_query ( T {}); } namespace std :: execution { // [exec.queries], queries enum class forward_progress_guarantee { concurrent , parallel , weakly_parallel }; struct get_domain_t { see below }; struct get_scheduler_t { see below }; struct get_delegation_scheduler_t { see below }; struct get_forward_progress_guarantee_t { see below }; template < class CPO > struct get_completion_scheduler_t { see below }; inline constexpr get_domain_t get_domain {}; inline constexpr get_scheduler_t get_scheduler {}; inline constexpr get_delegation_scheduler_t get_delegation_scheduler {}; inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee {}; template < class CPO > inline constexpr get_completion_scheduler_t < CPO > get_completion_scheduler {}; struct empty_env {}; struct get_env_t { see below }; inline constexpr get_env_t get_env {}; template < class T > using env_of_t = decltype ( get_env ( declval < T > ())); // [exec.domain.default], execution domains struct default_domain ; // [exec.sched], schedulers struct scheduler_t {}; template < class Sch > concept scheduler = see below ; // [exec.recv], receivers struct receiver_t {}; template < class Rcvr > concept receiver = see below ; template < class Rcvr , class Completions > concept receiver_of = see below ; struct set_value_t { see below }; struct set_error_t { see below }; struct set_stopped_t { see below }; inline constexpr set_value_t set_value {}; inline constexpr set_error_t set_error {}; inline constexpr set_stopped_t set_stopped {}; // [exec.opstate], operation states struct operation_state_t {}; template < class O > concept operation_state = see below ; struct start_t { see below }; inline constexpr start_t start {}; // [exec.snd], senders struct sender_t {}; template < class Sndr > concept sender = see below ; template < class Sndr , class Env = empty_env > concept sender_in = see below ; template < class Sndr , class Rcvr > concept sender_to = see below ; template < class ... Ts > struct type - list ; // exposition only // [exec.getcomplsigs], completion signatures struct get_completion_signatures_t { see below }; inline constexpr get_completion_signatures_t get_completion_signatures {}; template < class Sndr , class Env = empty_env > requires sender_in < Sndr , Env > using completion_signatures_of_t = call - result - t < get_completion_signatures_t , Sndr , Env > ; 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 Sndr , class Env = empty_env , template < class ... > class Tuple = decayed - tuple , template < class ... > class Variant = variant - or - empty > requires sender_in < Sndr , Env > using value_types_of_t = see below ; template < class Sndr , class Env = empty_env , template < class ... > class Variant = variant - or - empty > requires sender_in < Sndr , Env > using error_types_of_t = see below ; template < class Sndr , class Env = empty_env > requires sender_in < Sndr , Env > inline constexpr bool sends_stopped = see below ; template < class Sndr , class Env > using single - sender - value - type = see below ; // exposition only template < class Sndr , class Env > concept single - sender = see below ; // exposition only template < sender Sndr > using tag_of_t = see below ; // [exec.snd.transform], sender transformations template < class Domain , sender Sndr , queryable ... Env > requires ( sizeof ...( Env ) <= 1 ) constexpr sender decltype ( auto ) transform_sender ( Domain dom , Sndr && sndr , const Env & ... env ) noexcept ( see below ); // [exec.snd.transform.env], environment transformations template < class Domain , sender Sndr , queryable Env > constexpr queryable decltype ( auto ) transform_env ( Domain dom , Sndr && sndr , Env && env ) noexcept ; // [exec.snd.apply], sender algorithm application template < class Domain , class Tag , sender Sndr , class ... Args > constexpr decltype ( auto ) apply_sender ( Domain dom , Tag , Sndr && sndr , Args && ... args ) noexcept ( see below ); // [exec.connect], the connect sender algorithm struct connect_t { see below }; inline constexpr connect_t connect {}; template < class Sndr , class Rcvr > using connect_result_t = decltype ( connect ( declval < Sndr > (), declval < Rcvr > ())); // [exec.factories], sender factories struct just_t { see below }; struct just_error_t { see below }; struct just_stopped_t { see below }; struct schedule_t { see below }; inline constexpr just_t just {}; inline constexpr just_error_t just_error {}; inline constexpr just_stopped_t just_stopped {}; inline constexpr schedule_t schedule {}; inline constexpr unspecified read {}; template < scheduler Sndr > using schedule_result_t = decltype ( schedule ( declval < Sndr > ())); // [exec.adapt], sender adaptors template < class - type D > struct sender_adaptor_closure { }; struct starts_on_t { see below }; struct continues_on_t { see below }; struct on_t { see below }; struct schedule_from_t { see below }; struct then_t { see below }; struct upon_error_t { see below }; struct upon_stopped_t { see below }; struct let_value_t { see below }; struct let_error_t { see below }; struct let_stopped_t { see below }; struct bulk_t { see below }; struct split_t { see below }; struct when_all_t { see below }; struct when_all_with_variant_t { see below }; struct into_variant_t { see below }; struct stopped_as_optional_t { see below }; struct stopped_as_error_t { see below }; inline constexpr starts_on_t starts_on {}; inline constexpr continues_on_t continues_on {}; inline constexpr on_t on {}; 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 into_variant_t into_variant {}; inline constexpr stopped_as_optional_t stopped_as_optional {}; inline constexpr stopped_as_error_t stopped_as_error {}; // [exec.utils], sender and receiver utilities // [exec.utils.cmplsigs] template < class Fn > concept completion - signature = // exposition only see below ; template < completion - signature ... Fns > struct completion_signatures {}; template < class Sigs > // exposition only concept valid - completion - signatures = see below ; // [exec.utils.tfxcmplsigs] template < valid - completion - signatures InputSignatures , valid - completion - signatures AdditionalSignatures = completion_signatures <> , template < class ... > class SetValue = see below , template < class > class SetError = see below , valid - completion - signatures SetStopped = completion_signatures < set_stopped_t () >> using transform_completion_signatures = completion_signatures < see below > ; template < sender Sndr , class Env = empty_env , valid - completion - signatures AdditionalSignatures = completion_signatures <> , template < class ... > class SetValue = see below , template < class > class SetError = see below , valid - completion - signatures SetStopped = completion_signatures < set_stopped_t () >> requires sender_in < Sndr , Env > using transform_completion_signatures_of = transform_completion_signatures < completion_signatures_of_t < Sndr , Env > , AdditionalSignatures , SetValue , SetError , SetStopped > ; // [exec.ctx], execution resources // [exec.run.loop], run_loop class run_loop ; } namespace std :: this_thread { // [exec.consumers], consumers struct sync_wait_t { see below }; struct sync_wait_with_variant_t { see below }; inline constexpr sync_wait_t sync_wait {}; inline constexpr sync_wait_with_variant_t sync_wait_with_variant {}; } namespace std :: execution { // [exec.as.awaitable] struct as_awaitable_t { see below }; inline constexpr as_awaitable_t as_awaitable {}; // [exec.with.awaitable.senders] template < class - type Promise > struct with_awaitable_senders ; } 
- 
       The exposition-only type variant - or - empty < Ts ... > - 
         If sizeof ...( Ts ) variant - or - empty < Ts ... > variant < Us ... > Us ... decay_t < Ts > ... 
- 
         Otherwise, variant - or - empty < Ts ... > namespace std :: execution { struct empty - variant { // exposition only empty - variant () = delete ; }; } 
 
- 
         
- 
       For types Sndr Env single - sender - value - type < Sndr , Env > - 
         value_types_of_t < Sndr , Env , decay_t , type_identity_t > 
- 
         Otherwise, void value_types_of_t < Sndr , Env , tuple , variant > variant < tuple <>> variant <> 
- 
         Otherwise, value_types_of_t < Sndr , Env , decayed - tuple , type_identity_t > 
- 
         Otherwise, single - sender - value - type < Sndr , Env > 
 
- 
         
- 
       The exposition-only concept single - sender namespace std :: execution { template < class Sndr , class Env > concept single - sender = sender_in < Sndr , Env > && requires { typename single - sender - value - type < Sndr , Env > ; }; } 
34.5. Queries [exec.queries]
34.5.1. forwarding_query 
     - 
       forwarding_query 
- 
       The name forwarding_query q Q forwarding_query ( q ) - 
         MANDATE - NOTHROW ( q . query ( forwarding_query )) - 
           Mandates: The expression above has type bool q 
 
- 
           
- 
         Otherwise, trueifderived_from < Q , forwarding_query_t > true.
- 
         Otherwise, false.
 
- 
         
34.5.2. get_allocator 
     - 
       get_allocator 
- 
       The name get_allocator env get_allocator ( env ) MANDATE - NOTHROW ( as_const ( env ). query ( get_allocator )) - 
         Mandates: If the expression above is well-formed, its type satisfies simple - allocator 
 
- 
         
- 
       forwarding_query ( get_allocator ) true.
34.5.3. get_stop_token 
     - 
       get_stop_token 
- 
       The name get_stop_token env get_stop_token ( env ) - 
         MANDATE - NOTHROW ( as_const ( env ). query ( get_stop_token )) - 
           Mandates: The type of the expression above satisfies stoppable_token 
 
- 
           
- 
         Otherwise, never_stop_token {} 
 
- 
         
- 
       forwarding_query ( get_stop_token ) true.
34.5.4. execution :: get_env 
     - 
       execution :: get_env o execution :: get_env ( o ) - 
         MANDATE - NOTHROW ( as_const ( o ). get_env ()) - 
           Mandates: The type of the expression above satisfies queryable 
 
- 
           
- 
         Otherwise, empty_env {} 
 
- 
         
- 
       The value of get_env ( o ) o 
- 
       When passed a sender object, get_env get_env 
34.5.5. execution :: get_domain 
     - 
       get_domain 
- 
       The name get_domain env get_domain ( env ) MANDATE - NOTHROW ( as_const ( env ). query ( get_domain )) 
- 
       forwarding_query ( execution :: get_domain ) true.
34.5.6. execution :: get_scheduler 
     - 
       get_scheduler 
- 
       The name get_scheduler env get_scheduler ( env ) MANDATE - NOTHROW ( as_const ( env ). query ( get_scheduler )) - 
         Mandates: If the expression above is well-formed, its type satisfies scheduler 
 
- 
         
- 
       forwarding_query ( execution :: get_scheduler ) true.
34.5.7. execution :: get_delegation_scheduler 
     - 
       get_delegation_scheduler 
- 
       The name get_delegation_scheduler env get_delegation_scheduler ( env ) MANDATE - NOTHROW ( as_const ( env ). query ( get_delegation_scheduler )) - 
         Mandates: If the expression above is well-formed, its type satisfies scheduler 
 
- 
         
- 
       forwarding_query ( execution :: get_delegation_scheduler ) true.
34.5.8. execution :: get_forward_progress_guarantee 
namespace std :: execution { enum class forward_progress_guarantee { concurrent , parallel , weakly_parallel }; } 
- 
       get_forward_progress_guarantee 
- 
       The name get_forward_progress_guarantee sch Sch decltype (( sch )) Sch scheduler get_forward_progress_guarantee get_forward_progress_guarantee ( sch ) - 
         MANDATE - NOTHROW ( as_const ( sch ). query ( get_forward_progress_guarantee )) - 
           Mandates: The type of the expression above is forward_progress_guarantee 
 
- 
           
- 
         Otherwise, forward_progress_guarantee :: weakly_parallel 
 
- 
         
- 
       If get_forward_progress_guarantee ( sch ) sch forward_progress_guarantee :: concurrent forward_progress_guarantee :: parallel 
34.5.9. execution :: get_completion_scheduler 
     - 
       get_completion_scheduler < completion - tag > 
- 
       The name get_completion_scheduler q get_completion_scheduler < completion - tag > ( q ) completion - tag set_value_t set_error_t set_stopped_t get_completion_scheduler < completion - tag > ( q ) MANDATE - NOTHROW ( as_const ( q ). query ( get_completion_scheduler < completion - tag > )) - 
         Mandates: If the expression above is well-formed, its type satisfies scheduler 
 
- 
         
- 
       Let completion - fn completion - tag completion - fn args sndr sender < decltype (( sndr )) > trueandget_completion_scheduler < completion - tag > ( get_env ( sndr )) sch sndr rcvr completion - fn ( rcvr , args ...) sch 
- 
       The expression forwarding_query ( get_completion_scheduler < completion - tag > ) true.
34.6. Schedulers [exec.sched]
- 
       The scheduler schedule schedule namespace std :: execution { template < class Sch > concept scheduler = derived_from < typename remove_cvref_t < Sch >:: scheduler_concept , scheduler_t > && queryable < Sch > && requires ( Sch && sch ) { { schedule ( std :: forward < Sch > ( sch )) } -> sender ; { auto ( get_completion_scheduler < set_value_t > ( get_env ( schedule ( std :: forward < Sch > ( sch ))))) } -> same_as < remove_cvref_t < Sch >> ; } && equality_comparable < remove_cvref_t < Sch >> && copy_constructible < remove_cvref_t < Sch >> ; } 
- 
       Let Sch Env sender_in < schedule_result_t < Sch > , Env > sender - in - of < schedule_result_t < Sch > , Env > 
- 
       None of a scheduler’s copy constructor, destructor, equality comparison, or swap schedule 
- 
       For any two values sch1 sch2 Sch sch1 == sch2 trueonly if bothsch1 sch2 
- 
       For a given scheduler expression sch get_completion_scheduler < set_value_t > ( get_env ( schedule ( sch ))) sch 
- 
       For a given scheduler expression sch get_domain ( sch ) get_domain ( get_env ( schedule ( sch ))) 
- 
       A scheduler type’s destructor shall not block pending completion of any receivers connected to the sender objects returned from schedule 
34.7. Receivers [exec.recv]
34.7.1. Receiver concepts [exec.recv.concepts]
- 
       A receiver represents the continuation of an asynchronous operation. The receiver receiver_of get_env namespace std :: execution { template < class Rcvr > concept receiver = derived_from < typename remove_cvref_t < Rcvr >:: receiver_concept , receiver_t > && requires ( const remove_cvref_t < Rcvr >& rcvr ) { { get_env ( rcvr ) } -> queryable ; } && move_constructible < remove_cvref_t < Rcvr >> && // rvalues are movable, and constructible_from < remove_cvref_t < Rcvr > , Rcvr > ; // lvalues are copyable template < class Signature , class Rcvr > concept valid - completion - for = // exposition only requires ( Signature * sig ) { [] < class Tag , class ... Args > ( Tag ( * )( Args ...)) requires callable < Tag , remove_cvref_t < Rcvr > , Args ... > {}( sig ); }; template < class Rcvr , class Completions > concept has - completions = // exposition only requires ( Completions * completions ) { [] < valid - completion - for < Rcvr > ... Sigs > ( completion_signatures < Sigs ... >* ) {}( completions ); }; template < class Rcvr , class Completions > concept receiver_of = receiver < Rcvr > && has - completions < Rcvr , Completions > ; } 
- 
       Class types that are marked final receiver 
- 
       Let rcvr op_state rcvr token get_stop_token ( get_env ( rcvr )) token rcvr op_state token token 
34.7.2. execution :: set_value 
     - 
       set_value set_value_t set_value ( rcvr , vs ...) rcvr vs rcvr MANDATE - NOTHROW ( rcvr . set_value ( vs ...)) 
34.7.3. execution :: set_error 
     - 
       set_error set_error_t set_error ( rcvr , err ) rcvr err rcvr MANDATE - NOTHROW ( rcvr . set_error ( err )) 
34.7.4. execution :: set_stopped 
     - 
       set_stopped set_stopped_t set_stopped ( rcvr ) rcvr rcvr const MANDATE - NOTHROW ( rcvr . set_stopped ()) 
34.8. Operation states [exec.opstate]
- 
       The operation_state namespace std :: execution { template < class O > concept operation_state = derived_from < typename O :: operation_state_concept , operation_state_t > && is_object_v < O > && requires ( O & o ) { { start ( o ) } noexcept ; }; } 
- 
       If an operation_state operation_state start operation_state 
- 
       The program is ill-formed if it performs a copy or move construction or assigment operation on an operation state object created by connecting a library-provided sender. 
34.8.1. execution :: start 
     - 
       The name start op start ( op ) op MANDATE - NOTHROW ( op . start ()) 
- 
       If op . start () op start ( op ) 
34.9. Senders [exec.snd]
34.9.1. General [exec.snd.general]
- 
       For the purposes of this subclause, a sender is an object whose type satisfies the sender 
- 
       Subclauses [exec.factories] and [exec.adapt] define customizable algorithms that return senders. Each algorithm has a default implementation. Let sndr Sndr decltype (( sndr )) rcvr Rcvr env Env sender_to < Sndr , Rcvr > true. For the default implementation of the algorithm that producedsndr sndr rcvr rcvr Sigs get_completion_signatures ( sndr , env ) completion_signatures Sigs sndr completion_signatures_of_t < Sndr , Env > Sigs 
- 
       This subclause makes use of the following exposition-only entities. - 
         For a queryable object env FWD - ENV ( env ) queryable q as FWD - ENV ( env ). query ( q , as ...) forwarding_query ( q ) false; otherwise, it is expression-equivalent toenv . query ( q , as ...) 
- 
         For a query object q v MAKE - ENV ( q , v ) env queryable env . query ( q ) v env . query ( q ) env 
- 
         For two queryable objects env1 env2 q as JOIN - ENV ( env1 , env2 ) env3 queryable env3 . query ( q , as ...) - 
           env1 . query ( q , as ...) 
- 
           otherwise, env2 . query ( q , as ...) 
- 
           otherwise, env3 . query ( q , as ...) 
 
- 
           
- 
         The results of FWD - ENV MAKE - ENV JOIN - ENV 
- 
         For a scheduler sch SCHED - ATTRS ( sch ) o1 queryable o1 . query ( get_completion_scheduler < Tag > ) sch Tag set_value_t set_stopped_t o1 . query ( get_domain ) sch . query ( get_domain ) SCHED - ENV ( sch ) o2 queryable o1 . query ( get_scheduler ) sch o2 . query ( get_domain ) sch . query ( get_domain ) 
- 
         For two subexpressions rcvr expr SET - VALUE ( rcvr , expr ) ( expr , set_value ( std :: move ( rcvr ))) expr void set_value ( std :: move ( rcvr ), expr ) TRY - EVAL ( rcvr , expr ) try { expr ; } catch (...) { set_error ( std :: move ( rcvr ), current_exception ()); } if expr expr TRY - SET - VALUE ( rcvr , expr ) TRY - EVAL ( rcvr , SET - VALUE ( rcvr , expr )) rcvr 
- 
template < class Default = default_domain , class Sndr > constexpr auto completion - domain ( const Sndr & sndr ) noexcept ; - 
           COMPL - DOMAIN ( T ) get_domain ( get_completion_scheduler < T > ( get_env ( sndr ))) 
- 
           Effects: If all of the types COMPL - DOMAIN ( set_value_t ) COMPL - DOMAIN ( set_error_t ) COMPL - DOMAIN ( set_stopped_t ) completion - domain < Default > ( sndr ) Default completion - domain < Default > ( sndr ) completion - domain < Default > ( sndr ) 
 
- 
           
- 
template < class Tag , class Env , class Default > constexpr decltype ( auto ) query - with - default ( Tag , const Env & env , Default && value ) noexcept ( see below ); - 
           Let e Tag ()( env ) static_cast < Default > ( std :: forward < Default > ( value )) 
- 
           Returns: e 
- 
           Remarks: The expression in the noexcept noexcept ( e ) 
 
- 
           
- 
template < class Sndr > constexpr auto get - domain - early ( const Sndr & sndr ) noexcept ; - 
           Effects: Equivalent to: return Domain (); Domain - 
             get_domain ( get_env ( sndr )) 
- 
             completion - domain ( sndr ) 
- 
             default_domain () 
 
- 
             
 
- 
           
- 
template < class Sndr , class Env > constexpr auto get - domain - late ( const Sndr & sndr , const Env & env ) noexcept ; - 
           Effects: Equivalent to: - 
             If sender - for < Sndr , continues_on_t > true, thenreturn Domain (); Domain [] { auto [ _ , sch , _ ] = sndr ; return query - or - default ( get_domain , sch , default_domain ()); }(); The continues_on schedule_from continues_on schedule_from continues_on continues_on 
- 
             Otherwise, return Domain (); Domain void - 
               get_domain ( get_env ( sndr )) 
- 
               completion - domain < void > ( sndr ) 
- 
               get_domain ( env ) 
- 
               get_domain ( get_scheduler ( env )) 
- 
               default_domain () 
 
- 
               
 
- 
             
 
- 
           
- 
template < callable Fun > requires is_nothrow_move_constructible_v < Fun > struct emplace - from { // exposition only Fun fun ; // exposition only using type = call - result - t < Fun > ; constexpr operator type () && noexcept ( nothrow - callable < Fun > ) { return std :: move ( fun )(); } constexpr type operator ()() && noexcept ( nothrow - callable < Fun > ) { return std :: move ( fun )(); } }; - 
           emplace - from tuple optional variant 
 
- 
           
- 
struct on - stop - request { // exposition only inplace_stop_source & stop - src ; // exposition only void operator ()() noexcept { stop - src . request_stop (); } }; 
- 
template < class T 0 , class T 1 , ... class T n > struct product - type { // exposition only T 0 t 0 ; // exposition only T 1 t 1 ; // exposition only ... T n t n ; // exposition only template < size_t I , class Self > constexpr decltype ( auto ) get ( this Self && self ) noexcept ; // exposition only template < class Self , class Fn > constexpr decltype ( auto ) apply ( this Self && self , Fn && fn ) // exposition only noexcept ( see below ); }; - 
           product - type tuple 
- 
           An expression of type product - type 
- 
template < size_t I , class Self > constexpr decltype ( auto ) get ( this Self && self ) noexcept ; - 
             Effects: Equivalent to: auto & [... ts ] = self ; return std :: forward_like < Self > ( ts ...[ I ]); 
 
- 
             
- 
template < class Self , class Fn > constexpr decltype ( auto ) apply ( this Self && self , Fn && fn ) noexcept ( see below ); - 
             Effects: Equivalent to: auto & [... ts ] = self ; return std :: forward < Fn > ( fn )( std :: forward_like < Self > ( ts )...); 
- 
             Requires: The expression in the return 
- 
             Remarks: The expression in the noexcept trueif thereturn false.
 
- 
             
 
- 
           
- 
template < class Tag , class Data = see below , class ... Child > constexpr auto make - sender ( Tag tag , Data && data , Child && ... child ); - 
           Mandates: The following expressions are true:- 
             semiregular < Tag > 
- 
             movable - value < Data > 
- 
             ( sender < Child > && ...) 
 
- 
             
- 
           Returns: A prvalue of type basic - sender < Tag , decay_t < Data > , decay_t < Child > ... > basic - sender namespace std :: execution { template < class Tag > concept completion - tag = // exposition only same_as < Tag , set_value_t > || same_as < Tag , set_error_t > || same_as < Tag , set_stopped_t > ; template < template < class ... > class T , class ... Args > concept valid - specialization = requires { typename T < Args ... > ; }; // exposition only struct default - impls { // exposition only static constexpr auto get - attrs = see below ; static constexpr auto get - env = see below ; static constexpr auto get - state = see below ; static constexpr auto start = see below ; static constexpr auto complete = see below ; }; template < class Tag > struct impls - for : default - impls {}; // exposition only template < class Sndr , class Rcvr > // exposition only using state - type = decay_t < call - result - t < decltype ( impls - for < tag_of_t < Sndr >>:: get - state ), Sndr , Rcvr &>> ; template < class Index , class Sndr , class Rcvr > // exposition only using env - type = call - result - t < decltype ( impls - for < tag_of_t < Sndr >>:: get - env ), Index , state - type < Sndr , Rcvr >& , const Rcvr &> ; template < class Sndr , size_t I = 0 > using child - type = decltype ( declval < Sndr > (). template get < I + 2 > ()); // exposition only template < class Sndr > using indices - for = remove_reference_t < Sndr >:: indices - for ; // exposition only template < class Sndr , class Rcvr > struct basic - state { // exposition only basic - state ( Sndr && sndr , Rcvr && rcvr ) noexcept ( see below ) : rcvr ( std :: move ( rcvr )) , state ( impls - for < tag_of_t < Sndr >>:: get - state ( std :: forward < Sndr > ( sndr ), rcvr )) { } Rcvr rcvr ; // exposition only state - type < Sndr , Rcvr > state ; // exposition only }; template < class Sndr , class Rcvr , class Index > requires valid - specialization < env - type , Index , Sndr , Rcvr > struct basic - receiver { // exposition only using receiver_concept = receiver_t ; using tag - t = tag_of_t < Sndr > ; // exposition only using state - t = state - type < Sndr , Rcvr > ; // exposition only static constexpr const auto & complete = impls - for < tag - t >:: complete ; // exposition only template < class ... Args > requires callable < decltype ( complete ), Index , state - t & , Rcvr & , set_value_t , Args ... > void set_value ( Args && ... args ) && noexcept { complete ( Index (), op -> state , op -> rcvr , set_value_t (), std :: forward < Args > ( args )...); } template < class Error > requires callable < decltype ( complete ), Index , state - t & , Rcvr & , set_error_t , Error > void set_error ( Error && err ) && noexcept { complete ( Index (), op -> state , op -> rcvr , set_error_t (), std :: forward < Error > ( err )); } void set_stopped () && noexcept requires callable < decltype ( complete ), Index , state - t & , Rcvr & , set_stopped_t > { complete ( Index (), op -> state , op -> rcvr , set_stopped_t ()); } auto get_env () const noexcept -> env - type < Index , Sndr , Rcvr > { return impls - for < tag - t >:: get - env ( Index (), op -> state , op -> rcvr ); } basic - state < Sndr , Rcvr >* op ; // exposition only }; constexpr auto connect - all = see below ; // exposition only template < class Sndr , class Rcvr > using connect - all - result = call - result - t < // exposition only decltype ( connect - all ), basic - state < Sndr , Rcvr >* , Sndr , indices - for < Sndr >> ; template < class Sndr , class Rcvr > requires valid - specialization < state - type , Sndr , Rcvr > && valid - specialization < connect - all - result , Sndr , Rcvr > struct basic - operation : basic - state < Sndr , Rcvr > { // exposition only using operation_state_concept = operation_state_t ; using tag - t = tag_of_t < Sndr > ; // exposition only connect - all - result < Sndr , Rcvr > inner - ops ; // exposition only basic - operation ( Sndr && sndr , Rcvr && rcvr ) noexcept ( see below ) // exposition only : basic - state < Sndr , Rcvr > ( std :: forward < Sndr > ( sndr ), std :: move ( rcvr )) , inner - ops ( connect - all ( this , std :: forward < Sndr > ( sndr ), indices - for < Sndr > ())) {} void start () & noexcept { auto & [... ops ] = inner - ops ; impls - for < tag - t >:: start ( this -> state , this -> rcvr , ops ...); } }; template < class Sndr , class Env > using completion - signatures - for = see below ; // exposition only template < class Tag , class Data , class ... Child > struct basic - sender : product - type < Tag , Data , Child ... > { // exposition only using sender_concept = sender_t ; using indices - for = index_sequence_for < Child ... > ; // exposition only decltype ( auto ) get_env () const noexcept { auto & [ _ , data , ... child ] = * this ; return impls - for < Tag >:: get - attrs ( data , child ...); } template < decays - to < basic - sender > Self , receiver Rcvr > auto connect ( this Self && self , Rcvr rcvr ) noexcept ( see below ) -> basic - operation < Self , Rcvr > { return { std :: forward < Self > ( self ), std :: move ( rcvr )}; } template < decays - to < basic - sender > Self , class Env > auto get_completion_signatures ( this Self && self , Env && env ) noexcept -> completion - signatures - for < Self , Env > { return {}; } }; } 
- 
           Remarks: The default template argument for the Data semiregular 
- 
           It is unspecified whether a specialization of basic - sender 
- 
           An expression of type basic - sender 
- 
           The expression in the noexcept basic - state is_nothrow_move_constructible_v < Rcvr > && nothrow - callable < decltype ( impls - for < tag_of_t < Sndr >>:: get - state ), Sndr , Rcvr &> 
- 
           The object connect - all [] < class Sndr , class Rcvr , size_t ... Is > ( basic - state < Sndr , Rcvr >* op , Sndr && sndr , index_sequence < Is ... > ) noexcept ( see below ) -> decltype ( auto ) { auto & [ _ , data , ... child ] = sndr ; return product - type { connect ( std :: forward_like < Sndr > ( child ), basic - receiver < Sndr , Rcvr , integral_constant < size_t , Is >> { op })...}; } - 
             Requires: The expression in the return 
- 
             Remarks: The expression in the noexcept trueif thereturn false.
 
- 
             
- 
           The expression in the noexcept basic - operation is_nothrow_constructible_v < basic - state < Self , Rcvr > , Self , Rcvr > && noexcept ( connect - all ( this , std :: forward < Sndr > ( sndr ), indices - for < Sndr > ())) 
- 
           The expression in the noexcept connect basic - sender is_nothrow_constructible_v < basic - operation < Self , Rcvr > , Self , Rcvr > 
- 
           The member default - impls :: get - attrs []( const auto & , const auto & ... child ) noexcept -> decltype ( auto ) { if constexpr ( sizeof ...( child ) == 1 ) return ( FWD - ENV ( get_env ( child )), ...); else return empty_env (); } 
- 
           The member default - impls :: get - env []( auto , auto & , const auto & rcvr ) noexcept -> decltype ( auto ) { return FWD - ENV ( get_env ( rcvr )); } 
- 
           The member default - impls :: get - state [] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) noexcept -> decltype ( auto ) { auto & [ _ , data , ... child ] = sndr ; return std :: forward_like < Sndr > ( data ); } 
- 
           The member default - impls :: start []( auto & , auto & , auto & ... ops ) noexcept -> void { ( execution :: start ( ops ), ...); } 
- 
           The member default - impls :: complete [] < class Index , class Rcvr , class Tag , class ... Args > ( Index , auto & state , Rcvr & rcvr , Tag , Args && ... args ) noexcept -> void requires callable < Tag , Rcvr , Args ... > { // Mandates: Index::value == 0 Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); } 
- 
           For a subexpression sndr Sndr decltype (( sndr )) rcvr Env sender_in < Sndr , Env > true.completion - signatures - for < Sndr , Env > completion_signatures sndr rcvr sender_in < Sndr , Env > false, the type denoted bycompletion - signatures - for < Sndr , Env > completion_signatures Recommended practice: When sender_in < Sndr , Env > false, implementations are encouraged to use the type denoted bycompletion - signatures - for < Sndr , Env > 
 
- 
           
- 
template < sender Sndr , queryable Env > constexpr auto write - env ( Sndr && sndr , Env && env ); // exposition only - 
           write - env rcvr queryable env get_env ( rcvr ) 
- 
           Let write - env - t 
- 
           Returns: make - sender ( write - env - t (), std :: forward < Env > ( env ), std :: forward < Sndr > ( sndr )) 
- 
           Remarks: The exposition-only class template impls - for write - env - t template <> struct impls - for < write - env - t > : default - impls { static constexpr auto get - env = []( auto , const auto & state , const auto & rcvr ) noexcept { return JOIN - ENV ( state , get_env ( rcvr )); }; }; 
 
- 
           
 
- 
         
34.9.2. Sender concepts [exec.snd.concepts]
- 
       The sender sender_in sender_to get_env connect namespace std :: execution { template < class Sigs > concept valid - completion - signatures = see below ; // exposition only template < class Sndr > concept is - sender = // exposition only derived_from < typename Sndr :: sender_concept , sender_t > ; template < class Sndr > concept enable - sender = // exposition only is - sender < Sndr > || is - awaitable < Sndr , env - promise < empty_env >> ; // [exec.awaitables] template < class Sndr > concept sender = bool ( enable - sender < remove_cvref_t < Sndr >> ) && // atomic constraint ([temp.constr.atomic]) requires ( const remove_cvref_t < Sndr >& sndr ) { { get_env ( sndr ) } -> queryable ; } && move_constructible < remove_cvref_t < Sndr >> && // senders are movable and constructible_from < remove_cvref_t < Sndr > , Sndr > ; // decay copyable template < class Sndr , class Env = empty_env > concept sender_in = sender < Sndr > && queryable < Env > && requires ( Sndr && sndr , Env && env ) { { get_completion_signatures ( std :: forward < Sndr > ( sndr ), std :: forward < Env > ( env )) } -> valid - completion - signatures ; }; template < class Sndr , class Rcvr > concept sender_to = sender_in < Sndr , env_of_t < Rcvr >> && receiver_of < Rcvr , completion_signatures_of_t < Sndr , env_of_t < Rcvr >>> && requires ( Sndr && sndr , Rcvr && rcvr ) { connect ( std :: forward < Sndr > ( sndr ), std :: forward < Rcvr > ( rcvr )); }; } 
- 
       Given a subexpression sndr Sndr decltype (( sndr )) rcvr Env Sndr Env completion_signatures completion_signatures_of_t < Sndr , Env > Sndr Env sender_in < Sndr , Env > sndr rcvr Sndr Env 
- 
       A type models the exposition-only concept valid - completion - signatures completion_signatures 
- 
       The exposition-only concepts sender - of sender - in - of namespace std :: execution { template < class ... As > using value - signature = set_value_t ( As ...); // exposition only template < class Sndr , class Env , class ... Values > concept sender - in - of = sender_in < Sndr , Env > && MATCHING - SIG ( // see [exec.general] set_value_t ( Values ...), value_types_of_t < Sndr , Env , value - signature , type_identity_t > ); template < class Sndr , class ... Values > concept sender - of = sender - in - of < Sndr , empty_env , Values ... > ; } 
- 
       Let sndr decltype (( sndr )) Sndr tag_of_t < Sndr > - 
         If the declaration auto && [ tag , data , ... children ] = sndr ; tag_of_t < Sndr > decltype ( auto ( tag )) 
- 
         Otherwise, tag_of_t < Sndr > 
 
- 
         
- 
       Let sender - for namespace std :: execution { template < class Sndr , class Tag > concept sender - for = sender < Sndr > && same_as < tag_of_t < Sndr > , Tag > ; } 
- 
       For a type T SET - VALUE - SIG ( T ) set_value_t () T void set_value_t ( T ) 
- 
       Library-provided sender types: - 
         Always expose an overload of a member connect 
- 
         Only expose an overload of a member connect copy_constructible 
 
- 
         
34.9.3. Awaitable helpers [exec.awaitables]
- 
       The sender concepts recognize awaitables as senders. For [exec], an awaitable is an expression that would be well-formed as the operand of a co_await 
- 
       For a subexpression c GET - AWAITER ( c , p ) c e p Promise await_transform operator co_await I have opened cwg#250 to give these transformations a term-of-art so we can more easily refer to it here. 
- 
       Let is - awaitable namespace std { template < class T > concept await - suspend - result = see below ; // exposition only template < class A , class Promise > concept is - awaiter = // exposition only requires ( A & a , coroutine_handle < Promise > h ) { a . await_ready () ? 1 : 0 ; { a . await_suspend ( h ) } -> await - suspend - result ; a . await_resume (); }; template < class C , class Promise > concept is - awaitable = requires ( C ( * fc )() noexcept , Promise & p ) { { GET - AWAITER ( fc (), p ) } -> is - awaiter < Promise > ; }; } await - suspend - result < T > trueif and only if one of the following istrue:- 
         T void 
- 
         T bool 
- 
         T coroutine_handle 
 
- 
         
- 
       For a subexpression c decltype (( c )) C p Promise await - result - type < C , Promise > decltype ( GET - AWAITER ( c , p ). await_resume ()) 
- 
       Let with - await - transform namespace std :: execution { template < class T , class Promise > concept has - as - awaitable = // exposition only requires ( T && t , Promise & p ) { { std :: forward < T > ( t ). as_awaitable ( p ) } -> is - awaitable < Promise &> ; }; template < class Derived > struct with - await - transform { template < class T > T && await_transform ( T && value ) noexcept { return std :: forward < T > ( value ); } template < has - as - awaitable < Derived > T > decltype ( auto ) await_transform ( T && value ) noexcept ( noexcept ( std :: forward < T > ( value ). as_awaitable ( declval < Derived &> ()))) { return std :: forward < T > ( value ). as_awaitable ( static_cast < Derived &> ( * this )); } }; } 
- 
       Let env - promise namespace std :: execution { template < class Env > struct env - promise : with - await - transform < env - promise < Env >> { unspecified get_return_object () noexcept ; unspecified initial_suspend () noexcept ; unspecified final_suspend () noexcept ; void unhandled_exception () noexcept ; void return_void () noexcept ; coroutine_handle <> unhandled_stopped () noexcept ; const Env & get_env () const noexcept ; }; } Specializations of env - promise 
34.9.4. execution :: default_domain 
namespace std :: execution { struct default_domain { template < sender Sndr , queryable ... Env > requires ( sizeof ...( Env ) <= 1 ) static constexpr sender decltype ( auto ) transform_sender ( Sndr && sndr , const Env & ... env ) noexcept ( see below ); template < sender Sndr , queryable Env > static constexpr queryable decltype ( auto ) transform_env ( Sndr && sndr , Env && env ) noexcept ; template < class Tag , sender Sndr , class ... Args > static constexpr decltype ( auto ) apply_sender ( Tag , Sndr && sndr , Args && ... args ) noexcept ( see below ); }; } 
34.9.4.1. Static members [exec.domain.default.statics]
template < sender Sndr , queryable ... Env > requires ( sizeof ...( Env ) <= 1 ) constexpr sender decltype ( auto ) transform_sender ( Sndr && sndr , const Env & ... env ) noexcept ( see below ); 
- 
       Let e tag_of_t < Sndr > (). transform_sender ( std :: forward < Sndr > ( sndr ), env ...) std :: forward < Sndr > ( sndr ) 
- 
       Returns: e 
- 
       Remarks: The exception specification is equivalent to noexcept ( e ) 
template < sender Sndr , queryable Env > constexpr queryable decltype ( auto ) transform_env ( Sndr && sndr , Env && env ) noexcept ; 
- 
       Let e tag_of_t < Sndr > (). transform_env ( std :: forward < Sndr > ( sndr ), std :: forward < Env > ( env )) static_cast < Env > ( std :: forward < Env > ( env )) 
- 
       Mandates: noexcept ( e ) true.
- 
       Returns: e 
template < class Tag , sender Sndr , class ... Args > constexpr decltype ( auto ) apply_sender ( Tag , Sndr && sndr , Args && ... args ) noexcept ( see below ); 
- 
       Let e Tag (). apply_sender ( std :: forward < Sndr > ( sndr ), std :: forward < Args > ( args )...) 
- 
       Constraints: e 
- 
       Returns: e 
- 
       Remarks: The exception specification is equivalent to noexcept ( e ) 
34.9.5. execution :: transform_sender 
namespace std :: execution { template < class Domain , sender Sndr , queryable ... Env > requires ( sizeof ...( Env ) <= 1 ) constexpr sender decltype ( auto ) transform_sender ( Domain dom , Sndr && sndr , const Env & ... env ) noexcept ( see below ); } 
- 
       Let transformed - sndr dom . transform_sender ( std :: forward < Sndr > ( sndr ), env ...) default_domain (). transform_sender ( std :: forward < Sndr > ( sndr ), env ...) final - sndr transformed - sndr transformed - sndr sndr transform_sender ( dom , transformed - sndr , env ...) 
- 
       Returns: final - sndr 
- 
       Remarks: The exception specification is equivalent to noexcept ( final - sndr ) 
34.9.6. execution :: transform_env 
namespace std :: execution { template < class Domain , sender Sndr , queryable Env > constexpr queryable decltype ( auto ) transform_env ( Domain dom , Sndr && sndr , Env && env ) noexcept ; } 
- 
       Let e dom . transform_env ( std :: forward < Sndr > ( sndr ), std :: forward < Env > ( env )) default_domain (). transform_env ( std :: forward < Sndr > ( sndr ), std :: forward < Env > ( env )) 
- 
       Mandates: noexcept ( e ) true.
- 
       Returns: e 
34.9.7. execution :: apply_sender 
namespace std :: execution { template < class Domain , class Tag , sender Sndr , class ... Args > constexpr decltype ( auto ) apply_sender ( Domain dom , Tag , Sndr && sndr , Args && ... args ) noexcept ( see below ); } 
- 
       Let e dom . apply_sender ( Tag (), std :: forward < Sndr > ( sndr ), std :: forward < Args > ( args )...) default_domain (). apply_sender ( Tag (), std :: forward < Sndr > ( sndr ), std :: forward < Args > ( args )...) 
- 
       Constraints: The expression e 
- 
       Returns: e 
- 
       Remarks: The exception specification is equivalent to noexcept ( e ) 
34.9.8. execution :: get_completion_signatures 
     - 
       get_completion_signatures sndr decltype (( sndr )) Sndr env decltype (( env )) Env new_sndr transform_sender ( decltype ( get - domain - late ( sndr , env )){}, sndr , env ) NewSndr decltype (( new_sndr )) get_completion_signatures ( sndr , env ) ( void ( sndr ), void ( env ), CS ()) void ( sndr ) void ( env ) CS - 
         decltype ( new_sndr . get_completion_signatures ( env )) 
- 
         Otherwise, remove_cvref_t < NewSndr >:: completion_signatures 
- 
         Otherwise, if is - awaitable < NewSndr , env - promise < Env >> true, then:completion_signatures < SET - VALUE - SIG ( await - result - type < NewSndr , env - promise < Env >> ), // see [exec.snd.concepts] set_error_t ( exception_ptr ), set_stopped_t () > 
- 
         Otherwise, CS 
 
- 
         
- 
       Let rcvr Rcvr receiver Sndr sender_in < Sndr , env_of_t < Rcvr >> true. LetSigs ... completion_signatures completion_signatures_of_t < Sndr , env_of_t < Rcvr >> CSO Sndr CSO ( rcvr , args ...) Sig Sigs ... MATCHING - SIG ( decayed - typeof < CSO > ( decltype ( args )...), Sig ) true([exec.general]).
34.9.9. execution :: connect 
     - 
       connect 
- 
       The name connect sndr rcvr Sndr decltype (( sndr )) Rcvr decltype (( rcvr )) new_sndr transform_sender ( decltype ( get - domain - late ( sndr , get_env ( rcvr ))){}, sndr , get_env ( rcvr )) DS DR decay_t < decltype (( new_sndr )) > decay_t < Rcvr > 
- 
       Let connect - awaitable - promise namespace std :: execution { struct connect - awaitable - promise : with - await - transform < connect - awaitable - promise > { connect - awaitable - promise ( DS & , DR & rcvr ) noexcept : rcvr ( rcvr ) {} suspend_always initial_suspend () noexcept { return {}; } [[ noreturn ]] suspend_always final_suspend () noexcept { terminate (); } [[ noreturn ]] void unhandled_exception () noexcept { terminate (); } [[ noreturn ]] void return_void () noexcept { terminate (); } coroutine_handle <> unhandled_stopped () noexcept { set_stopped ( std :: move ( rcvr )); return noop_coroutine (); } operation - state - task get_return_object () noexcept { return operation - state - task { coroutine_handle < connect - awaitable - promise >:: from_promise ( * this )}; } env_of_t < DR > get_env () const noexcept { return execution :: get_env ( rcvr ); } private : DR & rcvr ; // exposition only }; } 
- 
       Let operation - state - task namespace std :: execution { struct operation - state - task { using operation_state_concept = operation_state_t ; using promise_type = connect - awaitable - promise ; explicit operation - state - task ( coroutine_handle <> h ) noexcept : coro ( h ) {} operation - state - task ( operation - state - task && o ) noexcept : coro ( exchange ( o . coro , {})) {} ~ operation - state - task () { if ( coro ) coro . destroy (); } void start () & noexcept { coro . resume (); } private : coroutine_handle <> coro ; // exposition only }; } 
- 
       Let V await - result - type < DS , connect - awaitable - promise > Sigs completion_signatures < SET - VALUE - SIG ( V ), // see [exec.snd.concepts] set_error_t ( exception_ptr ), set_stopped_t () > and let connect - awaitable namespace std :: execution { template < class Fun , class ... Ts > auto suspend - complete ( Fun fun , Ts && ... as ) noexcept { // exposition only auto fn = [ & , fun ]() noexcept { fun ( std :: forward < Ts > ( as )...); }; struct awaiter { decltype ( fn ) fn ; static constexpr bool await_ready () noexcept { return false; } void await_suspend ( coroutine_handle <> ) noexcept { fn (); } [[ noreturn ]] void await_resume () noexcept { unreachable (); } }; return awaiter { fn }; } operation - state - task connect - awaitable ( DS sndr , DR rcvr ) requires receiver_of < DR , Sigs > { exception_ptr ep ; try { if constexpr ( same_as < V , void > ) { co_await std :: move ( sndr ); co_await suspend - complete ( set_value , std :: move ( rcvr )); } else { co_await suspend - complete ( set_value , std :: move ( rcvr ), co_await std :: move ( sndr )); } } catch (...) { ep = current_exception (); } co_await suspend - complete ( set_error , std :: move ( rcvr ), std :: move ( ep )); } } 
- 
       The expression connect ( sndr , rcvr ) - 
         new_sndr . connect ( rcvr ) - 
           Mandates: The type of the expression above satisfies operation_state 
 
- 
           
- 
         Otherwise, connect - awaitable ( new_sndr , rcvr ) 
- 
         Mandates: sender < Sndr > && receiver < Rcvr > true.
 
- 
         
34.9.10. Sender factories [exec.factories]
34.9.10.1. execution :: schedule 
     - 
       schedule 
- 
       The name schedule sch schedule ( sch ) sch . schedule () - 
         If the expression get_completion_scheduler < set_value_t > ( get_env ( sch . schedule ())) == sch false, the behavior of callingschedule ( sch ) 
- 
         Mandates: The type of sch . schedule () sender 
 
- 
         
34.9.10.2. execution :: just execution :: just_error execution :: just_stopped 
     - 
       just just_error just_stopped 
- 
       The names just just_error just_stopped just - cpo just just_error just_stopped ts Ts decltype (( ts )) just - cpo ( ts ...) - 
         ( movable - value < Ts > && ...) false, or
- 
         just - cpo just_error sizeof ...( ts ) == 1 false, or
- 
         just - cpo just_stopped sizeof ...( ts ) == 0 false;
 Otherwise, it is expression-equivalent to make - sender ( just - cpo , product - type { ts ...}) 
- 
         
- 
       For just just_error just_stopped set - cpo set_value set_error set_stopped impls - for just - cpo namespace std :: execution { template <> struct impls - for < decayed - typeof < just - cpo >> : default - impls { static constexpr auto start = []( auto & state , auto & rcvr ) noexcept -> void { auto & [... ts ] = state ; set - cpo ( std :: move ( rcvr ), std :: move ( ts )...); }; }; } 
34.9.10.3. execution :: read_env 
     - 
       read_env 
- 
       read_env q read_env ( q ) make - sender ( read_env , q ) 
- 
       The exposition-only class template impls - for read_env namespace std :: execution { template <> struct impls - for < decayed - typeof < read_env >> : default - impls { static constexpr auto start = []( auto query , auto & rcvr ) noexcept -> void { TRY - SET - VALUE ( rcvr , query ( get_env ( rcvr ))); }; }; } 
34.9.11. Sender adaptors [exec.adapt]
34.9.11.1. General [exec.adapt.general]
- 
       [exec.adapt] specifies a set of sender adaptors. 
- 
       The bitwise inclusive OR operator is overloaded for the purpose of creating sender chains. The adaptors also support function call syntax with equivalent semantics. 
- 
       Unless otherwise specified: - 
         A sender adaptor is prohibited from causing observable effects, apart from moving and copying its arguments, before the returned sender is connected with a receiver using connect start 
- 
         A parent sender ([async.ops]) with a single child sender sndr FWD - ENV ( get_env ( sndr )) 
- 
         A parent sender with more than one child sender has an associated attributes object equal to empty_env {} 
- 
         When a parent sender is connected to a receiver rcvr FWD - ENV ( get_env ( rcvr )) 
 These requirements apply to any function that is selected by the implementation of the sender adaptor. 
- 
         
- 
       If a sender returned from a sender adaptor specified in [exec.adapt] is specified to include set_error_t ( Err ) decay_t < Err > exception_ptr exception_ptr exception_ptr 
34.9.11.2. Sender adaptor closure objects [exec.adapt.objects]
- 
       A pipeable sender adaptor closure object is a function object that accepts one or more sender sender c sndr decltype (( sndr )) sender sender c ( sndr ) sndr | c Given an additional pipeable sender adaptor closure object d c | d e e - 
         Its target object is an object d2 decltype ( auto ( d )) d 
- 
         It has one bound argument entity, an object c2 decltype ( auto ( c )) c 
- 
         Its call pattern is d2 ( c2 ( arg )) arg e 
 
- 
         
The expression 
- 
       An object t T T derived_from < sender_adaptor_closure < T >> T sender_adaptor_closure < U > U T sender 
- 
       The template parameter D sender_adaptor_closure cv D | D derived_from < sender_adaptor_closure < D >> cv D | operator | 
- 
       A pipeable sender adaptor object is a customization point object that accepts a sender sender 
- 
       If a pipeable sender adaptor object accepts only one argument, then it is a pipeable sender adaptor closure object. 
- 
       If a pipeable sender adaptor object adaptor sndr decltype (( sndr )) sender args ... adaptor ( sndr , args ...) BoundArgs decltype ( auto ( args ))... adaptor ( args ...) f - 
         Its target object is a copy of adaptor 
- 
         Its bound argument entities bound_args BoundArgs ... std :: forward < decltype (( args )) > ( args )... 
- 
         Its call pattern is adaptor ( rcvr , bound_args ...) rcvr f 
 The expression adaptor ( args ...) 
- 
         
34.9.11.3. execution :: starts_on 
     - 
       starts_on 
- 
       The name starts_on sch sndr decltype (( sch )) scheduler decltype (( sndr )) sender starts_on ( sch , sndr ) 
- 
       Otherwise, the expression starts_on ( sch , sndr ) transform_sender ( query - or - default ( get_domain , sch , default_domain ()), make - sender ( starts_on , sch , sndr )) except that sch 
- 
       Let out_sndr env OutSndr decltype (( out_sndr )) sender - for < OutSndr , starts_on_t > false, then the expressionsstarts_on . transform_env ( out_sndr , env ) starts_on . transform_sender ( out_sndr , env ) - 
         starts_on . transform_env ( out_sndr , env ) auto && [ _ , sch , _ ] = out_sndr ; return JOIN - ENV ( SCHED - ENV ( sch ), FWD - ENV ( env )); 
- 
         starts_on . transform_sender ( out_sndr , env ) auto && [ _ , sch , sndr ] = out_sndr ; return let_value ( schedule ( sch ), [ sndr = std :: forward_like < OutSndr > ( sndr )]() mutable noexcept ( is_nothrow_move_constructible_v < decay_t > ) { return std :: move ( sndr ); }); 
 
- 
         
- 
       Let out_sndr starts_on ( sch , sndr ) OutSndr decltype (( out_sndr )) out_rcvr Env sender_in < OutSndr , Env > true. Letop out_sndr out_rcvr start ( op ) sndr sch sch out_rcvr 
34.9.11.4. execution :: continues_on 
     - 
       continues_on 
- 
       The name continues_on sch sndr decltype (( sch )) scheduler decltype (( sndr )) sender continues_on ( sndr , sch ) 
- 
       Otherwise, the expression continues_on ( sndr , sch ) transform_sender ( get - domain - early ( sndr ), make - sender ( continues_on , sch , sndr )) except that sndr 
- 
       The exposition-only class template impls - for continues_on_t namespace std :: execution { template <> struct impls - for < continues_on_t > : default - impls { static constexpr auto get_attrs = []( const auto & data , const auto & child ) noexcept -> decltype ( auto ) { return JOIN - ENV ( SCHED - ATTRS ( data ), FWD - ENV ( get_env ( child ))); }; }; } 
- 
       Let sndr env Sndr decltype (( sndr )) sender - for < Sndr , continues_on_t > false, then the expressioncontinues_on . transform_sender ( sndr , env ) auto [ _ , data , child ] = sndr ; return schedule_from ( std :: move ( data ), std :: move ( child )); This causes the continues_on ( sndr , sch ) schedule_from ( sch , sndr ) continues_on 
- 
       Let out_sndr continues_on ( sndr , sch ) OutSndr decltype (( out_sndr )) out_rcvr Env sender_in < OutSndr , Env > true. Letop out_sndr out_rcvr start ( op ) sndr out_rcvr sch sch out_rcvr 
34.9.11.5. execution :: schedule_from 
     - 
       schedule_from schedule_from continues_on 
- 
       The name schedule_from sch sndr Sch decltype (( sch )) Sndr decltype (( sndr )) Sch scheduler Sndr sender schedule_from ( sch , sndr ) 
- 
       Otherwise, the expression schedule_from ( sch , sndr ) transform_sender ( query - or - default ( get_domain , sch , default_domain ()), make - sender ( schedule_from , sch , sndr )) except that sch 
- 
       The exposition-only class template impls - for schedule_from_t namespace std :: execution { template <> struct impls - for < schedule_from_t > : default - impls { static constexpr auto get - attrs = see below ; static constexpr auto get - state = see below ; static constexpr auto complete = see below ; }; } - 
         The member impls - for < schedule_from_t >:: get - attrs []( const auto & data , const auto & child ) noexcept -> decltype ( auto ) { return JOIN - ENV ( SCHED - ATTRS ( data ), FWD - ENV ( get_env ( child ))); } 
- 
         The member impls - for < schedule_from_t >:: get - state [] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) noexcept ( see below ) requires sender_in < child - type < Sndr > , env_of_t < Rcvr >> { auto & [ _ , sch , child ] = sndr ; using sched_t = decltype ( auto ( sch )); using variant_t = see below ; using receiver_t = see below ; using operation_t = connect_result_t < schedule_result_t < sched_t > , receiver_t > ; constexpr bool nothrow = noexcept ( connect ( schedule ( sch ), receiver_t { nullptr })); struct state - type { Rcvr & rcvr ; // exposition only variant_t async - result ; // exposition only operation_t op - state ; // exposition only explicit state - type ( sched_t sch , Rcvr & rcvr ) noexcept ( nothrow ) : rcvr ( rcvr ), op - state ( connect ( schedule ( sch ), receiver_t { this })) {} }; return state - type { sch , rcvr }; } - 
           Objects of the local class state - type 
- 
           Let Sigs completion_signatures completion_signatures_of_t < child - type < Sndr > , env_of_t < Rcvr >> as - tuple Tag ( Args ...) tuple decayed - tuple < Tag , Args ... > variant_t variant < monostate , as - tuple < Sigs > ... > 
- 
           receiver_t namespace std :: execution { struct receiver - type { using receiver_concept = receiver_t ; state - type * state ; // exposition only void set_value () && noexcept { visit ( [ this ] < class Tuple > ( Tuple & result ) noexcept -> void { if constexpr ( ! same_as < monostate , Tuple > ) { auto & [ tag , ... args ] = result ; tag ( std :: move ( state -> rcvr ), std :: move ( args )...); } }, state -> async - result ); } template < class Error > void set_error ( Error && err ) && noexcept { execution :: set_error ( std :: move ( state -> rcvr ), std :: forward < Error > ( err )); } void set_stopped () && noexcept { execution :: set_stopped ( std :: move ( state -> rcvr )); } decltype ( auto ) get_env () const noexcept { return FWD - ENV ( execution :: get_env ( state -> rcvr )); } }; } 
- 
           The expression in the noexcept trueif the construction of the returnedstate - type false.
 
- 
           
- 
         The member impls - for < schedule_from_t >:: complete [] < class Tag , class ... Args > ( auto , auto & state , auto & rcvr , Tag , Args && ... args ) noexcept -> void { using result_t = decayed - tuple < Tag , Args ... > ; constexpr bool nothrow = is_nothrow_constructible_v < result_t , Tag , Args ... > ; TRY - EVAL ( rcvr , [ & ]() noexcept ( nothrow ) { state . async - result . template emplace < result_t > ( Tag (), std :: forward < Args > ( args )...); }()); if ( state . async - result . valueless_by_exception ()) return ; if ( state . async - result . index () == 0 ) return ; start ( state . op - state ); }; 
 
- 
         
- 
       Let out_sndr schedule_from ( sch , sndr ) OutSndr decltype (( out_sndr )) out_rcvr Env sender_in < OutSndr , Env > true. Letop out_sndr out_rcvr start ( op ) sndr out_rcvr sch sch out_rcvr 
34.9.11.6. execution :: on 
     - 
       The on - 
         on ( sch , sndr ) sndr sch sndr on 
- 
         on ( sndr , sch , closure ) sndr sch closure sndr 
 
- 
         
- 
       The name on sch sndr on ( sch , sndr ) - 
         decltype (( sch )) scheduler 
- 
         decltype (( sndr )) sender sndr 
- 
         decltype (( sndr )) sender sndr 
 
- 
         
- 
       Otherwise, if decltype (( sndr )) sender on ( sch , sndr ) transform_sender ( query - or - default ( get_domain , sch , default_domain ()), make - sender ( on , sch , sndr )) except that sch 
- 
       For subexpressions sndr sch closure decltype (( sch )) scheduler decltype (( sndr )) sender closure on ( sndr , sch , closure ) transform_sender ( get - domain - early ( sndr ), make - sender ( on , product - type { sch , closure }, sndr )) except that sndr 
- 
       Let out_sndr env OutSndr decltype (( out_sndr )) Env decltype (( env )) sender - for < OutSndr , on_t > false, then the expressionson . transform_env ( out_sndr , env ) on . transform_sender ( out_sndr , env ) - 
         Let not - a - scheduler not - a - sender struct not - a - sender { using sender_concept = sender_t ; auto get_completion_signatures ( auto && ) const { return see below ; } }; where the member function get_completion_signatures completion_signatures 
- 
         The expression on . transform_env ( out_sndr , env ) auto && [ _ , data , _ ] = out_sndr ; if constexpr ( scheduler < decltype ( data ) > ) { return JOIN - ENV ( SCHED - ENV ( std :: forward_like < OutSndr > ( data )), FWD - ENV ( std :: forward < Env > ( env ))); } else { return std :: forward < Env > ( env ); } 
- 
         The expression on . transform_sender ( out_sndr , env ) auto && [ _ , data , child ] = out_sndr ; if constexpr ( scheduler < decltype ( data ) > ) { auto orig_sch = query - with - default ( get_scheduler , env , not - a - scheduler ()); if constexpr ( same_as < decltype ( orig_sch ), not - a - scheduler > ) { return not - a - sender {}; } else { return continues_on ( starts_on ( std :: forward_like < OutSndr > ( data ), std :: forward_like < OutSndr > ( child )), std :: move ( orig_sch )); } } else { auto & [ sch , closure ] = data ; auto orig_sch = query - with - default ( get_completion_scheduler < set_value_t > , get_env ( child ), query - with - default ( get_scheduler , env , not - a - scheduler ())); if constexpr ( same_as < decltype ( orig_sch ), not - a - scheduler > ) { return not - a - sender {}; } else { return write - env ( continues_on ( std :: forward_like < OutSndr > ( closure )( continues_on ( write - env ( std :: forward_like < OutSndr > ( child ), SCHED - ENV ( orig_sch )), sch )), orig_sch ), SCHED - ENV ( sch )); } } 
- 
         Recommended practice: Implementations should use the return type of not - a - sender :: get_completion_signatures on 
 
- 
         
- 
       Let out_sndr on ( sch , sndr ) OutSndr decltype (( out_sndr )) out_rcvr Env sender_in < OutSndr , Env > true. Letop out_sndr out_rcvr start ( op ) - 
         Remember the current scheduler, get_scheduler ( get_env ( rcvr )) 
- 
         Start sndr sch 
- 
         Upon sndr 
- 
         Forward sndr out_rcvr 
 If any scheduling operation fails, an error completion on out_rcvr 
- 
         
- 
       Let out_sndr on ( sndr , sch , closure ) OutSndr decltype (( out_sndr )) out_rcvr Env sender_in < OutSndr , Env > true. Letop out_sndr out_rcvr start ( op ) - 
         Remember the current scheduler, which is the first of the following expressions that is well-formed: - 
           get_completion_scheduler < set_value_t > ( get_env ( sndr )) 
- 
           get_scheduler ( get_env ( rcvr )) 
 
- 
           
- 
         Start sndr 
- 
         Upon sndr sch 
- 
         Forward sndr closure ( S ) S sndr 
- 
         Upon completion of the operation started in step 4, transfer execution back to the execution resource associated with the scheduler remembered in step 1 and forward the operation’s async result to out_rcvr 
 If any scheduling operation fails, an error completion on out_rcvr 
- 
         
34.9.11.7. execution :: then execution :: upon_error execution :: upon_stopped 
     - 
       then upon_error upon_stopped 
- 
       The names then upon_error upon_stopped then - cpo then upon_error upon_stopped sndr f decltype (( sndr )) sender decltype (( f )) movable - value then - cpo ( sndr , f ) 
- 
       Otherwise, the expression then - cpo ( sndr , f ) transform_sender ( get - domain - early ( sndr ), make - sender ( then - cpo , f , sndr )) except that sndr 
- 
       For then upon_error upon_stopped set - cpo set_value set_error set_stopped impls - for then - cpo namespace std :: execution { template <> struct impls - for < decayed - typeof < then - cpo >> : default - impls { static constexpr auto complete = [] < class Tag , class ... Args > ( auto , auto & fn , auto & rcvr , Tag , Args && ... args ) noexcept -> void { if constexpr ( same_as < Tag , decayed - typeof < set - cpo >> ) { TRY - SET - VALUE ( rcvr , invoke ( std :: move ( fn ), std :: forward < Args > ( args )...)); } else { Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); } }; }; } 
- 
       The expression then - cpo ( sndr , f ) out_sndr - 
         Invokes f sndr then upon_error upon_stopped f out_sndr 
- 
         Forwards all other completion operations unchanged. 
 
- 
         
34.9.11.8. execution :: let_value execution :: let_error execution :: let_stopped 
     - 
       let_value let_error let_stopped 
- 
       For let_value let_error let_stopped set - cpo set_value set_error set_stopped let - cpo let_value let_error let_stopped sndr let - env ( sndr ) - 
         SCHED - ENV ( get_completion_scheduler < decayed - typeof < set - cpo >> ( get_env ( sndr ))) 
- 
         MAKE - ENV ( get_domain , get_domain ( get_env ( sndr ))) 
- 
         ( void ( sndr ), empty_env {}) 
 
- 
         
- 
       The names let_value let_error let_stopped sndr f F f decltype (( sndr )) sender decltype (( f )) movable - value let - cpo ( sndr , f ) F invocable let_stopped ( sndr , f ) 
- 
       Otherwise, the expression let - cpo ( sndr , f ) transform_sender ( get - domain - early ( sndr ), make - sender ( let - cpo , f , sndr )) except that sndr 
- 
       The exposition-only class template impls - for let - cpo namespace std :: execution { template < class State , class Rcvr , class ... Args > void let - bind ( State & state , Rcvr & rcvr , Args && ... args ); // exposition only template <> struct impls - for < decayed - typeof < let - cpo >> : default - impls { static constexpr auto get - state = see below ; static constexpr auto complete = see below ; }; } - 
         Let receiver2 namespace std :: execution { template < class Rcvr , class Env > struct receiver2 { using receiver_concept = receiver_t ; template < class ... Args > void set_value ( Args && ... args ) && noexcept { execution :: set_value ( std :: move ( rcvr ), std :: forward < Args > ( args )...); } template < class Error > void set_error ( Error && err ) && noexcept { execution :: set_error ( std :: move ( rcvr ), std :: forward < Error > ( err )); } void set_stopped () && noexcept { execution :: set_stopped ( std :: move ( rcvr )); } decltype ( auto ) get_env () const noexcept { return JOIN - ENV ( env , FWD - ENV ( execution :: get_env ( rcvr ))); } Rcvr & rcvr ; // exposition only Env env ; // exposition only }; } 
- 
         impls - for < decayed - typeof < let - cpo >>:: get - state [] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) requires see below { auto & [ _ , fn , child ] = sndr ; using fn_t = decay_t < decltype ( fn ) > ; using env_t = decltype ( let - env ( child )); using args_variant_t = see below ; using ops2_variant_t = see below ; struct state - type { fn_t fn ; // exposition only env_t env ; // exposition only args_variant_t args ; // exposition only ops2_variant_t ops2 ; // exposition only }; return state - type { std :: forward_like < Sndr > ( fn ), let - env ( child ), {}, {}}; } - 
           Let Sigs completion_signatures completion_signatures_of_t < child - type < Sndr > , env_of_t < Rcvr >> LetSigs Sigs decayed - typeof < set - cpo > as - tuple as - tuple < Tag ( Args ...) > decayed - tuple < Args ... > args_variant_t variant < monostate , as - tuple < LetSigs > ... > 
- 
           Given a type Tag Args as - sndr2 as - sndr2 < Tag ( Args ...) > call - result - t < Fn , decay_t < Args >& ... > ops2_variant_t variant < monostate , connect_result_t < as - sndr2 < LetSigs > , receiver2 < Rcvr , Env >> ... > 
- 
           The requires-clause constraining the above lambda is satisfied if and only if the types args_variant_t ops2_variant_t 
 
- 
           
- 
         The exposition-only function template let - bind using args_t = decayed - tuple < Args ... > ; auto mkop2 = [ & ] { return connect ( apply ( std :: move ( state . fn ), state . args . template emplace < args_t > ( std :: forward < Args > ( args )...)), receiver2 { rcvr , std :: move ( state . env )}); }; start ( state . ops2 . template emplace < decltype ( mkop2 ()) > ( emplace - from { mkop2 })); 
- 
         impls - for < decayed - typeof < let - cpo >>:: complete [] < class Tag , class ... Args > ( auto , auto & state , auto & rcvr , Tag , Args && ... args ) noexcept -> void { if constexpr ( same_as < Tag , decayed - typeof < set - cpo >> ) { TRY - EVAL ( rcvr , let - bind ( state , rcvr , std :: forward < Args > ( args )...)); } else { Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); } } 
 
- 
         
- 
       Let sndr env Sndr decltype (( sndr )) sender - for < Sndr , decayed - typeof < let - cpo >> false, then the expressionlet - cpo . transform_env ( sndr , env ) JOIN - ENV ( let - env ( sndr ), FWD - ENV ( env )) 
- 
       Let the subexpression out_sndr let - cpo ( sndr , f ) rcvr connect ( out_sndr , rcvr ) connect ( out_sndr , rcvr ) - 
         invokes f set - cpo sndr 
- 
         makes its completion dependent on the completion of a sender returned by f 
- 
         propagates the other completion operations sent by sndr 
 
- 
         
34.9.11.9. execution :: bulk 
     - 
       bulk 
- 
       The name bulk sndr shape f Shape decltype ( auto ( shape )) decltype (( sndr )) sender Shape integral decltype (( f )) movable - value bulk ( sndr , shape , f ) 
- 
       Otherwise, the expression bulk ( sndr , shape , f ) transform_sender ( get - domain - early ( sndr ), make - sender ( bulk , product - type { shape , f }, sndr )) except that sndr 
- 
       The exposition-only class template impls - for bulk_t namespace std :: execution { template <> struct impls - for < bulk_t > : default - impls { static constexpr auto complete = see below ; }; } - 
         The member impls - for < bulk_t >:: complete [] < class Index , class State , class Rcvr , class Tag , class ... Args > ( Index , State & state , Rcvr & rcvr , Tag , Args && ... args ) noexcept -> void requires see below { if constexpr ( same_as < Tag , set_value_t > ) { auto & [ shape , f ] = state ; constexpr bool nothrow = noexcept ( f ( auto ( shape ), args ...)); TRY - EVAL ( rcvr , [ & ]() noexcept ( nothrow ) { for ( decltype ( auto ( shape )) i = 0 ; i < shape ; ++ i ) { f ( auto ( i ), args ...); } Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); }()); } else { Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); } } - 
           The expression in the requires-clause of the lambda above is trueif and only ifTag set_value_t f ( auto ( shape ), args ...) 
 
- 
           
 
- 
         
- 
       Let the subexpression out_sndr bulk ( sndr , shape , f ) rcvr connect ( out_sndr , rcvr ) connect ( out_sndr , rcvr ) - 
         on a value completion operation, invokes f ( i , args ...) i Shape 0 shape args 
- 
         propagates all completion operations sent by sndr 
 
- 
         
34.9.11.10. execution :: split 
     - 
       split 
- 
       Let split - env env get_stop_token ( env ) inplace_stop_token 
- 
       The name split sndr Sndr decltype (( sndr )) sender_in < Sndr , split - env > false,split ( sndr ) 
- 
       Otherwise, the expression split ( sndr ) transform_sender ( get - domain - early ( sndr ), make - sender ( split , {}, sndr )) except that sndr - 
         The default implementation of transform_sender 
 
- 
         
- 
       Let local - state namespace std :: execution { struct local - state - base { // exposition only virtual ~ local - state - base () = default ; virtual void notify () noexcept = 0 ; // exposition only }; template < class Sndr , class Rcvr > struct local - state : local - state - base { // exposition only using on - stop - callback = // exposition only stop_callback_of_t < stop_token_of_t < env_of_t < Rcvr >> , on - stop - request > ; local - state ( Sndr && sndr , Rcvr & rcvr ) noexcept ; ~ local - state (); void notify () noexcept override ; private : optional < on - stop - callback > on_stop ; // exposition only shared - state < Sndr >* sh_state ; // exposition only Rcvr * rcvr ; // exposition only }; } - 
local - state ( Sndr && sndr , Rcvr & rcvr ) noexcept ; - 
           Effects: Equivalent to: auto & [ _ , data , _ ] = sndr ; this -> sh_state = data . sh_state . get (); this -> sh_state -> inc - ref (); this -> rcvr = addressof ( rcvr ); 
 
- 
           
- 
~ local - state (); - 
           Effects: Equivalent to: sh_state -> dec - ref (); 
 
- 
           
- 
void notify () noexcept override ; - 
           Effects: Equivalent to: on_stop . reset (); visit ( [ this ]( const auto & tupl ) noexcept -> void { apply ( [ this ]( auto tag , const auto & ... args ) noexcept -> void { tag ( std :: move ( * rcvr ), args ...); }, tupl ); }, sh_state -> result ); 
 
- 
           
 
- 
- 
       Let split - receiver namespace std :: execution { template < class Sndr > struct split - receiver { using receiver_concept = receiver_t ; template < class Tag , class ... Args > void complete ( Tag , Args && ... args ) noexcept { // exposition only using tuple_t = decayed - tuple < Tag , Args ... > ; try { sh_state -> result . template emplace < tuple_t > ( Tag (), std :: forward < Args > ( args )...); } catch (...) { using tuple_t = tuple < set_error_t , exception_ptr > ; sh_state -> result . template emplace < tuple_t > ( set_error , current_exception ()); } sh_state -> notify (); } template < class ... Args > void set_value ( Args && ... args ) && noexcept { complete ( execution :: set_value , std :: forward < Args > ( args )...); } template < class Error > void set_error ( Error && err ) && noexcept { complete ( execution :: set_error , std :: forward < Error > ( err )); } void set_stopped () && noexcept { complete ( execution :: set_stopped ); } struct env { // exposition only shared - state < Sndr >* sh - state ; // exposition only inplace_stop_token query ( get_stop_token_t ) const noexcept { return sh - state -> stop_src . get_token (); } }; env get_env () const noexcept { return env { sh_state }; } shared - state < Sndr >* sh_state ; // exposition only }; } 
- 
       Let shared - state namespace std :: execution { template < class Sndr > struct shared - state { using variant - type = see below ; // exposition only using state - list - type = see below ; // exposition only explicit shared - state ( Sndr && sndr ); void start - op () noexcept ; // exposition only void notify () noexcept ; // exposition only void inc - ref () noexcept ; // exposition only void dec - ref () noexcept ; // exposition only inplace_stop_source stop_src {}; // exposition only variant - type result {}; // exposition only state - list - type waiting_states ; // exposition only atomic < bool > completed { false}; // exposition only atomic < size_t > ref_count { 1 }; // exposition only connect_result_t < Sndr , split - receiver < Sndr >> op_state ; // exposition only }; } - 
         Let Sigs completion_signatures completion_signatures_of_t < Sndr > Tag Args as - tuple as - tuple < Tag ( Args ...) > decayed - tuple < Tag , Args ... > variant - type variant < tuple < set_stopped_t > , tuple < set_error_t , exception_ptr > , as - tuple < Sigs > ... > 
- 
         Let state - list - type local - state - base 
- 
explicit shared - state ( Sndr && sndr ); - 
           Effects: Initializes op_state connect ( std :: forward < Sndr > ( sndr ), split - receiver { this }) 
- 
           Postcondition: waiting_states completed false.
 
- 
           
- 
void start - op () noexcept ; - 
           Effects: Calls inc - ref () stop_src . stop_requested () true, callsnotify () start ( op_state ) 
 
- 
           
- 
void notify () noexcept ; - 
           Effects: Atomically does the following: - 
             Sets completed true, and
- 
             Exchanges waiting_states prior_states 
 Then, for each pointer p prior_states p -> notify () dec - ref () 
- 
             
 
- 
           
- 
void inc - ref () noexcept ; - 
           Effects: Increments ref_count 
 
- 
           
- 
void dec - ref () noexcept ; - 
           Effects: Decrements ref_count ref_count 0 delete this 
- 
           Synchronization: If dec - ref () ref_count 0 dec - ref () ref_count 0 
 
- 
           
 
- 
         
- 
       Let split - impl - tag sndr split . transform_sender ( sndr ) auto && [ tag , _ , child ] = sndr ; auto * sh_state = new shared - state { std :: forward_like < decltype (( sndr )) > ( child )}; return make - sender ( split - impl - tag (), shared - wrapper { sh_state , tag }); where shared - wrapper shared - state sh_state shared - wrapper copyable sh_state -> inc - ref () sh_state sh_state -> dec - ref () 
- 
       The exposition-only class template impls - for split - impl - tag namespace std :: execution { template <> struct impls - for < split - impl - tag > : default - impls { static constexpr auto get - state = see below ; static constexpr auto start = see below ; }; } - 
         The member impls - for < split - impl - tag >:: get - state [] < class Sndr > ( Sndr && sndr , auto & rcvr ) noexcept { return local - state { std :: forward < Sndr > ( sndr ), rcvr }; } 
- 
         The member impls - for < split - impl - tag >:: start template < class Sndr , class Rcvr > void operator ()( local - state < Sndr , Rcvr >& state , Rcvr & rcvr ) const noexcept ; - 
           Effects: If state . sh_state -> completed true, callsstate . notify () - 
             Calls: state . on_stop . emplace ( get_stop_token ( get_env ( rcvr )), on - stop - request { state . sh_state -> stop_src }); 
- 
             Then atomically does the following: - 
               Reads the value c state . sh_state -> completed 
- 
               Inserts addressof ( state ) state . sh_state -> waiting_states c false.
 
- 
               
- 
             If c true, callsstate . notify () 
- 
             Otherwise, if addressof ( state ) state . sh_state -> waiting_states state . sh_state -> start - op () 
 
- 
             
 
- 
           
 
- 
         
34.9.11.11. execution :: when_all 
     - 
       when_all when_all_with_variant when_all when_all_with_variant ( sndrs ...) when_all ( into_variant ( sndrs )...) sndrs sender 
- 
       The names when_all when_all_with_variant sndrs Sndrs decltype (( sndrs ))... CD common_type_t < decltype ( get - domain - early ( sndrs ))... > when_all ( sndrs ...) when_all_with_variant ( sndrs ...) - 
         sizeof ...( sndrs ) 
- 
         ( sender < Sndrs > && ...) false, or
- 
         CD 
 
- 
         
- 
       The expression when_all ( sndrs ...) transform_sender ( CD (), make - sender ( when_all , {}, sndrs ...)) 
- 
       The exposition-only class template impls - for when_all_t namespace std :: execution { template <> struct impls - for < when_all_t > : default - impls { static constexpr auto get - attrs = see below ; static constexpr auto get - env = see below ; static constexpr auto get - state = see below ; static constexpr auto start = see below ; static constexpr auto complete = see below ; }; } - 
         The member impls - for < when_all_t >:: get - attrs []( auto && , auto && ... child ) noexcept { if constexpr ( same_as < CD , default_domain > ) { return empty_env (); } else { return MAKE - ENV ( get_domain , CD ()); } } 
- 
         The member impls - for < when_all_t >:: get - env [] < class State , class Rcvr > ( auto && , State & state , const Receiver & rcvr ) noexcept { return JOIN - ENV ( MAKE - ENV ( get_stop_token , state . stop_src . get_token ()), get_env ( rcvr )); } 
- 
         The member impls - for < when_all_t >:: get - state [] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) noexcept ( e ) -> decltype ( e ) { return e ; } where e std :: forward < Sndr > ( sndr ). apply ( make - state < Rcvr > ()) and where make - state template < class Sndr , class Env > concept max -1 - sender - in = sender_in < Sndr , Env > && // exposition only ( tuple_size_v < value_types_of_t < Sndr , Env , tuple , tuple >> <= 1 ); enum class disposition { started , error , stopped }; // exposition only template < class Rcvr > struct make - state { template < max -1 - sender - in < env_of_t < Rcvr >> ... Sndrs > auto operator ()( auto , auto , Sndrs && ... sndrs ) const { using values_tuple = see below ; using errors_variant = see below ; using stop_callback = stop_callback_of_t < stop_token_of_t < env_of_t < Rcvr >> , on - stop - request > ; struct state - type { void arrive ( Rcvr & rcvr ) noexcept { if ( 0 == -- count ) { complete ( rcvr ); } } void complete ( Rcvr & rcvr ) noexcept ; // see below atomic < size_t > count { sizeof ...( sndrs )}; // exposition only inplace_stop_source stop_src {}; // exposition only atomic < disposition > disp { disposition :: started }; // exposition only errors_variant errors {}; // exposition only values_tuple values {}; // exposition only optional < stop_callback > on_stop { nullopt }; // exposition only }; return state - type {}; } }; - 
           Let copy-fail be exception_ptr none - such none - such 
- 
           The alias values_tuple tuple < value_types_of_t < Sndrs , env_of_t < Rcvr > , decayed - tuple , optional > ... > tuple <> 
- 
           The alias errors_variant variant < none - such , copy - fail , Es ... > Es 
- 
           The member void state :: complete ( Rcvr & rcvr ) noexcept - 
             If disp disposition :: started auto tie = [] < class ... T > ( tuple < T ... >& t ) noexcept { return tuple < T & ... > ( t ); }; auto set = [ & ]( auto & ... t ) noexcept { set_value ( std :: move ( rcvr ), std :: move ( t )...); }; on_stop . reset (); apply ( [ & ]( auto & ... opts ) noexcept { apply ( set , tuple_cat ( tie ( * opts )...)); }, values ); 
- 
             Otherwise, if disp disposition :: error on_stop . reset (); visit ( [ & ] < class Error > ( Error & error ) noexcept { if constexpr ( ! same_as < Error , none - such > ) { set_error ( std :: move ( rcvr ), std :: move ( error )); } }, errors ); 
- 
             Otherwise, evaluates: on_stop . reset (); set_stopped ( std :: move ( rcvr )); 
 
- 
             
 
- 
           
- 
         The member impls - for < when_all_t >:: start [] < class State , class Rcvr , class ... Ops > ( State & state , Rcvr & rcvr , Ops & ... ops ) noexcept -> void { state . on_stop . emplace ( get_stop_token ( get_env ( rcvr )), on - stop - request { state . stop_src }); if ( state . stop_src . stop_requested ()) { state . on_stop . reset (); set_stopped ( std :: move ( rcvr )); } else { ( start ( ops ), ...); } } 
- 
         The member impls - for < when_all_t >:: complete [] < class Index , class State , class Rcvr , class Set , class ... Args > ( this auto & complete , Index , State & state , Rcvr & rcvr , Set , Args && ... args ) noexcept -> void { if constexpr ( same_as < Set , set_error_t > ) { if ( disposition :: error != state . disp . exchange ( disposition :: error )) { state . stop_src . request_stop (); TRY - EMPLACE - ERROR ( state . errors , std :: forward < Args > ( args )...); } } else if constexpr ( same_as < Set , set_stopped_t > ) { auto expected = disposition :: started ; if ( state . disp . compare_exchange_strong ( expected , disposition :: stopped )) { state . stop_src . request_stop (); } } else if constexpr ( ! same_as < decltype ( State :: values ), tuple <>> ) { if ( state . disp == disposition :: started ) { auto & opt = get < Index :: value > ( state . values ); TRY - EMPLACE - VALUE ( complete , opt , std :: forward < Args > ( args )...); } } state . arrive ( rcvr ); } where TRY - EMPLACE - ERROR ( v , e ) v e try { v . template emplace < decltype ( auto ( e )) > ( e ); } catch (...) { v . template emplace < exception_ptr > ( current_exception ()); } if the expression decltype ( auto ( e ))( e ) v . template emplace < decltype ( auto ( e )) > ( e ) TRY - EMPLACE - VALUE ( c , o , as ...) c o as try { o . emplace ( as ...); } catch (...) { c ( Index (), state , rcvr , set_error , current_exception ()); return ; } if the expression decayed - tuple < decltype ( as )... > { as ...} o . emplace ( as ...) 
 
- 
         
- 
       The expression when_all_with_variant ( sndrs ...) transform_sender ( CD (), make - sender ( when_all_with_variant , {}, sndrs ...)); 
- 
       Given subexpressions sndr env sender - for < decltype (( sndr )), when_all_with_variant_t > false, then the expressionwhen_all_with_variant . transform_sender ( sndr , env ) auto && [ _ , _ , ... child ] = sndr ; return when_all ( into_variant ( std :: forward_like < decltype (( sndr )) > ( child ))...); This causes the when_all_with_variant ( sndrs ...) when_all ( into_variant ( sndrs )...) when_all_with_variant 
34.9.11.12. execution :: into_variant 
     - 
       into_variant variant tuple 
- 
       The name into_variant sndr Sndr decltype (( sndr )) Sndr sender into_variant ( sndr ) 
- 
       Otherwise, the expression into_variant ( sndr ) transform_sender ( get - domain - early ( sndr ), make - sender ( into_variant , {}, sndr )) except that sndr 
- 
       The exposition-only class template impls - for into_variant namespace std :: execution { template <> struct impls - for < into_variant_t > : default - impls { static constexpr auto get - state = see below ; static constexpr auto complete = see below ; }; } - 
         The member impls - for < into_variant_t >:: get - state [] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) noexcept -> type_identity < value_types_of_t < child - type < Sndr > , env_of_t < Rcvr >>> { return {}; } 
- 
         The member impls - for < into_variant_t >:: complete [] < class State , class Rcvr , class Tag , class ... Args > ( auto , State , Rcvr & rcvr , Tag , Args && ... args ) noexcept -> void { if constexpr ( same_as < Tag , set_value_t > ) { using variant_type = typename State :: type ; TRY - SET - VALUE ( rcvr , variant_type ( decayed - tuple < Args ... > { std :: forward < Args > ( args )...})); } else { Tag ()( std :: move ( rcvr ), std :: forward < Args > ( args )...); } } 
 
- 
         
34.9.11.13. execution :: stopped_as_optional 
     - 
       stopped_as_optional optional optional optional 
- 
       The name stopped_as_optional sndr Sndr decltype (( sndr )) stopped_as_optional ( sndr ) transform_sender ( get - domain - early ( sndr ), make - sender ( stopped_as_optional , {}, sndr )) except that sndr 
- 
       Let sndr env Sndr decltype (( sndr )) Env decltype (( env )) sender - for < Sndr , stopped_as_optional_t > false, or if the typesingle - sender - value - type < Sndr , Env > void stopped_as_optional . transform_sender ( sndr , env ) auto && [ _ , _ , child ] = sndr ; using V = single - sender - value - type < Sndr , Env > ; return let_stopped ( then ( std :: forward_like < Sndr > ( child ), [] < class ... Ts > ( Ts && ... ts ) noexcept ( is_nothrow_constructible_v < V , Ts ... > ) { return optional < V > ( in_place , std :: forward < Ts > ( ts )...); }), []() noexcept { return just ( optional < V > ()); }); 
34.9.11.14. execution :: stopped_as_error 
     - 
       stopped_as_error 
- 
       The name stopped_as_error sndr err Sndr decltype (( sndr )) Err decltype (( err )) Sndr sender Err movable - value stopped_as_error ( sndr , err ) stopped_as_error ( sndr , err ) transform_sender ( get - domain - early ( sndr ), make - sender ( stopped_as_error , err , sndr )) except that sndr 
- 
       Let sndr env Sndr decltype (( sndr )) Env decltype (( env )) sender - for < Sndr , stopped_as_error_t > false, then the expressionstopped_as_error . transform_sender ( sndr , env ) auto && [ _ , err , child ] = sndr ; using E = decltype ( auto ( err )); return let_stopped ( std :: forward_like < Sndr > ( child ), [ err = std :: forward_like < Sndr > ( err )]() mutable noexcept ( is_nothrow_move_constructible_v < E > ) { return just_error ( std :: move ( err )); }); 
34.9.12. Sender consumers [exec.consumers]
34.9.12.1. this_thread :: sync_wait 
     - 
       this_thread :: sync_wait this_thread :: sync_wait_with_variant sync_wait 
- 
       Let sync - wait - env namespace std :: this_thread { struct sync - wait - env { execution :: run_loop * loop ; // exposition only auto query ( execution :: get_scheduler_t ) const noexcept { return loop -> get_scheduler (); } auto query ( execution :: get_delegation_scheduler_t ) const noexcept { return loop -> get_scheduler (); } }; } 
- 
       Let sync - wait - result - type sync - wait - with - variant - result - type namespace std :: this_thread { template < execution :: sender_in < sync - wait - env > Sndr > using sync - wait - result - type = optional < execution :: value_types_of_t < Sndr , sync - wait - env , decayed - tuple , type_identity_t >> ; template < execution :: sender_in < sync - wait - env > Sndr > using sync - wait - with - variant - result - type = optional < execution :: value_types_of_t < Sndr , sync - wait - env >> ; } 
- 
       The name this_thread :: sync_wait sndr Sndr decltype (( sndr )) sender_in < Sndr , sync - wait - env > false, the expressionthis_thread :: sync_wait ( sndr ) sndr apply_sender ( get - domain - early ( sndr ), sync_wait , sndr ) Mandates: - 
         The type sync - wait - result - type < Sndr > 
- 
         same_as < decltype ( e ), sync - wait - result - type < Sndr >> true, wheree apply_sender 
 
- 
         
- 
       Let sync - wait - state sync - wait - receiver namespace std :: this_thread { template < class Sndr > struct sync - wait - state { // exposition only execution :: run_loop loop ; // exposition only exception_ptr error ; // exposition only sync - wait - result - type < Sndr > result ; // exposition only }; template < class Sndr > struct sync - wait - receiver { // exposition only using receiver_concept = execution :: receiver_t ; sync - wait - state < Sndr >* state ; // exposition only template < class ... Args > void set_value ( Args && ... args ) && noexcept ; template < class Error > void set_error ( Error && err ) && noexcept ; void set_stopped () && noexcept ; sync - wait - env get_env () const noexcept { return { & state -> loop }; } }; } - 
template < class ... Args > void set_value ( Args && ... args ) && noexcept ; - 
           Effects: Equivalent to: try { state -> result . emplace ( std :: forward < Args > ( args )...); } catch (...) { state -> error = current_exception (); } state -> loop . finish (); 
 
- 
           
- 
template < class Error > void set_error ( Error && err ) && noexcept ; - 
           Effects: Equivalent to: state -> error = AS - EXCEPT - PTR ( std :: forward < Error > ( err )); // see [exec.general] state -> loop . finish (); 
 
- 
           
- 
void set_stopped () && noexcept ; - 
           Effects: Equivalent to state -> loop . finish () 
 
- 
           
 
- 
- 
       For a subexpression sndr Sndr decltype (( sndr )) sender_to < Sndr , sync - wait - receiver < Sndr >> false, the expressionsync_wait . apply_sender ( sndr ) sync - wait - state < Sndr > state ; auto op = connect ( sndr , sync - wait - receiver < Sndr > { & state }); start ( op ); state . loop . run (); if ( state . error ) { rethrow_exception ( std :: move ( state . error )); } return std :: move ( state . result ); 
- 
       The behavior of this_thread :: sync_wait ( sndr ) - 
         It blocks the current thread of execution ([defns.block]) with forward progress guarantee delegation ([intro.progress]) until the specified sender completes. The default implementation of sync_wait run_loop get_delegation_scheduler sync - wait - receiver run_loop 
- 
         It returns the specified sender’s async results as follows: - 
           For a value completion, the result datums are returned in a tuple optional 
- 
           For an error completion, an exception is thrown. 
- 
           For a stopped completion, a disengaged optional 
 
- 
           
 
- 
         
- 
       The name this_thread :: sync_wait_with_variant sndr Sndr decltype ( into_variant ( sndr )) sender_in < Sndr , sync - wait - env > false,this_thread :: sync_wait_with_variant ( sndr ) sndr apply_sender ( get - domain - early ( sndr ), sync_wait_with_variant , sndr ) Mandates: - 
         The type sync - wait - with - variant - result - type < Sndr > 
- 
         same_as < decltype ( e ), sync - wait - with - variant - result - type < Sndr >> true, wheree apply_sender 
 
- 
         
- 
       If callable < sync_wait_t , Sndr > false, the expressionsync_wait_with_variant . apply_sender ( sndr ) using result_type = sync - wait - with - variant - result - type < Sndr > ; if ( auto opt_value = sync_wait ( into_variant ( sndr ))) { return result_type ( std :: move ( get < 0 > ( * opt_value ))); } return result_type ( nullopt ); 
- 
       The behavior of this_thread :: sync_wait_with_variant ( sndr ) - 
         It blocks the current thread of execution ([defns.block]) with forward progress guarantee delegation ([intro.progress]) until the specified sender completes. The default implementation of sync_wait_with_variant sync_wait 
- 
         It returns the specified sender’s async results as follows: - 
           For a value completion, the result datums are returned in an engaged optional variant tuple 
- 
           For an error completion, an exception is thrown. 
- 
           For a stopped completion, a disengaged optional 
 
- 
           
 
- 
         
34.10. Sender/receiver utilities [exec.utils]
34.10.1. execution :: completion_signatures 
     - 
       completion_signatures 
- 
       [Example: struct my_sender { using sender_concept = sender_t ; using completion_signatures = execution :: completion_signatures < set_value_t (), set_value_t ( int , float ), set_error_t ( exception_ptr ), set_error_t ( error_code ), set_stopped_t () > ; }; // Declares my_sender to be a sender that can complete by calling // one of the following for a receiver expression rcvr: // set_value(rcvr) // set_value(rcvr, int{...}, float{...}) // set_error(rcvr, exception_ptr{...}) // set_error(rcvr, error_code{...}) // set_stopped(rcvr) -- end example] 
- 
       [exec.utils.cmplsigs] makes use of the following exposition-only entities: template < class Fn > concept completion - signature = see below ; template < bool > struct indirect - meta - apply { template < template < class ... > class T , class ... As > using meta - apply = T < As ... > ; // exposition only }; template < class ... > concept always - true= true; // exposition only - 
         A type Fn completion - signature - 
           set_value_t ( Vs ...) Vs 
- 
           set_error_t ( Err ) Err 
- 
           set_stopped_t () 
 
- 
           
 template < class Tag , valid - completion - signatures Completions , template < class ... > class Tuple , template < class ... > class Variant > using gather - signatures = see below ; - 
         Let Fns completion_signatures Completions TagFns Fns Tag Ts n n TagFns Tuple Variant gather - signatures < Tag , Completions , Tuple , Variant > META - APPLY ( Variant , META - APPLY ( Tuple , Ts 0 ...), META - APPLY ( Tuple , Ts 1 ...), ... META - APPLY ( Tuple , Ts m -1 . ..)) m TagFns META - APPLY ( T , As ...) typename indirect - meta - apply < always - true< As ... >>:: template meta - apply < T , As ... > ; 
- 
         The purpose of META - APPLY Variant Tuple gather - signatures 
 
- 
         
- 
namespace std :: execution { template < completion - signature ... Fns > struct completion_signatures {}; template < class Sndr , class Env = empty_env , template < class ... > class Tuple = decayed - tuple , template < class ... > class Variant = variant - or - empty > requires sender_in < Sndr , Env > using value_types_of_t = gather - signatures < set_value_t , completion_signatures_of_t < Sndr , Env > , Tuple , Variant > ; template < class Sndr , class Env = empty_env , template < class ... > class Variant = variant - or - empty > requires sender_in < Sndr , Env > using error_types_of_t = gather - signatures < set_error_t , completion_signatures_of_t < Sndr , Env > , type_identity_t , Variant > ; template < class Sndr , class Env = empty_env > requires sender_in < Sndr , Env > inline constexpr bool sends_stopped = ! same_as < type - list <> , gather - signatures < set_stopped_t , completion_signatures_of_t < Sndr , Env > , type - list , type - list >> ; } 
34.10.2. execution :: transform_completion_signatures 
     - 
       transform_completion_signatures completion_signatures 
- 
       [Example: // Given a sender Sndr and an environment Env, adapt the completion // signatures of Sndr by lvalue-ref qualifying the values, adding an additional // exception_ptr error completion if its not already there, and leaving the // other completion signatures alone. template < class ... Args > using my_set_value_t = completion_signatures < set_value_t ( add_lvalue_reference_t < Args > ...) > ; using my_completion_signatures = transform_completion_signatures < completion_signatures_of_t < Sndr , Env > , completion_signatures < set_error_t ( exception_ptr ) > , my_set_value_t > ; -- end example] 
- 
       [exec.utils.tfxcmplsigs] makes use of the following exposition-only entities: template < class ... As > using default - set - value = completion_signatures < set_value_t ( As ...) > ; template < class Err > using default - set - error = completion_signatures < set_error_t ( Err ) > ; 
- 
namespace std :: execution { template < valid - completion - signatures InputSignatures , valid - completion - signatures AdditionalSignatures = completion_signatures <> , template < class ... > class SetValue = default - set - value , template < class > class SetError = default - set - error , valid - completion - signatures SetStopped = completion_signatures < set_stopped_t () >> using transform_completion_signatures = completion_signatures < see below > ; } - 
         SetValue As SetValue < As ... > valid - completion - signatures < SetValue < As ... >> 
- 
         SetError Err SetError < Err > valid - completion - signatures < SetError < Err >> 
 Then: - 
         Let Vs ... type - list gather - signatures < set_value_t , InputSignatures , SetValue , type - list > 
- 
         Let Es ... type - list gather - signatures < set_error_t , InputSignatures , type_identity_t , error - list > error - list error - list < Ts ... > type - list < SetError < Ts > ... > 
- 
         Let Ss completion_signatures <> gather - signatures < set_stopped_t , InputSignatures , type - list , type - list > type - list <> SetStopped 
 Then: - 
         If any of the above types are ill-formed, then transform_completion_signatures < InputSignatures , AdditionalSignatures , SetValue , SetError , SetStopped > 
- 
         Otherwise, transform_completion_signatures < InputSignatures , AdditionalSignatures , SetValue , SetError , SetStopped > completion_signatures < Sigs ... > Sigs ... completion_signatures AdditionalSignatures , Vs ..., Es ..., Ss 
 
- 
         
34.11. Execution contexts [exec.ctx]
34.11.1. execution :: run_loop 
     - 
       A run_loop run () run () 
- 
       A run_loop run_loop 
- 
       Concurrent invocations of the member functions of run_loop run pop_front push_back finish 
- 
       Recommended practice: Implementations are encouraged to use an intrusive queue of operation states to hold the work units to make scheduling allocation-free. namespace std :: execution { 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 ; // exposition only run_loop * loop ; // exposition only run - loop - opstate - base * next ; // exposition only }; template < class Rcvr > 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 (); }; } 
34.11.1.1. Associated types [exec.run.loop.types]
class run - loop - scheduler ; 
- 
       run - loop - scheduler scheduler 
- 
       Instances of run - loop - scheduler run_loop 
- 
       Two instances of run - loop - scheduler run_loop 
- 
       Let sch run - loop - scheduler schedule ( sch ) run - loop - sender sch 
class run - loop - sender ; 
- 
       run - loop - sender sender Env completion_signatures_of_t < run - loop - sender , Env > completion_signatures < set_value_t (), set_error_t ( exception_ptr ), set_stopped_t () > 
- 
       An instance of run - loop - sender run_loop 
- 
       Let sndr run - loop - sender rcvr receiver_of < decltype (( rcvr )), CS > truewhereCS completion_signatures C set_value_t set_stopped_t - 
         The expression connect ( sndr , rcvr ) run - loop - opstate < decay_t < decltype (( rcvr )) >> ( void ( sndr ), auto ( rcvr )) 
- 
         The expression get_completion_scheduler < C > ( get_env ( sndr )) sndr run - loop - scheduler run - loop - scheduler sndr 
 
- 
         
template < class Rcvr > struct run - loop - opstate ; 
- 
       run - loop - opstate < Rcvr > run - loop - opstate - base 
- 
       Let o const run - loop - opstate < Rcvr > REC ( o ) const Rcvr rcvr connect o - 
         The object to which REC ( o ) o 
- 
         The type run - loop - opstate < Rcvr > run - loop - opstate - base :: execute () o . execute () if ( get_stop_token ( REC ( o )). stop_requested ()) { set_stopped ( std :: move ( REC ( o ))); } else { set_value ( std :: move ( REC ( o ))); } 
- 
         The expression start ( o ) try { o . loop -> push - back ( addressof ( o )); } catch (...) { set_error ( std :: move ( REC ( o )), current_exception ()); } 
 
- 
         
34.11.1.2. Constructor and destructor [exec.run.loop.ctor]
run_loop () noexcept ; 
- 
       Postconditions: count is 0 
~ run_loop (); 
- 
       Effects: If count is not 0 terminate () 
34.11.1.3. Member functions [exec.run.loop.members]
run - loop - opstate - base * pop - front (); 
- 
       Effects: Blocks ([defns.block]) until one of the following conditions is true:- 
         count is 0 pop - front nullptr 
- 
         count is greater than 0 1 
 
- 
         
void push - back ( run - loop - opstate - base * item ); 
- 
       Effects: Adds item 1 
- 
       Synchronization: This operation synchronizes with the pop - front item 
run - loop - scheduler get_scheduler (); 
- 
       Returns: An instance of run - loop - scheduler run_loop 
void run (); 
- 
       Precondition: state is starting. 
- 
       Effects: Sets the state to running. Then, equivalent to: while ( auto * op = pop - front ()) { op -> execute (); } 
- 
       Remarks: When state changes, it does so without introducing data races. 
void finish (); 
- 
       Effects: Changes state to finishing. 
- 
       Synchronization: finish pop - front nullptr 
34.12. Coroutine utilities [exec.coro.utils]
34.12.1. execution :: as_awaitable 
     - 
       as_awaitable namespace std :: execution { template < class Sndr , class Promise > concept awaitable - sender = single - sender < Sndr , env_of_t < Promise >> && sender_to < Sndr , awaitable - receiver > && // see below requires ( Promise & p ) { { p . unhandled_stopped () } -> convertible_to < coroutine_handle <>> ; }; template < class Sndr , class Promise > class sender - awaitable ; } - 
         The type sender - awaitable < Sndr , Promise > namespace std :: execution { template < class Sndr , class Promise > class sender - awaitable { struct unit {}; // exposition only using value - type = // exposition only single - sender - value - type < Sndr , env_of_t < Promise >> ; using result - type = // exposition only conditional_t < is_void_v < value - type > , unit , value - type > ; struct awaitable - receiver ; // exposition only variant < monostate , result - type , exception_ptr > result {}; // exposition only connect_result_t < Sndr , awaitable - receiver > state ; // exposition only public : sender - awaitable ( Sndr && sndr , Promise & p ); static constexpr bool await_ready () noexcept { return false; } void await_suspend ( coroutine_handle < Promise > ) noexcept { start ( state ); } value - type await_resume (); }; } - 
           awaitable - receiver struct awaitable - receiver { using receiver_concept = receiver_t ; variant < monostate , result - type , exception_ptr >* result - ptr ; // exposition only coroutine_handle < Promise > continuation ; // exposition only // ... see below }; Let rcvr awaitable - receiver crcvr const rcvr vs err Err - 
             If constructible_from < result - type , decltype (( vs ))... > set_value ( rcvr , vs ...) try { rcvr . result - ptr -> template emplace < 1 > ( vs ...); } catch (...) { rcvr . result - ptr -> template emplace < 2 > ( current_exception ()); } rcvr . continuation . resume (); Otherwise, set_value ( rcvr , vs ...) 
- 
             The expression set_error ( rcvr , err ) rcvr . result - ptr -> template emplace < 2 > ( AS - EXCEPT - PTR ( err )); // see [exec.general] rcvr . continuation . resume (); 
- 
             The expression set_stopped ( rcvr ) static_cast < coroutine_handle <>> ( rcvr . continuation . promise (). unhandled_stopped ()). resume (); 
- 
             For any expression tag forwarding - query as get_env ( crcvr ). query ( tag , as ...) tag ( get_env ( as_const ( crcvr . continuation . promise ())), as ...) 
 
- 
             
- 
           sender - awaitable ( Sndr && sndr , Promise & p ); - 
             Effects: Initializes state connect ( std :: forward < Sndr > ( sndr ), awaitable - receiver { addressof ( result ), coroutine_handle < Promise >:: from_promise ( p )}) 
 
- 
             
- 
           value - type await_resume (); - 
             Effects: Equivalent to: if ( result . index () == 2 ) rethrow_exception ( get < 2 > ( result )); if constexpr ( ! is_void_v < value - type > ) return std :: forward < value - type > ( get < 1 > ( result )); 
 
- 
             
 
- 
           
 
- 
         
- 
       as_awaitable expr p p Expr decltype (( expr )) Promise decay_t < decltype (( p )) > as_awaitable ( expr , p ) - 
         expr . as_awaitable ( p ) - 
           Mandates: is - awaitable < A , Promise > true, whereA 
 
- 
           
- 
         Otherwise, ( void ( p ), expr ) is - awaitable < Expr , U > true, whereU Promise await_transform - 
           Preconditions: is - awaitable < Expr , Promise > trueand the expressionco_await expr U Promise 
 
- 
           
- 
         Otherwise, sender - awaitable { expr , p } awaitable - sender < Expr , Promise > true.
- 
         Otherwise, ( void ( p ), expr ) 
 except that the evaluations of expr p 
- 
         
34.12.2. execution :: with_awaitable_senders 
     - 
       with_awaitable_senders In addition, it provides a default implementation of unhandled_stopped () set_stopped unhandled_stopped namespace std :: execution { template < class - type Promise > struct with_awaitable_senders { template < class 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 ; }; } 
- 
template < class OtherPromise > requires ( ! same_as < OtherPromise , void > ) 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 ; } 
- 
         
- 
template < class Value > call - result - t < as_awaitable_t , Value , Promise &> await_transform ( Value && value ); - 
         Effects: Equivalent to: 
 return as_awaitable ( std :: forward < Value > ( value ), static_cast < Promise &> ( * this )); 
-