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> is true, then
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 with schedule_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 to continues_on. That is why it is given special treatment here. — end
note]
- Otherwise,
where Domain is the first of the following expressions that is well-formed and whose type is not void:
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 is completes_on(Pred,Sch), return the domain of Pred and use domain of env as a fallback.
- when
sndr is schedule_from(Sch,Pred), return the domain of Sch and use default_domain as a fallback (ignoring env).
- otherwise, return the domain of
sndr and use domain of env as a fallback.
-
Change the SCHED-ATTRS and SCHED-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 the get_domain query.
-
Change all uses of SCHED-ATTRS and SCHED-ENV as appropriate.
Proposed Resolution
[Editorial note: Change [exec.snd.expos]/p6 as follows:]
-
For a scheduler sch and environment env,
SCHED-ATTRS(sch, env) is an expression o1 whose type
satisfies queryable such that: [Editorial note: reformatted as a list.]
-
o1.query(get_completion_scheduler<Tag
set_value_t>) is an expression with the same type and value as
sch where Tag is one of set_value_t or set_stopped_t, and such
that
-
o1.query(get_completion_scheduler<Tag>) is ill-formed for Tag
other than set_value_t,
-
o1.query(get_domain) is expression-equivalent to sch.query(get_domain),
and
-
For a pack of subexpressions as and query object Q such that
forwarding_query(Q) is true, o1.query(Q, as...) is
expression-equivalent to env.query(Q, as...).
SCHED-ATTRS(sch) is expression-equivalent to
SCHED-ATTRS(sch, execution::env<>{}).
SCHED-ENV(sch, env) is an expression o2 whose type
satisfies queryable such that: [Editorial note: reformatted as a list.]
-
o2.query(get_scheduler) is a prvalue with the same type and value as sch, and
such that
-
o2.query(get_domain) is expression-equivalent to
sch.query(get_domain)., and
-
For a pack of subexpressions as and query object Q such that
forwarding_query(Q) is true, o1.query(Q, as...) is
expression-equivalent to env.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 not void:
get_domain(get_env(sndr))
completion-domain<void>(sndr)
get_domain(env)
get_domain(get_scheduler(env))
default_domain()
-
If sender-for<Sndr,
continues_on_tschedule_from_t> is true, then
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_onschedule_from algorithm works
in tandem with schedule_from ([exec.schedule.from])continues_on
([exec.continues.on]) to give scheduler authors a way to customize both how
to transition ontooff of (continues_on) and off
ofonto (schedule_from) a given execution context. Thus,
continues_onschedule_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_onschedule_from. That is why it is given
special treatment here. — end note]
-
Otherwise, if sender-for<Sndr, continues_on_t> is
true, then
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)
where Domain is the first of the following expressions that is well-formed and
whose type is not void:
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 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)));
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);
}
Background:
continues_onandschedule_from:std::executionhas two customizable algorithms for transfering execution from one context to another:continues_onandschedule_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_fromalgorithm looks for customizations based on the domain of the destination, and thecontinues_onalgorithm 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. Thecontinues_onalgorithm is required to lower to the result of a call toschedule_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_onandschedule_fromcustomizations points: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_onuses the domain of the source to find its customization. And by asking for the scheduler's domain,schedule_fromuses the domain of the destination.The final piece is the transformation, within the
connectcustomization point, of thecontinues_onsender to theschedule_fromsender, which is done with thecontinues_on.transform_sender(Sndr, Env)member function (see [exec.continues.on] p5).Problem 1
When
connect-time customization was added tostd::executionin[@P3303], the logic of
continues_on/schedule_fromcustomization accidentally got reversed: The exposition-only
get-domain-latefunction,which is called from
connect, is used to determine the domain to use to find a sendertransform function. It says:
Paragraph 14.1 above gets the roles of
continues_onandschedule_frommixed 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)returnstransform_sender(get-domain-early(sndr), make-sender(then, fn, sndr)); i.e., the domain is pulled fromsndr. 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 thethensender will start.If we look at the
connectcustomization point at how a late customization is found, we see that before it does anything else, it transforms the input sender as follows:We can see that when passed a
thensender, we ask thethensender for its domain (and use the domain of the receiver's env as a fallback). That means that forthensenders,connectdispatches to a customization based on the domain of thethensender itself. That is different from early customization, which used the domain of the predecessor. The inconsistency is not intentional.For
thenand most other adaptors, it doesn't make any difference. Thethensender completes wherever its predecessor completes, so the "start" and "complete" domains are the same. That is not the case, however, forcontinues_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
connectuses the wrong domain to find a customization forcontinues_on. The domain of thecontinues_on(sndr, sched)sender is the domain on which it will complete, which is the domain ofsched. Butcontinues_onshould 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_domainandget_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 theschedule_fromsender: early or late, it should always use the domain of the destination scheduler). So we can leaveconnectalone and treatcontinues_onas special inget-domain-late.Back to First Principles
Given what we already know about why
schedule_fromandcontinues_onboth 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 expressionA | 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) | Bget_domain(get_env(ALGO(A...)))should return...schedule_fromSchordefault_domainSchordefault_domainSchcontinues_onAordefault_domainAor domain of EnvSchBSchordefault_domainSchor domain of EnvWe can use this table to correct the effected parts of the spec:
get-domain-late(sndr, env)should be changed to:sndriscompletes_on(Pred,Sch), return the domain ofPredand use domain ofenvas a fallback.sndrisschedule_from(Sch,Pred), return the domain ofSchand usedefault_domainas a fallback (ignoringenv).sndrand use domain ofenvas a fallback.Change the
SCHED-ATTRSandSCHED-ENVpseudo-macros from [exec.snd.expos] toaccept a fallback environment in addition to a scheduler, but never use the fallback
env to answer the
get_domainquery.Change all uses of
SCHED-ATTRSandSCHED-ENVas appropriate.Proposed Resolution
[Editorial note: Change [exec.snd.expos]/p6 as follows:]
[Editorial note: Change [exec.snd.expos]/p14 as follows:]
[Editorial note: Change [exec.starts.on]/p4.1 as follows:]
[Editorial note: Change [exec.continues.on]/p4 as follows:]
[Editorial note: Change [exec.on]/p7 as follows:]