Description
Background: continues_on
and schedule_from
:
std::execution
has two customizable algorithms for transfering execution from one context to another: continues_on
and schedule_from
. The reason for having two is due to the fact there are two execution contexts in play: the context we're transitioning from and the one we're transitioning to. The source context may need special sauce to transfer execution back to the CPU, and the destination context may need special sauce to transition from the CPU onto it.
The schedule_from
algorithm looks for customizations based on the domain of the destination, and the continues_on
algorithm dispatches based on the domain of the source. A "domain" is a tag type associated with an execution context that is used to find algorithm customizations for that context. The continues_on
algorithm is required to lower to the result of a call to schedule_from
. In this way, every context transition gets all the special sauce it needs to get from one arbitrary context to another, even if the two contexts know nothing about each other.
We can see this in the definitions of the continues_on
and schedule_from
customizations points:
Algorithm | Returns |
---|---|
continues_on(sndr, sched) |
transform_sender(get-domain-early(sndr), make-sender(continues_on, sched, sndr)) |
schedule_from(sched, sndr) |
transform_sender(query-or-default(get_domain, sched, default_domain{}), make-sender(schedule_from, sched, sndr)) |
By asking for the predecessor sender's domain, continues_on
uses the domain of the source to find its customization. And by asking for the scheduler's domain, schedule_from
uses the domain of the destination.
The final piece is the transformation, within the connect
customization point, of the continues_on
sender to the schedule_from
sender, which is done with the continues_on.transform_sender(Sndr, Env)
member function (see [exec.continues.on] p5).
Problem 1
When connect
-time customization was added to std::execution
in
[@P3303], the logic of continues_on
/schedule_from
customization accidentally got reversed: The exposition-only get-domain-late
function,
which is called from connect
, is used to determine the domain to use to find a sender
transform function. It says:
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>
istrue
, thenreturn Domain();
where
Domain
is the type of the following expression:[] { auto [_, sch, _] = sndr; return query-or-default(get_domain, sch, default_domain()); }();
[Note 1: The
continues_on
algorithm works in tandem withschedule_from
([exec.schedule.from]) to give scheduler authors a way to customize both how to
transition onto (continues_on
) and off of (schedule_from
) a given execution
context. Thus,continues_on
ignores the domain of the predecessor and uses the
domain of the destination scheduler to select a customization, a property that is
unique tocontinues_on
. That is why it is given special treatment here. — end
note]
- Otherwise,
return Domain();
where
Domain
is the first of the following expressions that is well-formed and whose type is notvoid
:
get_domain(get_env(sndr))
completion-domain<void>(sndr)
get_domain(env)
get_domain(get_scheduler(env))
default_domain()
Paragraph 14.1 above gets the roles of continues_on
and schedule_from
mixed up. They should be reversed.
Problem 2
All of the adaptor algorithm CPOs use the domain of the predecessor(s) to find customizations. For example, then(sndr, fn)
returns transform_sender(get-domain-early(sndr), make-sender(then, fn, sndr))
; i.e., the domain is pulled from sndr
. A sender that advertizes a domain is making an assertion about where it will complete. Where the predecessor completes is where the current sender will start. Therefore, then(sndr, fn)
is dispatching based on where the then
sender will start.
If we look at the connect
customization point at how a late customization is found, we see that before it does anything else, it transforms the input sender as follows:
transform_sender(decltype(get-domain-late(sndr, get_env(rcvr))){}, sndr, get_env(rcvr))
We can see that when passed a then
sender, we ask the then
sender for its domain (and use the domain of the receiver's env as a fallback). That means that for then
senders, connect
dispatches to a customization based on the domain of the then
sender itself. That is different from early customization, which used the domain of the predecessor. The inconsistency is not intentional.
For then
and most other adaptors, it doesn't make any difference. The then
sender completes wherever its predecessor completes, so the "start" and "complete" domains are the same. That is not the case, however, for continues_on
. The domain on which it starts can be different from the domain on which it completes.
The tl;dr of problem 2 is that connect
uses the wrong domain to find a customization for continues_on
. The domain of the continues_on(sndr, sched)
sender is the domain on which it will complete, which is the domain of sched
. But continues_on
should be using the domain on which it starts to find a customization.
A principled solution would be to recognize that a sender really has 2 associated domains: the starting domain and the completing domain. it follows that we should have two queries: get_starting_domain
and get_completion_domain
. Early customization would use the completion domain of the predecessor, whereas late customization would use the starting domain of the sender itself.
A simpler solution recognizes that for all adaptors besides continues_on
, the starting domain and the completion domain are the same, so separate queries are not needed. (This is even true for the schedule_from
sender: early or late, it should always use the domain of the destination scheduler). So we can leave connect
alone and treat continues_on
as special in get-domain-late
.
Back to First Principles
Given what we already know about why schedule_from
and continues_on
both exist and find customization the way they do, we can build a table that shows how customizations should be selected for all the scenarios of interest. Consider the sender expression A | continues_on(Sch) | B
. The table below shows the domain that each algorithm should use to find a customization.
Table 2: Domain that should be used to find customizations in A | continues_on(Sch) | B
Algorithm | Early customization uses... | Late customization uses... | get_domain(get_env(ALGO(A...))) should return... |
---|---|---|---|
schedule_from |
domain of Sch or default_domain |
domain of Sch or default_domain |
domain of Sch |
continues_on |
domain of A or default_domain |
domain of A or domain of Env |
domain of Sch |
B |
domain of Sch or default_domain |
domain of Sch or domain of Env |
n/a |
We can use this table to correct the effected parts of the spec:
-
get-domain-late
(sndr, env)
should be changed to:- when
sndr
iscompletes_on(Pred,Sch)
, return the domain ofPred
and use domain ofenv
as a fallback. - when
sndr
isschedule_from(Sch,Pred)
, return the domain ofSch
and usedefault_domain
as a fallback (ignoringenv
). - otherwise, return the domain of
sndr
and use domain ofenv
as a fallback.
- when
-
Change the
SCHED-ATTRS
andSCHED-ENV
pseudo-macros from [exec.snd.expos] to
accept a fallback environment in addition to a scheduler, but never use the fallback
env to answer theget_domain
query. -
Change all uses of
SCHED-ATTRS
andSCHED-ENV
as appropriate.
Proposed Resolution
[Editorial note: Change [exec.snd.expos]/p6 as follows:]
For a scheduler
sch
and environmentenv
,
SCHED-ATTRS(sch, env)
is an expressiono1
whose type
satisfiesqueryable
such that: [Editorial note: reformatted as a list.]
o1.query(get_completion_scheduler<
is an expression with the same type and value asTag
set_value_t>)
sch
where,Tag
is one ofset_value_t
orset_stopped_t
and such
that
o1.query(get_completion_scheduler<Tag>)
is ill-formed forTag
other thanset_value_t
,
o1.query(get_domain)
is expression-equivalent tosch.query(get_domain)
,
andFor a pack of subexpressions
as
and query objectQ
such that
forwarding_query(Q)
istrue
,o1.query(Q, as...)
is
expression-equivalent toenv.query(Q, as...)
.
SCHED-ATTRS(sch)
is expression-equivalent to
SCHED-ATTRS(sch, execution::env<>{})
.
SCHED-ENV(sch, env)
is an expressiono2
whose type
satisfiesqueryable
such that: [Editorial note: reformatted as a list.]
o2.query(get_scheduler)
is a prvalue with the same type and value assch
,and
such that
o2.query(get_domain)
is expression-equivalent to
sch.query(get_domain)
., andFor a pack of subexpressions
as
and query objectQ
such that
forwarding_query(Q)
istrue
,o1.query(Q, as...)
is
expression-equivalent toenv.query(Q, as...)
.
SCHED-ENV(sch)
is expression-equivalent to
SCHED-ENV(sch, execution::env<>{})
.
[Editorial note: Change [exec.snd.expos]/p14 as follows:]
template<class Sndr, class Env> constexpr auto get-domain-late(const Sndr& sndr, const Env& env) noexcept;
Effects: Equivalent to:
[Editorial note: Taken from 14.3 with edits.] Let
DEFAULT-LATE-DOMAIN(S, E)
be the first of the following
expressions that is well-formed and whose type is notvoid
:
get_domain(get_env(sndr))
completion-domain<void>(sndr)
get_domain(env)
get_domain(get_scheduler(env))
default_domain()
If
sender-for<Sndr,
is
continues_on_tschedule_from_t>true
, thenreturn Domain();
where
Domain
is the type of the following expression:[] { auto [_, sch, _] = sndr; return query-or-default(get_domain, sch, default_domain()); }();
[Note 1: The
continues_on
schedule_from
algorithm works
in tandem withschedule_from
([exec.schedule.from])continues_on
([exec.continues.on]) to give scheduler authors a way to customize both how
to transitionontooff of (continues_on
) andoffonto (
ofschedule_from
) a given execution context. Thus,
continues_on
schedule_from
ignores the domain of the
predecessor and uses the domain of the destination scheduler to select a
customization, a property that is unique to
continues_on
schedule_from
. That is why it is given
special treatment here. — end note]
Otherwise, if
sender-for<Sndr, continues_on_t>
is
true
, thenreturn Domain();
where
Domain
is the type of the following expression:[] { const auto& [_, _, child] = sndr; return DEFAULT-LATE-DOMAIN(child, env); }();
Otherwise,
DEFAULT-LATE-DOMAIN(sndr, env)
return Domain();
where
Domain
is the first of the following expressions that is well-formed and
whose type is notvoid
:
get_domain(get_env(sndr))
completion-domain<void>(sndr)
get_domain(env)
get_domain(get_scheduler(env))
default_domain()
[Editorial note: Change [exec.starts.on]/p4.1 as follows:]
Let [Editorial note: ...as before...]; otherwise:
starts_on.transform_env(out_sndr, env)
is equivalent to:auto&& [_, sch, _] = out_sndr;
return JOIN-ENV(SCHED-ENV(sch), FWD-ENV(env));return SCHED-ENV(sch, env);
[Editorial note: Change [exec.continues.on]/p4 as follows:]
The exposition-only class template
impls-for
is specialized forcontinues_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)));return SCHED-ATTRS(data, get_env(child)); }; }; }
[Editorial note: Change [exec.on]/p7 as follows:]
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)));return SCHED-ENV(std::forward_like<OutSndr>(data), std::forward<Env>(env)); } else { return std::forward<Env>(env); }