33 Execution control library [exec]

33.9 Senders [exec.snd]

33.9.12 Sender adaptors [exec.adapt]

33.9.12.1 General [exec.adapt.general]

Subclause [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, and start is called on the resulting operation state.
  • A parent sender ([exec.async.ops]) with a single child sender sndr has an associated attribute object equal to FWD-ENV(get_env(sndr)) ([exec.fwd.env]).
  • 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, any receiver used to connect a child sender has an associated environment equal to 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) among its set of completion signatures where decay_t<Err> denotes the type exception_ptr, but the implementation does not potentially evaluate an error completion operation with an exception_ptr argument, the implementation is allowed to omit the exception_ptr error completion signature from the set.

33.9.12.2 Closure objects [exec.adapt.obj]

A pipeable sender adaptor closure object is a function object that accepts one or more sender arguments and returns a sender.
For a pipeable sender adaptor closure object c and an expression sndr such that decltype((sndr)) models sender, the following expressions are equivalent and yield a sender: c(sndr) sndr | c
Given an additional pipeable sender adaptor closure object d, the expression c | d produces another pipeable sender adaptor closure object e:
e is a perfect forwarding call wrapper ([func.require]) with the following properties:
  • Its target object is an object d2 of type decltype(auto(d)) direct-non-list-initialized with d.
  • It has one bound argument entity, an object c2 of type decltype(auto(c)) direct-non-list-initialized with c.
  • Its call pattern is d2(c2(arg)), where arg is the argument used in a function call expression of e.
The expression c | d is well-formed if and only if the initializations of the state entities ([func.def]) of e are all well-formed.
An object t of type T is a pipeable sender adaptor closure object if T models derived_from<sender_adaptor_closure<T>>, T has no other base classes of type sender_adaptor_closure<U> for any other type U, and T does not satisfy sender.
The template parameter D for sender_adaptor_closure can be an incomplete type.
Before any expression of type cv D appears as an operand to the | operator, D shall be complete and model derived_from<sender_adaptor_closure<D>>.
The behavior of an expression involving an object of type cv D as an operand to the | operator is undefined if overload resolution selects a program-defined operator| function.
A pipeable sender adaptor object is a customization point object that accepts a sender as its first argument and returns a 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 accepts more than one argument, then let sndr be an expression such that decltype((sndr)) models sender, let args... be arguments such that adaptor(sndr, args...) is a well-formed expression as specified below, and let BoundArgs be a pack that denotes decltype(auto(args))....
The expression adaptor(args...) produces a pipeable sender adaptor closure object f that is a perfect forwarding call wrapper with the following properties:
  • Its target object is a copy of adaptor.
  • Its bound argument entities bound_args consist of objects of types BoundArgs... direct-non-list-initialized with std​::​forward<decltype((args))>(args)..., respectively.
  • Its call pattern is adaptor(rcvr, bound_args...), where rcvr is the argument used in a function call expression of f.
The expression adaptor(args...) is well-formed if and only if the initializations of the bound argument entities of the result, as specified above, are all well-formed.

33.9.12.3 execution​::​starts_on [exec.starts.on]

starts_on adapts an input sender into a sender that will start on an execution agent belonging to a particular scheduler's associated execution resource.
The name starts_on denotes a customization point object.
For subexpressions sch and sndr, if decltype((
sch))
does not satisfy scheduler, or decltype((sndr)) does not satisfy sender, starts_on(sch, sndr) is ill-formed.
Otherwise, the expression starts_on(sch, sndr) is expression-equivalent to: transform_sender( query-or-default(get_domain, sch, default_domain()), make-sender(starts_on, sch, sndr)) except that sch is evaluated only once.
Let out_sndr and env be subexpressions such that OutSndr is decltype((out_sndr)).
If sender-for<OutSndr, starts_on_t> is false, then the expressions starts_on.transform_env(out_sndr, env) and starts_on.transform_sender(out_sndr, env) are ill-formed; otherwise
  • starts_on.transform_env(out_sndr, env) is equivalent to: auto&& [_, sch, _] = out_sndr; return JOIN-ENV(SCHED-ENV(sch), FWD-ENV(env));
  • starts_on.transform_sender(out_sndr, env) is equivalent to: 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<OutSndr>>) { return std::move(sndr); });
Let out_sndr be a subexpression denoting a sender returned from starts_on(sch, sndr) or one equal to such, and let OutSndr be the type decltype((out_sndr)).
Let out_rcvr be a subexpression denoting a receiver that has an environment of type Env such that sender_in<OutSndr, Env> is true.
Let op be an lvalue referring to the operation state that results from connecting out_sndr with out_rcvr.
Calling start(op) shall start sndr on an execution agent of the associated execution resource of sch.
If scheduling onto sch fails, an error completion on out_rcvr shall be executed on an unspecified execution agent.

33.9.12.4 execution​::​continues_on [exec.continues.on]

continues_on adapts a sender into one that completes on the specified scheduler.
The name continues_on denotes a pipeable sender adaptor object.
For subexpressions sch and sndr, if decltype((sch)) does not satisfy scheduler, or decltype((sndr)) does not satisfy sender, continues_on(sndr, sch) is ill-formed.
Otherwise, the expression continues_on(sndr, sch) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(continues_on, sch, sndr)) except that sndr is evaluated only once.
The exposition-only class template impls-for is specialized for continues_on_t as follows: 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 and env be subexpressions such that Sndr is decltype((sndr)).
If sender-for<Sndr, continues_on_t> is false, then the expression continues_on.transform_sender(sndr, env) is ill-formed; otherwise, it is equal to: auto [_, data, child] = sndr; return schedule_from(std::move(data), std::move(child));
[Note 1: 
This causes the continues_on(sndr, sch) sender to become schedule_from(sch, sndr) when it is connected with a receiver whose execution domain does not customize continues_on.
— end note]
Let out_sndr be a subexpression denoting a sender returned from continues_on(sndr, sch) or one equal to such, and let OutSndr be the type decltype((out_sndr)).
Let out_rcvr be a subexpression denoting a receiver that has an environment of type Env such that sender_in<OutSndr, Env> is true.
Let op be an lvalue referring to the operation state that results from connecting out_sndr with out_rcvr.
Calling start(op) shall start sndr on the current execution agent and execute completion operations on out_rcvr on an execution agent of the execution resource associated with sch.
If scheduling onto sch fails, an error completion on out_rcvr shall be executed on an unspecified execution agent.

33.9.12.5 execution​::​schedule_from [exec.schedule.from]

schedule_from schedules work dependent on the completion of a sender onto a scheduler's associated execution resource.
[Note 1: 
schedule_from is not meant to be used in user code; it is used in the implementation of continues_on.
— end note]
The name schedule_from denotes a customization point object.
For some subexpressions sch and sndr, let Sch be decltype((sch)) and Sndr be decltype((sndr)).
If Sch does not satisfy scheduler, or Sndr does not satisfy sender, schedule_from(sch, sndr) is ill-formed.
Otherwise, the expression schedule_from(sch, sndr) is expression-equivalent to: transform_sender( query-or-default(get_domain, sch, default_domain()), make-sender(schedule_from, sch, sndr)) except that sch is evaluated only once.
The exposition-only class template impls-for ([exec.snd.general]) is specialized for schedule_from_t as follows: 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 is initialized with a callable object equivalent to the following lambda: [](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 is initialized with a callable object equivalent to the following lambda: []<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 can be used to initialize a structured binding.
Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<child-type<Sndr>, env_of_t<Rcvr>>.
Let as-tuple be an alias template that transforms a completion signature Tag(Args...) into the tuple specialization decayed-tuple<Tag, Args...>.
Then variant_t denotes the type variant<monostate, as-tuple<Sigs>...>, except with duplicate types removed.
receiver_t is an alias for the following exposition-only class: 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 clause of the lambda is true if the construction of the returned state-type object is not potentially throwing; otherwise, false.
The member impls-for<schedule_from_t>​::​complete is initialized with a callable object equivalent to the following lambda: []<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 be a subexpression denoting a sender returned from schedule_from(sch, sndr) or one equal to such, and let OutSndr be the type decltype((out_sndr)).
Let out_rcvr be a subexpression denoting a receiver that has an environment of type Env such that sender_in<OutSndr, Env> is true.
Let op be an lvalue referring to the operation state that results from connecting out_sndr with out_rcvr.
Calling start(op) shall start sndr on the current execution agent and execute completion operations on out_rcvr on an execution agent of the execution resource associated with sch.
If scheduling onto sch fails, an error completion on out_rcvr shall be executed on an unspecified execution agent.

33.9.12.6 execution​::​on [exec.on]

The on sender adaptor has two forms:
  • on(sch, sndr), which starts a sender sndr on an execution agent belonging to a scheduler sch's associated execution resource and that, upon sndr's completion, transfers execution back to the execution resource on which the on sender was started.
  • on(sndr, sch, closure), which upon completion of a sender sndr, transfers execution to an execution agent belonging to a scheduler sch's associated execution resource, then executes a sender adaptor closure closure with the async results of the sender, and that then transfers execution back to the execution resource on which sndr completed.
The name on denotes a pipeable sender adaptor object.
For subexpressions sch and sndr, on(sch, sndr) is ill-formed if any of the following is true:
  • decltype((sch)) does not satisfy scheduler, or
  • decltype((sndr)) does not satisfy sender and sndr is not a pipeable sender adaptor closure object ([exec.adapt.obj]), or
  • decltype((sndr)) satisfies sender and sndr is also a pipeable sender adaptor closure object.
Otherwise, if decltype((sndr)) satisfies sender, the expression on(sch, sndr) is expression-equivalent to: transform_sender( query-or-default(get_domain, sch, default_domain()), make-sender(on, sch, sndr)) except that sch is evaluated only once.
For subexpressions sndr, sch, and closure, if the expression on(sndr, sch, closure) is ill-formed; otherwise, it is expression-equivalent to: transform_sender( get-domain-early(sndr), make-sender(on, product-type{sch, closure}, sndr)) except that sndr is evaluated only once.
Let out_sndr and env be subexpressions, let OutSndr be decltype((out_sndr)), and let Env be decltype((env)).
If sender-for<OutSndr, on_t> is false, then the expressions on.transform_env(out_sndr, env) and on.transform_sender(out_sndr, env) are ill-formed.
Otherwise: Let not-a-scheduler be an unspecified empty class type, and let not-a-sender be the exposition-only type: 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 returns an object of a type that is not a specialization of the completion_signatures class template.
The expression on.transform_env(out_sndr, env) has effects equivalent to: 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) has effects equivalent to: 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 to inform users that their usage of on is incorrect because there is no available scheduler onto which to restore execution.
Let out_sndr be a subexpression denoting a sender returned from on(sch, sndr) or one equal to such, and let OutSndr be the type decltype((out_sndr)).
Let out_rcvr be a subexpression denoting a receiver that has an environment of type Env such that sender_in<OutSndr, Env> is true.
Let op be an lvalue referring to the operation state that results from connecting out_sndr with out_rcvr.
Calling start(op) shall
  • remember the current scheduler, get_scheduler(get_env(rcvr));
  • start sndr on an execution agent belonging to sch's associated execution resource;
  • upon sndr's completion, transfer execution back to the execution resource associated with the scheduler remembered in step 1; and
  • forward sndr's async result to out_rcvr.
If any scheduling operation fails, an error completion on out_rcvr shall be executed on an unspecified execution agent.
Let out_sndr be a subexpression denoting a sender returned from on(sndr, sch, closure) or one equal to such, and let OutSndr be the type decltype((out_sndr)).
Let out_rcvr be a subexpression denoting a receiver that has an environment of type Env such that sender_in<OutSndr, Env> is true.
Let op be an lvalue referring to the operation state that results from connecting out_sndr with out_rcvr.
Calling start(op) shall
  • 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 on the current execution agent;
  • upon sndr's completion, transfer execution to an agent owned by sch's associated execution resource;
  • forward sndr's async result as if by connecting and starting a sender closure(S), where S is a sender that completes synchronously with sndr's async result; and
  • upon completion of the operation started in the previous step, 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 shall be executed on an unspecified execution agent.

33.9.12.7 execution​::​then, execution​::​upon_error, execution​::​upon_stopped [exec.then]

then attaches an invocable as a continuation for an input sender's value completion operation.
upon_error and upon_stopped do the same for the error and stopped completion operations, respectively, sending the result of the invocable as a value completion.
The names then, upon_error, and upon_stopped denote pipeable sender adaptor objects.
Let the expression then-cpo be one of then, upon_error, or upon_stopped.
For subexpressions sndr and f, if decltype((sndr)) does not satisfy sender, or decltype((f)) does not satisfy movable-value, then-cpo(sndr, f) is ill-formed.
Otherwise, the expression then-cpo(sndr, f) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(then-cpo, f, sndr)) except that sndr is evaluated only once.
For then, upon_error, and upon_stopped, let set-cpo be set_value, set_error, and set_stopped, respectively.
The exposition-only class template impls-for ([exec.snd.general]) is specialized for then-cpo as follows: 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) has undefined behavior unless it returns a senderout_sndr that
  • invokes f or a copy of such with the value, error, or stopped result datums of sndr for then, upon_error, and upon_stopped, respectively, using the result value of f as out_sndr's value completion, and
  • forwards all other completion operations unchanged.

33.9.12.8 execution​::​let_value, execution​::​let_error, execution​::​let_stopped [exec.let]

let_value, let_error, and let_stopped transform a sender's value, error, and stopped completions, respectively, into a new child asynchronous operation by passing the sender's result datums to a user-specified callable, which returns a new sender that is connected and started.
For let_value, let_error, and let_stopped, let set-cpo be set_value, set_error, and set_stopped, respectively.
Let the expression let-cpo be one of let_value, let_error, or let_stopped.
For a subexpression sndr, let let-env(sndr) be expression-equivalent to the first well-formed expression below:
  • 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, and let_stopped denote pipeable sender adaptor objects.
For subexpressions sndr and f, let F be the decayed type of f.
If decltype((sndr)) does not satisfy sender or if decltype((f)) does not satisfy movable-value, the expression let-cpo(sndr, f) is ill-formed.
If F does not satisfy invocable, the expression let_stopped(sndr, f) is ill-formed.
Otherwise, the expression let-cpo(sndr, f) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(let-cpo, f, sndr)) except that sndr is evaluated only once.
The exposition-only class template impls-for ([exec.snd.general]) is specialized for let-cpo as follows: 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 denote the following exposition-only class template: 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 is initialized with a callable object equivalent to the following: []<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 be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<child-type<Sndr>, env_of_t<Rcvr>>.
Let LetSigs be a pack of those types in Sigs with a return type of decayed-typeof<set-cpo>.
Let as-tuple be an alias template such that as-tuple<Tag(Args...)> denotes the type decayed-tuple<Args...>.
Then args_variant_t denotes the type variant<monostate, as-tuple<LetSigs>...> except with duplicate types removed.
Given a type Tag and a pack Args, let as-sndr2 be an alias template such that as-sndr2<Tag(Args...)> denotes the type call-result-t<Fn, decay_t<Args>&...>.
Then ops2_variant_t denotes the type variant<monostate, connect_result_t<as-sndr2<LetSigs>, receiver2<Rcvr, Env>>...> except with duplicate types removed.
The requires-clause constraining the above lambda is satisfied if and only if the types args_variant_t and ops2_variant_t are well-formed.
The exposition-only function template let-bind has effects equivalent to: 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 is initialized with a callable object equivalent to the following: []<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 and env be subexpressions, and let Sndr be decltype((sndr)).
If sender-for<Sndr, decayed-typeof<let-cpo>> is false, then the expression let-cpo.transform_env(sndr, env) is ill-formed.
Otherwise, it is equal to JOIN-ENV(let-env(sndr), FWD-ENV(env)).
Let the subexpression out_sndr denote the result of the invocation let-cpo(sndr, f) or an object equal to such, and let the subexpression rcvr denote a receiver such that the expression connect(out_sndr, rcvr) is well-formed.
The expression connect(out_sndr, rcvr) has undefined behavior unless it creates an asynchronous operation ([exec.async.ops]) that, when started:
  • invokes f when set-cpo is called with sndr's result datums,
  • makes its completion dependent on the completion of a sender returned by f, and
  • propagates the other completion operations sent by sndr.

33.9.12.9 execution​::​bulk [exec.bulk]

bulk runs a task repeatedly for every index in an index space.
The name bulk denotes a pipeable sender adaptor object.
For subexpressions sndr, shape, and f, let Shape be decltype(auto(shape)).
If bulk(sndr, shape, f) is ill-formed.
Otherwise, the expression bulk(sndr, shape, f) is expression-equivalent to:
transform_sender(get-domain-early(sndr), make-sender(bulk, product-type{shape, f}, sndr)) except that sndr is evaluated only once.
The exposition-only class template impls-for ([exec.snd.general]) is specialized for bulk_t as follows: namespace std::execution { template<> struct impls-for<bulk_t> : default-impls { static constexpr auto complete = see below; }; }
The member impls-for<bulk_t>​::​complete is initialized with a callable object equivalent to the following lambda: []<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 true if and only if Tag denotes a type other than set_value_t or if the expression f(auto(shape), args...) is well-formed.
Let the subexpression out_sndr denote the result of the invocation bulk(sndr, shape, f) or an object equal to such, and let the subexpression rcvr denote a receiver such that the expression connect(out_sndr, rcvr) is well-formed.
The expression connect(out_sndr, rcvr) has undefined behavior unless it creates an asynchronous operation ([exec.async.ops]) that, when started,
  • on a value completion operation, invokes f(i, args...) for every i of type Shape from 0 to shape, where args is a pack of lvalue subexpressions referring to the value completion result datums of the input sender, and
  • propagates all completion operations sent by sndr.

33.9.12.10 execution​::​split [exec.split]

split adapts an arbitrary sender into a sender that can be connected multiple times.
Let split-env be the type of an environment such that, given an instance env, the expression get_stop_token(env) is well-formed and has type inplace_stop_token.
The name split denotes a pipeable sender adaptor object.
For a subexpression sndr, let Sndr be decltype((sndr)).
If sender_in<Sndr, split-env> is false, split(sndr) is ill-formed.
Otherwise, the expression split(sndr) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(split, {}, sndr)) except that sndr is evaluated only once.
[Note 1: 
The default implementation of transform_sender will have the effect of connecting the sender to a receiver.
It will return a sender with a different tag type.
— end note]
Let local-state denote the following exposition-only class template:
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_for_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 denote the following exposition-only class template: namespace std::execution { template<class Sndr> struct split-receiver { // exposition only 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 denote the following exposition-only class template: 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 be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<Sndr>.
For type Tag and pack Args, let as-tuple be an alias template such that as-tuple<Tag(Args...)> denotes the type decayed-tuple<Tag, Args...>.
Then variant-type denotes the type variant<tuple<set_stopped_t>, tuple<set_error_t, exception_ptr>, as-tuple<Sigs>...> but with duplicate types removed.
Let state-list-type be a type that stores a list of pointers to local-state-base objects and that permits atomic insertion.
explicit shared-state(Sndr&& sndr);
Effects: Initializes op_state with the result of connect(std​::​forward<Sndr>(sndr), split-receiver{this}).
Postconditions: waiting_states is empty, and completed is false.
void start-op() noexcept;
Effects: Evaluates inc-ref().
If stop_src.stop_requested() is true, evaluates notify(); otherwise, evaluates start(op_state).
void notify() noexcept;
Effects: Atomically does the following:
  • Sets completed to true, and
  • Exchanges waiting_states with an empty list, storing the old value in a local prior_states.
Then, for each pointer p in prior_states, evaluates p->notify().
Finally, evaluates dec-ref().
void inc-ref() noexcept;
Effects: Increments ref_count.
void dec-ref() noexcept;
Effects: Decrements ref_count.
If the new value of ref_count is 0, calls delete this.
Synchronization: If an evaluation of dec-ref() does not decrement the ref_count to 0 then synchronizes with the evaluation of dec-ref() that decrements ref_count to 0.
Let split-impl-tag be an empty exposition-only class type.
Given an expression sndr, the expression split.transform_sender(sndr) is equivalent to: 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 is an exposition-only class that manages the reference count of the shared-state object pointed to by sh_state.
shared-wrapper models copyable with move operations nulling out the moved-from object, copy operations incrementing the reference count by calling sh_state->inc-ref(), and assignment operations performing a copy-and-swap operation.
The destructor has no effect if sh_state is null; otherwise, it decrements the reference count by evaluating sh_state->dec-ref().
The exposition-only class template impls-for ([exec.snd.general]) is specialized for split-impl-tag as follows: 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 is initialized with a callable object equivalent to the following lambda expression: []<class Sndr>(Sndr&& sndr, auto& rcvr) noexcept { return local-state{std::forward<Sndr>(sndr), rcvr}; }
The member impls-for<split-impl-tag>​::​start is initialized with a callable object that has a function call operator equivalent to the following: template<class Sndr, class Rcvr> void operator()(local-state<Sndr, Rcvr>& state, Rcvr& rcvr) const noexcept;
Effects: If state.sh_state->completed is true, evaluates state.notify() and returns.
Otherwise, does the following in order:
  • Evaluates 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 of state.sh_state->completed, and
    • Inserts addressof(state) into state.sh_state->waiting_states if c is false.
  • If c is true, calls state.notify() and returns.
  • Otherwise, if addressof(state) is the first item added to state.sh_state->waiting_states, evaluates state.sh_state->start-op().

33.9.12.11 execution​::​when_all [exec.when.all]

when_all and when_all_with_variant both adapt multiple input senders into a sender that completes when all input senders have completed.
when_all only accepts senders with a single value completion signature and on success concatenates all the input senders' value result datums into its own value completion operation.
when_all_with_variant(sndrs...) is semantically equivalent to when_all(into_variant(sndrs)...), where sndrs is a pack of subexpressions whose types model sender.
The names when_all and when_all_with_variant denote customization point objects.
Let sndrs be a pack of subexpressions, let Sndrs be a pack of the types decltype((sndrs))..., and let CD be the type common_type_t<decltype(get-domain-early(sndrs))...>.
The expressions when_all(sndrs...) and when_all_with_variant(sndrs...) are ill-formed if any of the following is true:
The expression when_all(sndrs...) is expression-equivalent to: transform_sender(CD(), make-sender(when_all, {}, sndrs...))
The exposition-only class template impls-for ([exec.snd.general]) is specialized for when_all_t as follows: 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 is initialized with a callable object equivalent to the following lambda expression: [](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 is initialized with a callable object equivalent to the following lambda expression: []<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 is initialized with a callable object equivalent to the following lambda expression: []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(e) -> decltype(e) { return e; } where e is the expression std::forward<Sndr>(sndr).apply(make-state<Rcvr>()) and where make-state is the following exposition-only class template: 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_for_t<stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>; struct state-type { void arrive(Rcvr& rcvr) noexcept { // exposition only if (0 == --count) { complete(rcvr); } } void complete(Rcvr& rcvr) noexcept; // exposition only 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 if decay-copying any of the child senders' result datums can potentially throw; otherwise, none-such, where none-such is an unspecified empty class type.
The alias values_tuple denotes the type tuple<value_types_of_t<Sndrs, env_of_t<Rcvr>, decayed-tuple, optional>...> if that type is well-formed; otherwise, tuple<>.
The alias errors_variant denotes the type variant<none-such, copy-fail, Es...> with duplicate types removed, where Es is the pack of the decayed types of all the child senders' possible error result datums.
The member void state-type​::​complete(Rcvr& rcvr) noexcept behaves as follows:
  • If disp is equal to disposition​::​started, evaluates: 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 is equal to disposition​::​error, evaluates: 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 is initialized with a callable object equivalent to the following lambda expression: []<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 is initialized with a callable object equivalent to the following lambda expression: []<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), for subexpressions v and e, is equivalent to: try { v.template emplace<decltype(auto(e))>(e); } catch (...) { v.template emplace<exception_ptr>(current_exception()); } if the expression decltype(auto(e))(e) is potentially throwing; otherwise, v.template emplace<decltype(auto(e))>(e); and where TRY-EMPLACE-VALUE(c, o, as...), for subexpressions c, o, and pack of subexpressions as, is equivalent to: try { o.emplace(as...); } catch (...) { c(Index(), state, rcvr, set_error, current_exception()); return; } if the expression decayed-tuple<decltype(as)...>{as...} is potentially throwing; otherwise, o.emplace(as...).
The expression when_all_with_variant(sndrs...) is expression-equivalent to: transform_sender(CD(), make-sender(when_all_with_variant, {}, sndrs...));
Given subexpressions sndr and env, if sender-for<decltype((sndr)), when_all_with_variant_t> is false, then the expression when_all_with_variant.transform_sender(sndr, env) is ill-formed; otherwise, it is equivalent to: auto&& [_, _, ...child] = sndr; return when_all(into_variant(std::forward_like<decltype((sndr))>(child))...);
[Note 1: 
This causes the when_all_with_variant(sndrs...) sender to become when_all(into_variant(sndrs)...) when it is connected with a receiver whose execution domain does not customize when_all_with_variant.
— end note]

33.9.12.12 execution​::​into_variant [exec.into.variant]

into_variant adapts a sender with multiple value completion signatures into a sender with just one value completion signature consisting of a variant of tuples.
The name into_variant denotes a pipeable sender adaptor object.
For a subexpression sndr, let Sndr be decltype((sndr)).
If Sndr does not satisfy sender, into_variant(sndr) is ill-formed.
Otherwise, the expression into_variant(sndr) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(into_variant, {}, sndr)) except that sndr is only evaluated once.
The exposition-only class template impls-for ([exec.snd.general]) is specialized for into_variant as follows: 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 is initialized with a callable object equivalent to the following lambda: []<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 is initialized with a callable object equivalent to the following lambda: []<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)...); } }

33.9.12.13 execution​::​stopped_as_optional [exec.stopped.opt]

stopped_as_optional maps a sender's stopped completion operation into a value completion operation as a disengaged optional.
The sender's value completion operation is also converted into an optional.
The result is a sender that never completes with stopped, reporting cancellation by completing with a disengaged optional.
The name stopped_as_optional denotes a pipeable sender adaptor object.
For a subexpression sndr, let Sndr be decltype((sndr)).
The expression stopped_as_optional(sndr) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(stopped_as_optional, {}, sndr)) except that sndr is only evaluated once.
Let sndr and env be subexpressions such that Sndr is decltype((sndr)) and Env is decltype((env)).
If sender-for<Sndr, stopped_as_optional_t> is false, or if the type single-sender-value-type<Sndr, Env> is ill-formed or void, then the expression stopped_as_optional.transform_sender(sndr, env) is ill-formed; otherwise, it is equivalent to: 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>()); });

33.9.12.14 execution​::​stopped_as_error [exec.stopped.err]

stopped_as_error maps an input sender's stopped completion operation into an error completion operation as a custom error type.
The result is a sender that never completes with stopped, reporting cancellation by completing with an error.
The name stopped_as_error denotes a pipeable sender adaptor object.
For some subexpressions sndr and err, let Sndr be decltype((sndr)) and let Err be decltype((err)).
If the type Sndr does not satisfy sender or if the type Err does not satisfy movable-value, stopped_as_error(sndr, err) is ill-formed.
Otherwise, the expression stopped_as_error(sndr, err) is expression-equivalent to: transform_sender(get-domain-early(sndr), make-sender(stopped_as_error, err, sndr)) except that sndr is only evaluated once.
Let sndr and env be subexpressions such that Sndr is decltype((sndr)) and Env is decltype((env)).
If sender-for<Sndr, stopped_as_error_t> is false, then the expression stopped_as_error.transform_sender(sndr, env) is ill-formed; otherwise, it is equivalent to: 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)); });