11---
22title : Scheduler Affinity
3- document : P3941R2
4- date : 2026-02-23
3+ document : P3941R3
4+ date : 2026-03-21
55audience :
66 - Concurrency Working Group (SG1)
77 - Library Evolution Working Group (LEWG)
@@ -31,6 +31,12 @@ meet its objective at run-time.
3131
3232# Change History
3333
34+ ## R3
35+
36+ - rebase changes on the customization changes [ P3826r3] ( https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html )
37+ - use ` transform_sender ` in ` as_awaitable ` to locate possible customization of nested
38+ senders in [[ exec.as.awaitable] ( https://wg21.link/exec.as.awaitable#7 )]
39+
3440## R2
3541
3642- added requirement on ` get_scheduler ` /` get_start_scheduler `
@@ -549,6 +555,13 @@ algorithm a better name.
549555
550556# Wording Changes
551557
558+ ::: ednote
559+ This wording is relative to [N5032](https://wg21.link/N5032) with
560+ the changes from
561+ [P3826r3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html)
562+ applied.
563+ :::
564+
552565::: ednote
553566If `get_start_scheduler` is introduced add it to the synopsis in
554567[execution.syn] after `get_scheduler` as follows:
@@ -565,6 +578,8 @@ namespace std::execution {
565578 struct get_forward_progress_guarantee_t { @_ unspecified_ @ };
566579 template<class CPO >
567580 struct get_completion_scheduler_t { @_ unspecified_ @ };
581+ template<class CPO = void >
582+ struct get_completion_domain_t { @_ unspecified_ @ };
568583 struct get_await_completion_adaptor_t { @_ unspecified_ @ };
569584
570585 inline constexpr get_domain_t get_domain{};
@@ -575,6 +590,8 @@ namespace std::execution {
575590 inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{};
576591 template<class CPO >
577592 constexpr get_completion_scheduler_t<CPO > get_completion_scheduler{};
593+ template<class CPO = void >
594+ constexpr get_completion_domain_t<CPO > get_completion_domain{};
578595 inline constexpr get_await_completion_adaptor_t get_await_completion_adaptor{};
579596 ...
580597}
@@ -609,55 +626,62 @@ expression `get_start_scheduler(get_env(rcvr))` is well-formed, an operation
609626state that is the result of calling `connect(sndr, rcvr)` shall, if
610627it is started, be started on an execution agent associated with the
611628scheduler `get_start_scheduler(get_env(rcvr))`.
629+ :::
630+
631+ ::: ednote
632+
633+ If `get_start_scheduler` is introduced change
634+ [[exec.snd.expos](https://wg21.link/exec.snd.expos)] paragraph 8 to have <code><i>SCHED-ENV</i></code>
635+ use `get_start_scheduler` instead of `get_scheduler`:
612636
613637:::
614638
639+ [8]{.pnum} <code><i>SCHED-ENV</i>(sch)</code> is an expression `o2` whose type
640+ satisfies <code><i>queryable</i></code> such that `o2.query(@[get_scheduler]{.rm}@@[get_start_scheduler]{.add}@)`
641+ is a prvalue with the same type and value as `sch`, and such that
642+ `o2.query(get_domain)` is expression-equivalent to `sch.query(get_domain)`.
643+
644+
615645::: ednote
616- If `get_start_scheduler` is introduced change how `on` gets its
617- scheduler in [exec.on], i.e., change the use from `get_scheduler`
618- to use `get_start_scheduler`:
646+ The specification of `on` [[exec.on](https://wg21.link/exec.on)] shouldn't use `write_env` as it does,
647+ i.e., this change removes these (not removing them was an oversight
648+ in
649+ [P3826](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3826r3.html)).
650+ In addition, if `get_start_scheduler` is introduced change how `on`
651+ gets its scheduler in [[exec.on](https://wg21.link/exec.on)], i.e., change the use from
652+ `get_scheduler` to use `get_start_scheduler`:
619653:::
620654
621655<p>
622656...
623657</p>
624658
625659<p>
626- The expression `on.transform_sender(out_sndr, env)` has effects equivalent to:
660+ [8]{.pnum} Otherwise, the expression `on.transform_sender(set_value, out_sndr, env)` has effects equivalent to:
627661</p>
628662
629663```
630664auto&& [ _ , data, child] = out_sndr;
631665if constexpr (scheduler<decltype(data)>) {
632666 auto orig_sch =
633- @_ query-with-default_ @(get_ @[ start_ ] {.add}@scheduler , env, @_ not-a-scheduler_ @());
634-
635- if constexpr (same_as<decltype(orig_sch), @_ not-a-scheduler_ @>) {
636- return @_ not-a-sender_ @{};
637- } else {
638- return continues_on(
639- starts_on(std::forward_like<OutSndr >(data), std::forward_like<OutSndr >(child)),
640- std::move(orig_sch));
641- }
667+ @_ call-with-default_ @(@[ get_scheduler] {.rm}@@[ get_start_scheduler] {.add}@, @_ not-a-scheduler_ @(), env);
668+
669+ return continues_on(
670+ starts_on(std::forward_like<OutSndr >(data), std::forward_like<OutSndr >(child)),
671+ std::move(orig_sch));
642672} else {
643673 auto& [ sch, closure] = data;
644- auto orig_sch = @_ query-with-default_ @(
645- get_completion_scheduler<set_value_t>,
646- get_env(child),
647- @_ query-with-default_ @(get_ @[ start_ ] {.add}@scheduler , env, @_ not-a-scheduler_ @()));
648-
649- if constexpr (same_as<decltype(orig_sch), @_ not-a-scheduler_ @>) {
650- return @_ not-a-sender_ @{};
651- } else {
652- return write_env(
653- continues_on(
654- std::forward_like<OutSndr >(closure)(
655- continues_on(
656- write_env(std::forward_like<OutSndr >(child), @_ SCHED-ENV_ @(orig_sch)),
657- sch)),
658- orig_sch),
659- @_ SCHED-ENV_ @(sch));
660- }
674+ auto orig_sch = @_ call-with-default_ @(
675+ get_completion_scheduler<set_value_t>, @_ not-a-scheduler_ @(), get_env(child), env);
676+
677+ return continues_on(
678+ @[ write_env(] {.rm}@
679+ std::forward_like<OutSndr >(closure)(
680+ continues_on(
681+ @[ write_env(] {.rm}@std ::forward_like<OutSndr >(child)@[ ,] {.rm}@ @[ _ SCHED-ENV_ (orig_sch))] {.rm}@,
682+ sch)),
683+ @[ _ SCHED-ENV_ (sch)),] {.rm}@
684+ orig_sch);
661685}
662686```
663687<p>
@@ -748,6 +772,71 @@ scheduler `get_scheduler(get_env(rcvr))`.
748772
749773:::
750774
775+ ::: ednote
776+
777+ Change [[exec.as.awaitable](https://wg21.link/exec.as.awaitable#7)]
778+ paragraph 7 such that it tries to locate a customization for
779+ `as_awaitable` on the transformed nested sender.
780+
781+ :::
782+
783+ [7]{.pnum} `as_awaitable` is a customization point object. For
784+ subexpressions `expr` and `p` where `p` is an lvalue, `Expr` names
785+ the type `decltype((expr))` and `Promise` names the type
786+ `decay_t<decltype((p))>, as_awaitable(expr, p)` is expression-equivalent
787+ to, except that the evaluations of `expr` and `p` are indeterminately
788+ sequenced:
789+
790+ <ul>
791+ <li>
792+ <p>[7.1]{.pnum}
793+ `expr.as_awaitable(p)` if that expression is well-formed.
794+ </p>
795+ <p>
796+ Mandates: `@_is-awaitable_@<A, Promise>` is `true`, where `A` is
797+ the type of the expression above.
798+ </p>
799+ </li>
800+ <li>
801+
802+ [7.?]{.pnum}
803+
804+ [`@_adapt-for-await-completion_@(transform_sender(expr, get_env(p))).as_awaitable(p)`
805+ if this expression is well-formed, `sender_in<Expr, env_of_t<Promise>>` is `true`,
806+ and `@_single-sender-value-type_@<Expr, env_of_t<Promise>>` is well-formed.]{.add}
807+
808+ </li>
809+ <li>
810+ [7.2]{.pnum} Otherwise, `(void(p), expr)` if
811+ `decltype(@_GET-AWAITER_@(expr))` satisfies `@_is-awaiter_@<Promise>`.
812+ </li>
813+ <li>
814+ <p>
815+ [7.3]{.pnum} [Otherwise, `@_sender-awaitable_@{@_adapted-expr_@, p}` if]{.rm}
816+ </p>
817+ <p>[`@_has-queryable-await-completion-adaptor_@<Expr>`]{.rm}</p>
818+ <p>[and]{.rm}</p>
819+ <p>[`@_awaitable-sender_@<decltype((@_adapted-expr_@)), Promise>`]{.rm}</p>
820+ <p>
821+ [are both satisfied, where `@_adapted-expr_@` is
822+ `get_await_completion_adaptor(get_env(expr))(expr)`, except that
823+ `expr` is evaluated only once.]{.rm}
824+ </p>
825+ </li>
826+ <li>
827+ [7.4]{.pnum} Otherwise, [`@_sender-awaitable_@{expr, p}` if
828+ `@_awaitable-sender_@<Expr, Promise>` is `true`.]{.rm}
829+ [`@_sender-awaitable_@{@_adapt-for-await-completion_@(transform_sender(expr, get_env(p))), P}` if `sender_in<Expr, env_of_t<Promise>>` is `true` and `@_single-sender-value-type_@<Expr, env_of_t<Promise>>` is well-formed]{.add}
830+
831+ </li>
832+ <li>[7.5]{.pnum} Otherwise, `(void(p), expr)`.
833+ </li>
834+ </ul>
835+
836+ [8]{.pnum} [The expression `@_adapt-for-await-completion_@(s)` is
837+ `get_await_completion_adaptor(get_env(s))(s)` if that is well-formed,
838+ and `s` otherwise.]{.add}
839+
751840::: ednote
752841Change [exec.affine.on] to use only one parameter, require an
753842infallible scheduler from the receiver, and add a default implementation
@@ -770,26 +859,8 @@ satisfy sender, <code>affine_on(sndr[, sch]{.rm})</code> is ill-formed.
770859
771860[3]{.pnum}
772861Otherwise, the expression <code>affine_on(sndr[, sch]{.rm})</code>
773- is expression-equivalent to:
774- <code>transform_sender(_get-domain-early_(sndr), _make-sender_(affine_on,
775- [sch]{.rm}[env<>()]{.add}, sndr))</code> except that `sndr`
776- is evaluated only once.
777-
778- [4]{.pnum}
779- The exposition-only class template <code>_impls-for_</code>
780- ([exec.snd.expos]) is specialized for `affine_on_t` as follows:
781-
782- ```c++
783- namespace std::execution {
784- template<>
785- struct impls-for<affine_on_t> : default-impls {
786- static constexpr auto get-attrs =
787- [](const auto&@[ data]{.rm}]@, const auto& child) noexcept -> decltype(auto) {
788- return @[_JOIN-ENV_(_SCHED-ATTRS_(data),_FWD-ENV_(]{.rm}@get_env(child)@[))]{.rm}@;
789- };
790- };
791- }
792- ```
862+ is expression-equivalent to
863+ `@_make-sender_@(affine_on, @[sch]{.rm}@@[env<>()]{.add}@, sndr)`.
793864
794865:::{.add}
795866[?]{.pnum}
@@ -805,18 +876,19 @@ using child_tag_t = tag_of_t<remove_cvref_t<decltype(child)>>;
805876if constexpr (requires(const child_tag_t& t){ t.affine_on(child, ev); })
806877 return t.affine_on(child, ev);
807878else
808- return write_env(
809- schedule_from(get_start_scheduler(get_env(ev)), write_env(std::move(child), ev)),
810- JOIN-ENV(env{prop{get_stop_token, never_stop_token()}}, ev)
811- );
879+ return continues_on(child, @_ UNSTOPPABLE-SCHEDULER_ @(get_start_scheduler(ev)));
812880```
813881
814- [ Note 1: This causes the ` affine_on(sndr) ` sender to become
815- ` schedule_from(sch, sndr) ` when it is connected with a receiver
816- ` rcvr ` whose execution domain does not customize ` affine_on ` ,
817- for which ` get_start_scheduler(get_env(rcvr)) ` is ` sch ` , and ` affine_on `
818- isn't specialized for the child sender.
819- end note]
882+ [?]{.pnum} For a subexpression `sch` whose type satisfies `scheduler`,
883+ let `@_UNSTOPPABLE-SCHEDULER_@(sch)` be an expression `e` whose type
884+ satisfies `scheduler` such that:
885+ <ul>
886+ <li>[?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.</li>
887+ <li>[?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)`
888+ is expression-equivalent to `sch.query(q, args...)`.</li>
889+ <li>[?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f`
890+ is expression-equivalent to `sch == other`.</li>
891+ </ul>
820892
821893[?]{.pnum}
822894_Recommended Practice_: Implementations should provide `affine_on`
@@ -875,26 +947,8 @@ satisfy sender, <code>affine_on(sndr[, sch]{.rm})</code> is ill-formed.
875947
876948[3]{.pnum}
877949Otherwise, the expression <code>affine_on(sndr[, sch]{.rm})</code>
878- is expression-equivalent to:
879- <code >transform_sender(_ get-domain-early_ (sndr), _ make-sender_ (affine_on,
880- [ sch] {.rm}[ env< ;> ; ()] {.add}, sndr))</code > except that ` sndr `
881- is evaluated only once.
882-
883- [ 4] {.pnum}
884- The exposition-only class template <code >_ impls-for_ </code >
885- ([ exec.snd.expos] ) is specialized for ` affine_on_t ` as follows:
886-
887- ``` c++
888- namespace std ::execution {
889- template<>
890- struct impls-for<affine_on_t> : default-impls {
891- static constexpr auto get-attrs =
892- [ ] (const auto&@[ data] {.rm}] @, const auto& child) noexcept -> decltype(auto) {
893- return @[ _ JOIN-ENV_ (_ SCHED-ATTRS_ (data),_ FWD-ENV_ (] {.rm}@get_env(child)@[ ))] {.rm}@;
894- };
895- };
896- }
897- ```
950+ is expression-equivalent to
951+ `@_make-sender_@(affine_on, @[sch]{.rm}@@[env<>()]{.add}@, sndr)`.
898952
899953:::{.add}
900954[?]{.pnum}
@@ -910,18 +964,19 @@ using child_tag_t = tag_of_t<remove_cvref_t<decltype(child)>>;
910964if constexpr (requires(const child_tag_t& t){ t.affine_on(child, ev); })
911965 return t.affine_on(child, ev);
912966else
913- return write_env(
914- schedule_from(get_scheduler(get_env(ev)), write_env(std::move(child), ev)),
915- JOIN-ENV(env{prop{get_stop_token, never_stop_token()}}, ev)
916- );
967+ return continues_on(child, @_ UNSTOPPABLE-SCHEDULER_ @(get_scheduler(ev)));
917968```
918969
919- [Note 1: This causes the `affine_on(sndr)` sender to become
920- `schedule_from(sch, sndr)` when it is connected with a receiver
921- `rcvr` whose execution domain does not customize `affine_on`,
922- for which `get_scheduler(get_env(rcvr))` is `sch`, and `affine_on`
923- isn't specialized for the child sender.
924- end note]
970+ [?]{.pnum} For a subexpression `sch` whose type satisfies `scheduler`,
971+ let `@_UNSTOPPABLE-SCHEDULER_@(sch)` be an expression `e` whose type
972+ satisfies `scheduler` such that:
973+ <ul>
974+ <li>[?.1]{.pnum} `schedule(e)` is expression-equivalent to `unstoppable(schedule(sch))`.</li>
975+ <li>[?.2]{.pnum} For any query object `q` and pack of subexpressions `args...`, `e.query(q, args...)`
976+ is expression-equivalent to `sch.query(q, args...)`.</li>
977+ <li>[?.3]{.pnum} Let `f` be the subexpression `@_UNSTOPPABLE-SCHEDULER_@(other)`. `e == f`
978+ is expression-equivalent to `sch == other`.</li>
979+ </ul>
925980
926981[?]{.pnum}
927982_Recommended Practice_: Implementations should provide `affine_on`
@@ -1065,9 +1120,9 @@ explicit task_scheduler(Sch&& sch, Allocator alloc = {});
10651120
10661121::: add
10671122[?]{.pnum}
1068- _Mandates_: Let `e ` be an environment and let `E` be `decltype(e)` .
1069- If `unstoppable_token<decltype(get_stop_token(e)) >` is `true`, then
1070- the type `completion_signatures_of_t<decltype(schedule(sch)) , E>`
1123+ _Mandates_: Let `E ` be the type of a queryable .
1124+ If `unstoppable_token<stop_token_of_t<E> >` is `true`, then
1125+ the type `completion_signatures_of_t<schedule_result_t<Sch> , E>`
10711126only includes `set_value_t()`, otherwise it may additionally include
10721127`set_stopped_t()`.
10731128:::
@@ -1105,7 +1160,7 @@ namespace std::execution {
11051160<code><i>ts-sender</i></code> is an exposition-only class that
11061161models `sender` ([exec.snd]) and for which
11071162<code>completion_signatures_of_t<<i>ts-sender</i>[, E]{.add}></code>
1108- denotes[:]{.rm}[ `completion_signatures<set_value_t()>` if `unstoppable_token<decltype(get_stop_token(declval <E>())) >` is `true`, and
1163+ denotes[:]{.rm}[ `completion_signatures<set_value_t()>` if `unstoppable_token<stop_token_of_t <E>>` is `true`, and
11091164otherwise `completion_signatures<set_value_t(), set_stopped_t()>`.]{.add}
11101165
11111166::: rm
@@ -1128,9 +1183,9 @@ In [exec.run.loop.types] change the paragraph defining the completion signatures
11281183class run-loop-sender;
11291184```
11301185
1131- [5 ]{.pnum}
1186+ [6 ]{.pnum}
11321187<code><i>run-loop-sender</i></code> is an exposition-only type that satisfies `sender`.
1133- [Let `E` be the type of an environment. If `unstoppable_token<decltype(get_stop_token(declval <E>())) >` is `true`,
1188+ [Let `E` be the type of an environment. If `unstoppable_token<stop_token_of_t <E>>` is `true`,
11341189then ]{.add} <code>completion_signatures_of_t<<i>run-loop-sender</i>[, E]{.add}></code> is
11351190
11361191::: rm
@@ -1149,7 +1204,7 @@ Otherwise it is
11491204```
11501205:::
11511206
1152- [6 ]{.pnum} An instance of <code><i>run-loop-sender</i></code> remains
1207+ [7 ]{.pnum} An instance of <code><i>run-loop-sender</i></code> remains
11531208valid until the end of the lifetime of its associated `run_loop`
11541209instance.
11551210
0 commit comments