There have been various suggestions that Senders and Receivers need a new language feature for customization points, to avoid the complexity of ADL tag_invoke.
This paper makes the case that C++ already has such a language facility, and it works just fine for the purposes of Senders and Receivers.
That language facility is member functions.
In a nutshell, the approach in this paper is relatively straightforward; for all non-query customization points, ADL tag_invoke overloads become member functions. Query customization points become query member functions that take the query tag as an argument.
This is because non-queries don't need to forward calls to customization points, but it's useful for queries to be able to forward queries.
In order to be able to write perfect-forwarding function templates that work both for lvalues and rvalues, we use deduced this. When there is no need to write a single function for both lvalues and rvalues, a traditional non-static member function will do.
A tag_invoke customization point for start
friend void tag_invoke(std::execution::start_t, recv_op& self) noexcept
becomes
void start() & noexcept
A perfect-forwarding connect
template <__decays_to<__t> _Self, receiver _Receiver> requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>> friend auto tag_invoke(std::execution::connect_t, _Self&& __self, _Receiver __rcvr)
becomes
template <__decays_to<__t> _Self, receiver _Receiver> requires sender_to<__copy_cvref_t<_Self, _Sender>, __receiver<_Receiver>> auto connect(this _Self&& __self, _Receiver __rcvr)
The call
tag_invoke(std::execution::connect, std::forward<Snd>(s), r);
becomes
std::forward<Snd>(s).connect(r);
A query
friend in_place_stop_token tag_invoke(std::execution::get_stop_token_t, const __t& __self) noexcept
becomes
in_place_stop_token query(std::execution::get_stop_token_t) const noexcept
After an LEWG discussion where it was suggested that tag parameters/arguments are untoward, and a very helpful suggestion that if we have wrappers anyway, we can use nested types instead, various people discussing this came to the conclusion that that feedback is right - we don't need the tags, except for query. We can add member typedef opt-ins to operation states, like we already have in receivers, and then we don't need those tag parameters/arguments.
Furthermore, we don't need to name the query function a "tag_query". It's a query, it takes a tag, but that tag-taking doesn't need to go into the name. It's a member function. If you manage to mix such functions in a wrapper class, don't do it. Don't multi-inherit things into your sender wrapper, don't multi-inherit a sender wrapper and something else. Or if you do, use whatever usual techniques to disambiguate declarations and calls, but mostly just don't do it.
First of all, two things, both rather major:
A bit of elaboration on the second point: consider that earlier query of get_stop_token in tag_invoke form. It's an example of that query for the when_all algorithm. But what needs to be done is that both that query (which is a hidden friend) and the when_all_t function object type are in a detail-namespace, and then outside that namespace, in namespace std::execution, the type is brought into scope with a using-declaration, and the actual function object is defined.
Roughly like this:
namespace you_will_have_trouble_coming_up_with_a_name_for_it { template <class Snd, class Recv, class Fn> struct my_then_operation { opstate op; struct t { friend void tag_invoke(start_t, t& self) noexcept { start(self.op_); } }; }; // ADL-protected internal senders and receivers omitted struct my_then_t { template <sender Snd, class Fn> // proper constraints omitted sender auto operator()(Snd&& sndr, Fn&& fn) const { // the actual implementation omitted } }; } using you_will_have_trouble_coming_up_with_a_name_for_it::my_then_t; constexpr my_then_t my_then{};
This has the effect of keeping the overload set small, when each and every type and its customizations are meticulously defined that way. Build times are decent, the sizes of overload sets are nicely controlled and are small, diagnostics for incorrect calls are hopefully fairly okay.
But that's not all there is to it. Generic code that uses such things should wrap its template parameters into utilities that prevent ADL via template parameters. You might see something like this gem:
// For hiding a template type parameter from ADL template <class _Ty> struct _X { using __t = struct _T { using __t = _Ty; }; }; template <class _Ty> using __x = __t<_X<_Ty>>;
and then use it like this:
using make_stream_env_t = stream_env<stdexec::__x<BaseEnv>>;
With member customization points, you don't need any such acrobatics.
The customization points are members. You define a customization point
as a member function, and you can just put your type directly into
whichever namespace you want (some might even use the global namespace),
and you don't need to use nested detail namespaces. Then you
call foo.connect(std::execution::connect, receiver);
and you
don't have to do the no-ADL wrapping in your template parameters either.
In other words, the benefits of avoiding ADL for the implementation include
Some of those are fairly traditional ADL-taming techniques, some may be recent realizations. None of them are necessary when members are used, none. This should greatly simplify the implementation. The benefits for the users are mostly the same, they don't need to apply any of those techniques, not for their custom schedulers, not for their custom senders, not for their algorithm customizations, not for anything.
The definition of customization points is much simpler, to a ridiculous extent. Using them is simpler; it's a member call, everybody knows what that does, and many people know what scopes that looks in, and a decent amount of people appreciate the many scopes it *doesn't* look in.
Composition and reuse and wrapping of customization points becomes much easier, because it's just.. ..good old OOP, if you want to look at it that way. We're not introducing a new language facility for which you need to figure out how to express various function compositions and such, the techniques and patterns are decades old, and work here as they always worked.
Well, we don't do anything for users who for some reason _have_ to use ADL customization points. But the reason for going for this approach is that we decouple Senders and Receivers from an unknown quantity, and avoid many or even most of the problems of using ADL customization points.
Other than that, I'm not sure such downsides exist.
A common concern with using wrappers is that they don't work if you have existing APIs that use the wrappees - introducing wrappers into such situations just doesn't work, because they simply aren't the same type, and can't be made the same type. And a further problem is having to deal with both the wrappers and wrappees as concrete types, and figuring out when to use which, and possibly having to duplicate code to deal with both.
The saving grace with Senders and Receivers is that they are wrapped everywhere all the time. Algorithms wrap senders, the wrapped senders wrap their receivers, and resulting operation states. This wrapping nests pretty much infinitely.
For cases where you need to use a concrete sender, it's probably type-erased, rather than being a use of a concrete target sender.
A very partial work-in-progress implementation exists as a branch of the reference implementation of P2300, at https://github.com/villevoutilainen/wg21_p2300_std_execution/tree/P2855_member_customization_points.
The implementation has the beginnings of a change from ADL tag_invoke overloads to non-static member functions and member functions using deduced this. It's rather rudimentary, and very incomplete, only covering operation states at this point.
It's possible to make customization point members private, and have them usable by the framework, by befriending the entry point (e.g. std::execution::connect, in a member connect(std::execution::connect_t)). It's perhaps ostensibly rare to need to do that, considering that it's somewhat unlikely that a sender wrapper or an operation state wrapper that provides the customization point would have oodles of other functionality. Nevertheless, we have made that possible in the prototype implementation, so we could do the same in the standard. This seems like an improvement over the ADL customization points. With them, anyone can do the ADL call, the access of a hidden friend doesn't matter.
It's sometimes plausible that a class with a member customization point inherits another class that provides the same customization point, and it's not an override of a virtual function. In such situations, the traditional technique works, bring in the base customization point via a using-declaration, which silences possible hiding warnings and also create an overload set. The expectation is that the situation and the technique are sufficiently well-known, since it's old-skool.
General note: the goal here is to replace tag_invokes with member functions, and remove all ADL-mitigating techniques; there are other changes I deemed necessary at least for this presentation: none of the CPOs are meant to be called unqualified after this paper's change(s). They are not customizable as such, in and of themselves, despite being CPOs. They are entry points. The entry points are called qualified (and in some cases have to be; you can't just call a foo.connect() on any coroutine result type, but you can call std::execution::connect() on it.), and they are customized by the mechanism depicted in P2999, if the thing customized is an algorithm, or by writing member functions, if the thing customized is not really a customization but rather an opt-in.
But note, though, that once the adoption of this paper's approach is done, we don't have to qualify anything in this specification, because all calls to namespace-scope functions are as-if qualified, and the rest is member calls.
Additionally, it might be tempting to remove the function objects set_value, set_error and set_stopped completely, but there are things that use them as generic function objects (see just-sender below), so that ability is left as-is.
Due to not using ADL, 16.4.6.17 Class template-heads can be removed, as it's an ADL-mitigating technique that isn't necessary when member functions are used for everything.
In [functional.syn], strike tag_invocable, nothrow_tag_invocable, tag_invoke_result, and tag_invoke:
// [func.tag_invoke], tag_invoke namespace tag-invoke { // exposition only void tag_invoke(); template<class Tag, class... Args> concept tag_invocable = requires (Tag&& tag, Args&&... args) { tag_invoke(std::forward<Tag>(tag), std::forward<Args>(args)...); }; template<class Tag, class... Args> concept nothrow_tag_invocable = tag_invocable<Tag, Args...> && requires (Tag&& tag, Args&&... args) { { tag_invoke(std::forward<Tag>(tag), std::forward<Args>(args)...) } noexcept; }; template<class Tag, class... Args> using tag_invoke_result_t = decltype(tag_invoke(declval<Tag>(), declval<Args>()...)); template<class Tag, class... Args> struct tag_invoke_result<Tag, Args...> { using type = tag_invoke_result_t<Tag, Args...>; // present if and only if tag_invocable<Tag, Args...> is true }; struct tag; // exposition only } inline constexpr tag-invoke::tag tag_invoke {}; using tag-invoke::tag_invocable; using tag-invoke::nothrow_tag_invocable; using tag-invoke::tag_invoke_result_t; using tag-invoke::tag_invoke_result;template<auto& Tag> using tag_t = decay_t<decltype(Tag)>;
Remove [func.tag_invoke]
In [exec.general]/p4.1, replace the specification of the exposition-only
mandate-nothrow-call
with the following:
For a subexpression
expr
, letMANDATE-NOTHROW(expr)
be expression-equivalent toexpr
.Mandates:
noexcept(expr)
istrue
.
In [exec.syn], remove ADL-protecting nested namespaces:
namespace queries { // exposition onlystruct forwarding_query_t; struct get_allocator_t; struct get_stop_token_t;} using queries::forwarding_query_t; using queries::get_allocator_t; using queries::get_stop_token_t;namespace std::execution { // [exec.queries], queries enum class forward_progress_guarantee;namespace queries { // exposition onlystruct get_domain_t; struct get_scheduler_t; struct get_delegatee_scheduler_t; struct get_forward_progress_guarantee_t; template<class CPO> struct get_completion_scheduler_t;} using queries::get_domain_t; using queries::get_scheduler_t; using queries::get_delegatee_scheduler_t; using queries::get_forward_progress_guarantee_t; using queries::get_completion_scheduler_t;inline constexpr get_domain_t get_domain{}; inline constexpr get_scheduler_t get_scheduler{}; inline constexpr get_delegatee_scheduler_t get_delegatee_scheduler{}; inline constexpr get_forward_progress_guarantee_t get_forward_progress_guarantee{}; template<class CPO> inline constexpr get_completion_scheduler_tget_completion_scheduler{}; namespace exec-envs { // exposition onlystruct empty_env {}; struct get_env_t;} using envs-envs::empty_env; using envs-envs::get_env_t;// [exec.domain.default], domains struct default_domain; // [exec.sched], schedulers struct scheduler_t {}; template<class Sch> concept scheduler = see below; //...namespace receivers { // exposition onlystruct set_value_t; struct set_error_t; struct set_stopped_t;} using receivers::set_value_t; using receivers::set_error_t; using receivers::set_stopped_t;// ...namespace op-state { // exposition onlystruct start_t;} using op-state::start_t;struct operation_state_t {}; // ...namespace completion-signatures { // exposition onlystruct get_completion_signatures_t;} using completion-signatures::get_completion_signatures_t;// ...namespace senders-connect { // exposition onlystruct connect_t;} using senders-connect::connect_t;// ...namespace senders-factories { // exposition onlystruct just_t; struct just_error_t; struct just_stopped_t; struct schedule_t;}using senders-factories::just_t;using senders-factories::just_error_t;using senders-factories::just_stopped_t;using senders-factories::schedule_t;inline constexpr just_t just{}; inline constexpr just_error_t just_error{}; inline constexpr just_stopped_t just_stopped{}; inline constexpr schedule_t schedule{}; inline constexpr unspecified read{}; // ...namespace sender-adaptor-closure { // exposition onlytemplate<class-type D> struct sender_adaptor_closure { };} using sender-adaptor-closure::sender_adaptor_closure;namespace sender-adaptors { // exposition onlystruct on_t; struct transfer_t; struct schedule_from_t; struct then_t; struct upon_error_t; struct upon_stopped_t; struct let_value_t; struct let_error_t; struct let_stopped_t; struct bulk_t; struct split_t; struct when_all_t; struct when_all_with_variant_t; struct into_variant_t; struct stopped_as_optional_t; struct stopped_as_error_t; struct ensure_started_t;} using sender-adaptors::on_t; using sender-adaptors::transfer_t; using sender-adaptors::schedule_from_t; using sender-adaptors::then_t; using sender-adaptors::upon_error_t; using sender-adaptors::upon_stopped_t; using sender-adaptors::let_value_t; using sender-adaptors::let_error_t; using sender-adaptors::let_stopped_t; using sender-adaptors::bulk_t; using sender-adaptors::split_t; using sender-adaptors::when_all_t; using sender-adaptors::when_all_with_variant_t; using sender-adaptors::into_variant_t; using sender-adaptors::stopped_as_optional_t; using sender-adaptors::stopped_as_error_t; using sender-adaptors::ensure_started_t;// ...namespace sender-consumers { // exposition onlystruct start_detached_t;} using sender-consumers::start_detached_t;// ... } namespace std::this_thread { // [exec.queries], queriesnamespace queries { // exposition onlystruct execute_may_block_caller_t;} using queries::execute_may_block_caller_t;inline constexpr execute_may_block_caller_t execute_may_block_caller{};namespace this-thread { // exposition onlystruct sync-wait-env; // exposition only template<class S> requires sender_in<S, sync-wait-env> using sync-wait-type = see below; // exposition only template<class S> using sync-wait-with-variant-type = see below; // exposition only struct sync_wait_t; struct sync_wait_with_variant_t;} using this-thread::sync_wait_t; using this-thread::sync_wait_with_variant_t;} namespace std::execution { // [exec.execute], one-way executionnamespace execute { // exposition onlystruct execute_t;} using execute::execute_t;inline constexpr execute_t execute{}; // [exec.as.awaitable]namespace coro-utils { // exposition onlystruct as_awaitable_t;} using coro-utils::as_awaitable_t;// [exec.with.awaitable.senders] template<class-type Promise> struct with_awaitable_senders; }
In [exec.get.env]/1, edit as follows:
execution::get_env is a customization point object. For some subexpression o of type O, execution::get_env(o) is expression-equivalent totag_invoke(std::get_env,const_cast<const O&>(o).get_env() if that expression is well-formed.
In [exec.fwd.env]/2.1, edit the expression form:
mandate-nothrow-call(tag_invoke, std::forwarding_query, q)MANDATE-NOTHROW(q.query(std::forwarding_query)) if that expression is well-formed.
In [exec.get.allocator]/2, edit as follows:
The name std::get_allocator denotes a query object. For some subexpression r, std::get_allocator(r) is expression-equivalent tomandate-nothrow-call(tag_invoke, std::get_allocator, as_const(r))MANDATE-NOTHROW(as_const(r).query(std::get_allocator)).
In [exec.get.stop.token]/2, edit as follows:
The name std::get_stop_token denotes a query object. For some subexpression r, std::get_stop_token(r) is expression-equivalent to:mandate-nothrow-call(tag_invoke, std::get_stop_token, as_const(r))MANDATE-NOTHROW(as_const(r).query(std::get_stop_token)), if this expression is well-formed.
In [exec.get.scheduler]/2, edit as follows:
The name execution::get_scheduler denotes a query object. For some subexpression r, execution::get_scheduler(r) is expression-equivalent tomandate-nothrow-call(tag_invoke, get_scheduler, as_const(r))MANDATE-NOTHROW(as_const(r).query(execution::get_scheduler)).
In [exec.get.scheduler]/4, edit as follows:
execution::get_scheduler() (with no arguments) is expression-equivalent to execution::read(execution::get_scheduler)
In [exec.get.delegatee.scheduler]/2, edit as follows:
The name execution::get_delegatee_scheduler denotes a query object. For some subexpression r, execution::get_delegatee_scheduler(r) is expression-equivalent tomandate-nothrow-call(tag_invoke, get_delegatee_scheduler, as_const(r))MANDATE-NOTHROW(as_const(r).query(execution::get_delegatee_scheduler)).
In [exec.get.forward.progress.guarantee]/2, edit as follows:
The name execution::get_forward_progress_guarantee denotes a query object. For some subexpression s, let S be decltype((s)). If S does not satisfy scheduler, get_forward_progress_guarantee is ill-formed. Otherwise, execution::get_forward_progress_guarantee(s) is expression-equivalent to:mandate-nothrow-call(tag_invoke, get_forward_progress_guarantee, as_const(s))MANDATE-NOTHROW(as_const(s).query(execution::get_forward_progress_guarantee)), if this expression is well-formed.
In [exec.execute.may.block.caller]/2.1, edit the expression form:
mandate-nothrow-call(tag_invoke, this_thread::execute_may_block_caller, as_const(s))MANDATE-NOTHROW(as_const(s).query(this_thread::execute_may_block_caller)), if this expression is well-formed.
In [exec.completion.scheduler]]/2, edit as follows:
The name execution::get_completion_scheduler denotes a query object template. For some subexpression q, let Q be decltype((q)). If the template argument Tag in get_completion_scheduler<Tag>(q) is not one of set_value_t, set_error_t, or set_stopped_t, get_completion_scheduler<Tag>(q) is ill-formed. Otherwise, execution::get_completion_scheduler<Tag>(q) is expression-equivalent tomandate-nothrow-call(tag_invoke, get_completion_scheduler<Tag>, as_const(q))MANDATE-NOTHROW(as_const(q).query(execution::get_completion_scheduler<Tag>)) if this expression is well-formed.
In [exec.sched]/1, edit as follows:
template<class Sch> inline constexpr bool enable-scheduler = // exposition only requires { requires derived_from<typename Sch::scheduler_concept, scheduler_t>; }; template<class Sch> concept scheduler = enable-scheduler<remove_cvref_t<Sch>> && queryable<Sch> && requires(Sch&& sch, const get_completion_scheduler_t<set_value_t> tag) { { schedule(std::forward<Sch>(sch)) } -> sender; {tag_invoke(tag, std::get_env(execution::get_env(execution::schedule(std::forward<Sch>(sch))).query(tag)))} -> same_as<remove_cvref_t<Sch>>; } && equality_comparable<remove_cvref_t<Sch>> && copy_constructible<remove_cvref_t<Sch>>;
In [exec.recv.concepts]/1, edit as follows:
template<class Rcvr> inline constexpr boolenable_receiverenable-receiver = // exposition only requires { requires derived_from<typename Rcvr::receiver_concept, receiver_t>; }; template<class Rcvr> concept receiver =enable_receiverenable-receiver<remove_cvref_t<Rcvr>> && requires(const remove_cvref_t<Rcvr>& rcvr) { { execution::get_env(rcvr) } -> queryable; } && move_constructible<remove_cvref_t<Rcvr>> && // rvalues are movable, and constructible_from<remove_cvref_t<Rcvr>, Rcvr>; // lvalues are copyable
Strike [exec.recv.concepts]/2:
Remarks: Pursuant to [namespace.std], users can specialize enable_receiver to true for cv-unqualified program-defined types that model receiver, and false for types that do not. Such specializations shall be usable in constant expressions ([expr.const]) and have type const bool.
In [exec.set.value]/1, edit as follows:
execution::set_value is a value completion function ([async.ops]). Its associated completion tag is execution::set_value_t. The expression execution::set_value(R, Vs...) for some subexpression R and pack of subexpressions Vs is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is expression-equivalent tomandate-nothrow-call(tag_invoke, set_value, R, Vs...)MANDATE-NOTHROW(R.set_value(Vs...)).
In [exec.set.error]/1, edit as follows:
execution::set_error is an error completion function. Its associated completion tag is execution::set_error_t. The expression execution::set_error(R, E) for some subexpressions R and E is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is expression-equivalent tomandate-nothrow-call(tag_invoke, set_error, R, E)MANDATE-NOTHROW(R.set_error(E)).
In [exec.set.stopped]/1, edit as follows:
execution::set_stopped is a stopped completion function. Its associated completion tag is execution::set_stopped_t. The expression execution::set_stopped(R) for some subexpression R is ill-formed if R is an lvalue or a const rvalue. Otherwise, it is expression-equivalent tomandate-nothrow-call(tag_invoke, set_stopped, R)MANDATE-NOTHROW(R.set_stopped()).
In [exec.opstate]/1, edit as follows:
template<class O> inline constexpr bool enable-operation-state = // exposition only requires { requires derived_from<typename O::operation_state_concept, operation_state_t>; }; template<class O> concept operation_state = enable-operation-state<O> && queryable<O> && is_object_v<O> && requires (O& o) { { execution::start(o) } noexcept; };
In [exec.snd.concepts]/1, edit as follows:
template<class Sndr> inline constexpr boolenable_senderenable-sender = // exposition only requires { requires derived_from<typename Sndr::sender_concept, sender_t>; }; template<is-awaitable<env-promise<empty_env>> Sndr> // [exec.awaitables] inline constexpr boolenable_senderenable-sender<Sndr> = true; template<class Sndr> concept sender =enable_senderenable-sender<remove_cvref_t<Sndr>> && requires (const remove_cvref_t<Sndr>& sndr) { { execution::get_env(sndr) } -> queryable; } && move_constructible<remove_cvref_t<Sndr>> && // rvalues are movable, and constructible_from<remove_cvref_t<Sndr>, Sndr>; // lvalues are copyable template<class Sndr, class Env = empty_env> concept sender_in = sender<Sndr> && requires (Sndr&& sndr, Env&& env) { { execution::get_completion_signatures(std::forward<Sndr>(sndr), std::forward<Env>(env)) } -> valid-completion-signatures; }; template<class Sndr, class Rcvr> concept sender_to = sender_in<Sndr, env_of_t<Rcvr>> && receiver_of<Rcvr, completion_signatures_of_t<Sndr, env_of_t<Rcvr>>> && requires (Sndr&& sndr, Rcvr&& rcvr) { execution::connect(std::forward<Sndr>(sndr), std::forward<Rcvr>(rcvr)); };
Strike [exec.snd.concepts]/3:
Remarks: Pursuant to [namespace.std], users can specialize enable_sender to true for cv-unqualified program-defined types that model sender, and false for types that do not. Such specializations shall be usable in constant expressions ([expr.const]) and have type const bool.
In [exec.snd.concepts]/6, edit as follows:
Library-provided sender types: - Always expose an overload of acustomization ofmember connect that accepts an rvalue sender. - Only expose an overload of acustomization ofmember connect that accepts an lvalue sender if they model copy_constructible. - Model copy_constructible if they satisfy copy_constructible.
In [exec.awaitables]/5, edit as follows:
template<class T, class Promise> concept has-as-awaitable = requires (T&& t, Promise& p) { { std::forward<T>(t).as_awaitable(p) } -> is-awaitable<Promise&>; }; template<classhas-as-awaitable<Derived> T>requires tag_invocable<as_awaitable_t, T, Derived&>auto await_transform(T&& value) noexcept(noexcept(std::forward<T>(value).as_awaitable(declval<Derived&>())))noexcept(nothrow_tag_invocable<as_awaitable_t, T, Derived&>)->tag_invoke_result_t<as_awaitable_t, T, Derived&>decltype(std::forward<T>(value).as_awaitable(declval<Derived&>())) { returntag_invoke(as_awaitable,std::forward<T>(value),.as_awaitable(static_cast<Derived&>(*this)); }
In [exec.awaitables]/6, edit as follows:
template<class Env> struct env-promise : with-await-transform<env-promise<Env>> { unspecified get_return_object() noexcept; unspecified initial_suspend() noexcept; unspecified final_suspend() noexcept; void unhandled_exception() noexcept; void return_void() noexcept; coroutine_handle<> unhandled_stopped() noexcept;friend const Env& tag_invoke(get_env_t, const env-promise&) noexcept;const Env& get_env() const noexcept; };
In [exec.getcomplsigs]/1, edit as follows:
execution::get_completion_signatures is a customization point object. Let s be an expression such that decltype((s)) is S, and let e be an expression such that decltype((e)) is E. Then execution::get_completion_signatures(s, e) is expression-equivalent to:
tag_invoke_result_t<get_completion_signatures_t, S, E>{}decltype(s.get_completion_signatures(e)){} if that expression is well-formed,- ...
In [exec.connect]/3, edit as follows:
Let connect-awaitable-promise be the following class: struct connect-awaitable-promise : with-await-transform<connect-awaitable-promise> { DR& rcvr; // exposition only connect-awaitable-promise(DS&, DR& r) noexcept : rcvr(r) {} suspend_always initial_suspend() noexcept { return {}; } [[noreturn]] suspend_always final_suspend() noexcept { std::terminate(); } [[noreturn]] void unhandled_exception() noexcept { std::terminate(); } [[noreturn]] void return_void() noexcept { std::terminate(); } coroutine_handle<> unhandled_stopped() noexcept { execution::set_stopped((DR&&) rcvr); return noop_coroutine(); } operation-state-task get_return_object() noexcept { return operation-state-task{ coroutine_handle<connect-awaitable-promise>::from_promise(*this)}; }friendenv_of_t<const DR&>tag_invokeget_env(get_env_t, const connect-awaitable-promise& self) const noexcept { return execution::get_env(self.rcvr); } };
In [exec.connect]/4, edit as follows:
Let operation-state-task be the following class: struct operation-state-task { using promise_type = connect-awaitable-promise; coroutine_handle<> coro; // exposition only explicit operation-state-task(coroutine_handle<> h) noexcept : coro(h) {} operation-state-task(operation-state-task&& o) noexcept : coro(exchange(o.coro, {})) {} ~operation-state-task() { if (coro) coro.destroy(); }friendvoidtag_invoke(start_t,start(operation-state-task& self) & noexcept {self.coro.resume(); } };
In [exec.connect]/6, edit as follows:
If S does not satisfy sender or if R does not satisfy receiver, execution::connect(s, r) is ill-formed. Otherwise, the expression execution::connect(s, r) is expression-equivalent to:
tag_invoke(connect, s, r)s.connect(r)if connectable-with-tag-invoke<S, R> is modeledif that expression is well-formed. Mandates: The type of thetag_invokeexpression above satisfies operation_state.- Otherwise, connect-awaitable(s, r) if that expression is well-formed.
- Otherwise, execution::connect(s, r) is ill-formed.
In [exec.adapt.general]3,4,5, edit as follows:
Unless otherwise specified, a sender adaptor is required to not begin executing any functions that would observe or modify any of the arguments of the adaptor before the returned sender is connected with a receiver using execution::connect, and execution::start is called on the resulting operation state. This requirement applies to any function that is selected by the implementation of the sender adaptor. Unless otherwise specified, a parent sender ([async.ops]) with a single child sender s has an associated attribute object equal to FWD-QUERIES(execution::get_env(s)) ([exec.fwd.env]). Unless otherwise specified, a parent sender with more than one child senders has an associated attributes object equal to empty_env{}. These requirements apply to any function that is selected by the implementation of the sender adaptor. Unless otherwise specified, when a parent sender is connected to a receiver r, any receiver used to connect a child sender has an associated environment equal to FWD-QUERIES(execution::get_env(r)). This requirements applies to any sender returned from a function that is selected by the implementation of such sender adaptor.
Strike [exec.adapt.general]6:
For any sender type, receiver type, operation state type, queryable type, or coroutine promise type that is part of the implementation of any sender adaptor in this subclause and that is a class template, the template arguments do not contribute to the associated entities ([basic.lookup.argdep]) of a function call where a specialization of the class template is an associated entity. [Example:...
For the sender adapters, the changes from P2999 regarding how algorithms are customized already removes tag_invokes, so no further changes are needed to them.
Change [exec.as.awaitable]2 as follows:
as_awaitable is a customization point object. For some subexpressions expr and p where p is an lvalue, Expr names the type decltype((expr)) and Promise names the type decltype((p)), as_awaitable(expr, p) is expression-equivalent to the following:tag_invoke(as_awaitable, expr, p)expr.as_awaitable(p) if that expression is well-formed. Mandates: is-awaitable<A, Promise> is true, where A is the type of thetag_invokeexpression above.