diff --git a/3815.md b/3815.md new file mode 100644 index 0000000..8304ad4 --- /dev/null +++ b/3815.md @@ -0,0 +1,881 @@ +--- +title: Add `scope_association` concept to P3149 +document: P3815R0 +date: today +audience: + - LEWG Library Evolution Working Group +author: + - name: Ian Petersen + email: + - name: Jessica Wong + email: +toc: true +--- +Changes +======= + +## R0 +- First revision + + +Background and Motivation +========================= + +[@P3149R11] was approved for C++26 by WG21. The paper introduces two scope types, `simple_counting_scope` and +`counting_scope`, along with several basis operations, including `associate`, `spawn`, and `spawn_future`. In R11, the +example implementations of these facilities are expressed in terms of the `scope_token` concept: + +```cpp +template +concept scope_token = + copyable && + requires(const Token token) { + { token.try_associate() } -> same_as; + { token.disassociate() } noexcept -> same_as; + { token.wrap(declval()) } -> sender_in; + }; +``` + +During the development of these sample implementations, it was observed that reintroducing the `scope_association` +concept from [@P3149R7] would yield several benefits: + +- reduced binary size for both scopes and basis operations, +- fewer move/copy operations, +- and a simplified implementation model that in turn facilitates easier customization. + +These improvements can be achieved without any impact on the user-facing APIs proposed in R11. + +Illustrated below are the R11 implementations of `spawn` and `associate` in contrast with the `scope_association` +concept implementation. + +## execution::spawn +:::cmptable +### Before +```cpp +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = + connect_result_t; + + @_spawn-state_@(Alloc a, Sender&& sndr, Token t) + : @_alloc_@(std::move(a)), + @_op_@(connect(std::move(sndr), + @_spawn-receiver_@{this})), + @_token_@(std::move(t)) {} + + void run() noexcept { + if (@_token_@.try_associate()) + @_op_@.start(); + else + destroy(); + } + + void complete() noexcept override { + auto t = std::move(@_token_@); + destroy(); + t.disassociate(); + } +}; +``` +### After +```cpp +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = + connect_result_t; + using @_assoc-t_@ = remove_cvref_t().try_associate())>; + + @_spawn-state_@(Alloc a, Sender&& sndr, Token t) + : @_alloc_@(std::move(a)), + @_op_@(connect( + std::move(sndr), + @_spawn-receiver_@{this})) { + @_assoc_@ = t.try_associate(); + } + + void run() noexcept { + if (assoc) + @_op_@.start(); + else + destroy(); + } + + void complete() noexcept override { + auto a = std::move(assoc); + destroy(); + } +}; +``` +:::: + +## execution::associate +:::cmptable +### Before +```cpp +template +struct @_associate-data_@ { + explicit @_associate-data_@(Token t, Sender&& s) noexcept( + noexcept(t.wrap(std::forward(s))) && + noexcept(t.try_associate())) + : @_token_@(std::move(t)), + @_sndr_@(token.wrap(std::forward(s))) { + if (!@_token_@.try_associate()) { + @_sndr_@.reset(); + } + } + + @_associate-data_@(const @_associate-data_@& other) noexcept( + is_nothrow_copy_constructible_v<@_wrap-sender_@> && + noexcept(other.@_token_@.try_associate())) + requires copy_constructible<@_wrap-sender_@> + : token(other.token) { + if (other.sndr.has_value() && token.try_associate()) { + try { + sndr.emplace(*other.sndr); + } catch (...) { + token.disassociate(); + throw; + } + } + } + + @_associate-data_@(@_associate-data_@&& other) noexcept( + is_nothrow_move_constructible_v<@_wrap-sender_@>) + : @_sndr_@(std::move(other).@_sndr_@), + @_token_@(std::move(other).@_token_@) { + other.@_sndr_@.reset(); + } + + ~@_associate-data_@() { + @_sndr_@.reset(); + } + + optional> release() && noexcept( + is_nothrow_move_constructible_v<@_wrap-sender_@>) { + if (@_sndr_@) { + return optional{ + pair{std::move(@_token_@), std::move(*@_sndr_@)}}; + } else { + return nullopt; + } + } + +private: + optional<@_wrap-sender_@> @_sndr_@; + Token @_token_@; +}; +``` +### After +```cpp +template +struct @_associate-data_@ { + explicit @_associate-data_@(Token t, Sender&& s) noexcept( + noexcept(t.wrap(std::forward(s))) && + noexcept(t.try_associate())) + : @_sndr_@(t.wrap(std::forward(s))) { + sender_ref guard{addressof(sndr)}; + @_assoc_@ = t.try_associate(); + if (@_assoc_@) { + (void)guard.release(); + } + } + + @_associate-data_@(const @_associate-data_@& other) noexcept( + is_nothrow_copy_constructible_v<@_wrap-sender_@> && + is_nothrow_copy_constructible_v) + requires copy_constructible<@_wrap-sender_@> + : @_assoc_@(other.@_assoc_@) { + if (@_assoc_@) { + construct_at(addressof(@_sndr_@), other.@_sndr_@); + } + } + + @_associate-data_@(@_associate-data_@&& other) noexcept( + is_nothrow_move_constructible_v<@_wrap-sender_@>) + : @_associate-data_@(std::move(other).release()) {} + + ~@_associate-data_@() { + if (@_assoc_@) { + destroy_at(addressof(@_sndr_@)); + } + } + + pair release() && noexcept { + @_wrap-sender_@* p = @_assoc_@ ? addressof(@_sndr_@) : nullptr; + return pair{std::move(@_assoc_@), sender_ref{p}}; + } + + private: + assoc_t @_assoc_@; + union { + @_wrap-sender_@ @_sndr_@; + }; + + @_associate-data_@(pair parts) + : @_assoc_@(std::move(parts.first)) { + if (@_assoc_@) { + construct_at(addressof(@_sndr_@), std::move(parts.second)); + } + } +}; +``` +:::: + +Proposal +======== + +```cpp +template + concept scope_association = + movable && + default_initializable && + requires(Assoc assoc) { + { static_cast(assoc) } noexcept; + { assoc.try_associate() } -> same_as; + }; +``` +A type that models `scope_association` is an RAII handle that represents a possible association between a sender and an +async scope. If the scope association contextually converts to true then the object is “engaged” and represents an +association; otherwise, the object is “disengaged” and represents the lack of an association. Scope associations are +movable and not copyable, and expose a `try_associate` member function with semantics identical to the `try_associate` +member function on a type that models `scope_token`. + +The following are the proposed changes to `scope_token`, `associate`, `spawn`, `spawn_future`, `simple_counting_scope`, +and `counting_scope` with the adoption of `scope_association`. + +## `execution::scope_token` +The primary change to `scope_token` is to `try_associate`, which will return a `scope_association` rather than a +`bool`. + +```cpp +template + concept scope_token = + copyable && + requires(Token token) { + { token.try_associate() } -> scope_association; + { token.wrap(declval<@_test-sender_@>()) } -> sender_in<@_test-env_@>; + }; +``` + +The `try_associate` member function on a token attempts to create a new association with the scope; `try_associate` +returns an engaged association when the association is successful, and it may either return a disengaged association or +throw an exception to indicate failure. + +## `execution::associate` +With the application of the proposed changes, the copy behavior of the associate-sender returned from `associate` +becomes the following: + +If the sender, `snd`, provided to `associate()` is copyable then the resulting associate-sender is also copyable, with +the following rules: + +- copying an unassociated associate-sender invariably produces a new unassociated associate-sender; and +- copying an associated associate-sender requires copying the _`associate-data`_ it contains and the _`associate-data`_ +copy-constructor proceeds as follows: + + - The result of invoking the source's `@_association_@.try_associate()` will be passed to the destination +_`associate-data`_. + + - if the resulting association is engaged then copy the wrapped sender from the source into the destination +_`associate-data`_; the destination associate-sender is associated + - otherwise, the destination associate-sender is unassociated + +Furthermore, the _`operation-state`_’s destructor becomes the following: + +An _`operation-state`_ with its own association must invoke the association’s destructor as the last step of the +_`operation-state`_’s destructor. + +## `execution::spawn` +The behavior of `spawn` remains largely unchanged, with the primary difference being that _`op_t`_ now holds an +_`association`_ rather than a _`token`_. Upon completion of the _`operation-state`_, the destructor of the +_`association`_ is invoked, replacing the previous mechanism of explicitly calling `@_token_@.disassociate()` on the +local copy of the _`token`_. + +## `execution::spawn_future` +The changes to `spawn_future` reflect the same changes proposed in `spawn`. + +## `execution::simple_counting_scope` +The behavior of `simple_counting_scope` remains largely unchanged, with the primary difference being that the +disassociation is handled by the destructor of the association returned from `token.try_associate()`. + +## `execution::counting_scope` +The changes to `counting_scope` reflect the same changes proposed in `simple_counting_scope`. + +Wording +======= + +## Header `` synopsis [execution.syn]{.sref} + +To the `` synopsis [execution.syn]{.sref}, make the following change: + +``` + // [exec.scope] + // [exec.scope.concepts], scope concepts + @[template\ ]{.add}@ + @[concept\ scope_association\ =\ _see\ below_;]{.add}@ + + template + concept scope_token = @_see below_@; +``` + +## `execution::associate` + +To the subsection [exec.associate]{.sref}, make the following changes: + +[2]{.pnum} Let _`associate-data`_ be the following exposition-only class template: +``` +namespace std::execution { + +template +struct @_associate-data_@ { // @_exposition only_@ + using @_wrap-sender_@ = // @_exposition only_@ + remove_cvref_t().wrap(declval()))>; + @[using\ _assoc-t_\ =]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[decltype(declval().try_associate());]{.add}@ + + @[using\ _sender-ref_\ =]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[unique_ptr<_wrap-sender_,\ decltype(\[\](auto*\ p)\ noexcept\ {\ destroy_at(p);\ })>;]{.add}@ + + explicit @_associate-data_@(Token t, Sender&& s) + : @_sndr_@(t.wrap(std::forward(s)))@[,]{.rm}[\ {]{.add}@ + @[_token_(t)\ {]{.rm}@ + @[_sender-ref_\ guard{addressof(_sndr_)};]{.add}@ + + if (@[!_token_]{.rm}[_assoc_\ =\ t]{.add}@.try_associate()) + @[_sndr_.reset()]{.rm}[(void)guard.release()]{.add}@; + } + + @_associate-data_@(const @_associate-data_@& other) + noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && + noexcept(other.@[_token_]{.rm}[_assoc_]{.add}@.try_associate())); + + @_associate-data_@(@_associate-data_@&& other) + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); + + ~@_associate-data_@(); + + @[optional>]{.rm}@ + @[pair<_assoc-t_,\ _sender-ref_>]{.add}@ + release() && noexcept@[(is_nothrow_move_constructible_v<_wrap-sender_>)]{.rm}@; + +private: + @[optional<_wrap-sender_>\ _sndr_;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + @[Token\ _token_;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + + @[_associate-data_(pair<_assoc-t_,\ _scope-ref_>\ parts);]{.add}@ @[//\ _exposition\ only_]{.add}@ + + @[_assoc-t_\ _assoc_;]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[union\ {]{.add}@ + @[_wrap-sender_\ _sndr_;]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[};]{.add}@ +}; + +template + @_associate-data_@(Token, Sender&&) -> @_associate-data_@; + +} +``` + +[3]{.pnum} For an _`associate-data`_ object `a`, [`a.@_sndr_@.has_value()` is]{.rm} [`a.@_assoc_@` contextually converts +to]{.add} `true` if and only if an association was successfully made and is owned by `a`. + +``` +@_associate-data_@(const @_associate-data_@& other) + noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && + noexcept(other.@[_token_]{.rm}[_assoc_]{.add}@.try_associate())); +``` + +[4]{.pnum} _Constraints:_ `copy_constructible<@_wrap-sender_@>` is `true`. + +[5]{.pnum} _Effects:_ [Value-initializes _`sndr`_ and initializes _`token`_ with `other.@_token_@`. If +`other.@_sndr_@.has_value()` is `false`, no further effects; otherwise, calls `@_token_@.try_associate()` and, if that +returns `true`, calls `@_sndr_@.emplace(*other.@_sndr_@)` and, if that exits with an exception, calls +`@_token_@.disassociate()` before propagating the exception.]{.rm} [Initializes _`assoc`_ with +`other.@_assoc_@.try_associate()`. If _`assoc`_ contextually converts to `false`, no further effects; otherwise, +initializes _`sndr`_ with `other.@_sndr_@`.]{.add} + +``` +@_associate-data_@(@_associate-data_@&& other) + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); +``` + +[6]{.pnum} _Effects:_ [Initializes _`sndr`_ with `std::move(other.@_sndr_@)` and initializes _`token`_ with +`std::move(other.@_token_@)` and then calls `other.@_sndr_@.reset()`.]{.rm} [Equivalent to +`@_associate-data_@(std::move(other).release())`.]{.add} + +[`@_associate-data_@(pair<@_assoc-t_@, @_sender-ref_@> parts);`]{.add} + +[[7]{.pnum} _Effects:_ Initializes _`assoc`_ with `std::move(parts.first)`. If _`assoc`_ contextually converts to +`false`, no further effects; otherwise, initializes _`sndr`_ with `std::move(*parts.second)`.]{.add} + +`~@_associate-data_@();` + +[8]{.pnum} _Effects:_ [If `@_sndr_@.has_value()` returns `false` then no effect; otherwise, invokes `@_sndr_@.reset()` +before invoking `@_token_@.disassociate()`.]{.rm} [If _`assoc`_ contextually converts to `false` then no effect; +otherwise, destroys _`sndr`_.]{.add} + +[`optional>`]{.rm}\ +[`pair<@_assoc-t_@, @_scope-ref_@>`]{.add}\ +`@\ \ @release() && noexcept@[(is_nothrow_move_constructible_v<_wrap-sender_>)]{.rm}@;` + +[9]{.pnum} _Effects:_ [If `@_sndr_@.has_value()` returns `false` then returns an `optional` that does not contain a +value; otherwise returns an `optional` containing a value of type `pair` as if by:]{.rm} + +[`return optional(pair(@_token_@, std::move(*@_sndr_@)));`]{.rm} + +[Constructs an object `u` of type _`scope-ref`_ that is initialized with `nullptr` if _`assoc`_ +contextually converts to `false` and with `addressof(@_sndr_@)` otherwise, then returns +`pair{std::move(@_assoc_@), std::move(u)}`.]{.add} + +[[9]{.pnum} _Postconditions:_ _`sndr`_ does not contain a value.]{.rm} + +[10]{.pnum} The name `associate` denotes a pipeable sender adaptor object. For subexpressions `sndr` and `token`, if +`decltype((sndr))` does not satisfy `sender`, or `remove_cvref_t` does not satisfy `scope_token`, +then `associate(sndr, token)` is ill-formed. + +... + +[13]{.pnum} The member `@_impls-for_@::@_get-state_@` is initialized with a callable object equivalent to +the following lambda: +``` +[](Sndr&& sndr, Rcvr& rcvr) noexcept(@_see below_@) { + auto@[&&]{.add}@ [_, data] = std::forward(sndr); + + @[auto\ dataParts\ =\ std::move(data).release();]{.rm}@ + + @[using\ scope_token\ =\ decltype(dataParts->first);]{.rm}@ + @[using\ wrap_sender\ =\ decltype(dataParts->second);]{.rm}@ + + @[using\ associate_data_t\ =\ remove_cvref_t;]{.add}@ + @[using\ assoc_t\ =\ typename\ associate_data_t::_assoc-t_;]{.add}@ + @[using\ sender_ref_t\ =\ typename\ associate_data_t::_sender-ref_;]{.add}@ + + using op_t = connect_result_t<@[wrap_sender]{.rm}[typename\ sender_ref_t::element_type]{.add}@, Rcvr>; + + struct op_state { + @[bool]{.rm}[assoc_t]{.add}@ @_assoc_[_iated_\ =\ false]{.rm}@; // @_exposition only_@ + union { + Rcvr* @_rcvr_@; // @_exposition only_@ + @[struct\ {]{.rm}@ + @[scope_token\ _token_;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + @[\ \ ]{.rm}@op_t @_op_@; // @_exposition only_@ + @[}\ _assoc_;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + }; + + @[explicit\ op_state(Rcvr&\ r)\ noexcept]{.rm}@ + @[:\ _rcvr_(addressof(r))\ {}]{.rm}@ + + @[explicit\ op_state(scope_token\ tkn,\ wrap_sender&&\ sndr,\ Rcvr&\ r)\ try]{.rm}@ + @[:\ _associated_(true),]{.rm}@ + @[_assoc_(tkn,\ connect(std::move(sndr),\ std::move(r)))\ {]{.rm}@ + @[}]{.rm}@ + @[catch\ (...)\ {]{.rm}@ + @[tkn.disassociate();]{.rm}@ + @[throw;]{.rm}@ + @[}]{.rm}@ + + @[explicit\ op_state(pair\ parts,\ Rcvr&\ r)]{.add}@ + @[:\ _assoc_(std::move(parts.first))\ {]{.add}@ + @[if\ (_assoc_)]{.add}@ + @[::new\ (_voidify_(_op_))\ op_t{]{.add}@ + @[connect(std::move(*parts.second),\ std::move(r))};]{.add}@ + @[else]{.add}@ + @[rcvr\ =\ addressof(r);]{.add}@ + @[}]{.add}@ + + @[explicit\ op_state(associate_data_t&&\ ad,\ Rcvr&\ r)]{.add}@ + @[:\ op_state(std::move(ad).release(),\ r)\ {}]{.add}@ + + @[explicit\ op_state(const\ associate_data_t&\ ad,\ Rcvr&\ r)]{.add}@ + @[requires\ copy_constructible\]{.add}@ + @[:\ op_state(associate_data_t(ad).release(),\ r)\ {}]{.add}@ + + op_state(op_state&&) = delete; + + ~op_state() { + if (@_assoc[iated]{.rm}_@)@[\ {]{.rm}@ + @[_assoc_.]{.rm}_op_@.~op_t(); + @[_assoc_._token_.disassociate();]{.rm}@ + @[_assoc_._token_.~scope_token();]{.rm}@ + @[}]{.rm}@ + } + + void @_run_@() noexcept { // @_exposition only_@ + if (@_assoc[iated]{.rm}_@) + start(@[_assoc_.]{.rm}_op_@); + else + set_stopped(std::move(*@_rcvr_@)); + } + }; + + @[if\ (dataParts)]{.rm}@ + @[return\ op_state{std::move(dataParts->first),\ std::move(dataParts->second),\ rcvr};]{.rm}@ + @[else]{.rm}@ + @[\ \ ]{.rm}@return op_state{@[std::forward_like(data),\ ]{.add}@rcvr}; +} +``` + +[14]{.pnum} The expression in the `noexcept` clause of `@_impls-for_@::@_get-state_@` is + +`@\ \ [is_nothrow_constructible_v,\ Sndr>\ &&]{.rm}@`\ +`@\ \ [is_nothrow_move_constructible_v<_wrap-sender_>\ &&]{.rm}@`\ +`@\ \ [(is_rvalue_reference_v\ ||\ is_nothrow_constructible_v,\ Sndr>)\ &&]{.add}@`\ +`@\ \ _nothrow-callable_@` + +where _`wrap-sender`_ is the type `remove_cvref_t<@_data-type_@>::@_wrap-sender_@.` + +## `execution::spawn_future` + +To the subsection [exec.spawn.future]{.sref}, make the following changes: + +[7]{.pnum} Let _`spawn-future-state`_ be the exposition-only class template: + +``` +namespace std::execution { + template + struct @_spawn-future-state_@ // @_exposition only_@ + : @_spawn-future-state-base_@>> { + using @_sigs-t_@ = // @_exposition only_@ + completion_signatures_of_t<@_future-spawned-sender_@>; + using @_receiver-t_@ = // @_exposition only_@ + @_spawn-future-receiver_@<@_sigs-t_@>; + using @_op-t_@ = // @_exposition only_@ + connect_result_t<@_future-spawned-sender_@, @_receiver-t_@>; + + @_spawn-future-state_@(Alloc alloc, Sender&& sndr, Token token, Env env) // @_exposition only_@ + : @_alloc_@(std::move(alloc)), + @_op_@(connect( + write_env(@_stop-when_@(std::forward(sndr), @_ssource_@.get_token()), std::move(env)), + @_receiver-t_@(this))), + @[_token_(std::move(token)),]{.rm}@ + @_assoc[iated]{.rm}_@(token.try_associate()) { + if (@[associated]{.rm}[_assoc_]{.add}@) + start(@_op_@); + else + set_stopped(@_receiver-t_@(this)); + } + + void @_complete_@() noexcept override; // @_exposition only_@ + void @_consume_@(receiver auto& rcvr) noexcept; // @_exposition only_@ + void @_abandon_@() noexcept; // @_exposition only_@ + + private: + using @_alloc-t_@ = // @_exposition only_@ + typename allocator_traits::template rebind_alloc<@_spawn-future-state_@>; + @[using\ _assoc-t_\ =]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[remove_cvref_t().try_associate())>;]{.add}@ + + @_alloc-t_@ @_alloc_@; // @_exposition only_@ + @_ssource-t_@ @_ssource_@; // @_exposition only_@ + @_op-t_@ @_op_@; // @_exposition only_@ + @[Token]{.rm}[_assoc-t_]{.add}\ _[token]{.rm}[assoc]{.add}_@; // @_exposition only_@ + @[bool\ _associated_;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + + void @_destroy_@() noexcept; // @_exposition only_@ + }; +} +``` + +... + +`void @_destroy_@() noexcept;` + +[12]{.pnum} _Effects:_ Equivalent to: + +``` +@[auto\ token\ =\ std::move(this->_token_);]{.rm}@ +@[bool\ associated\ =\ this->_associated_;]{.rm}@ +@[auto\ assoc\ =\ std::move(this->_assoc_);]{.add}@ +@[{]{.rm}@ +@[\ \ ]{.rm}@auto alloc = std::move(this->@_alloc_@); + +@[\ \ ]{.rm}@allocator_traits::destroy(alloc, this); +@[\ \ ]{.rm}@allocator_traits::deallocate(alloc, this, 1); +@[}]{.rm}@ + +@[if\ (associated)]{.rm}@ +@[\ \ token.disassociate();]{.rm}@ +``` + +## `execution::spawn` + +To the subsection [exec.spawn]{.sref}, make the following changes: + +[5]{.pnum} Let spawn-state be the exposition-only class template: + +``` +namespace std::execution { + template + struct @_spawn-state_@ : @_spawn-state-base_@ { // @_exposition only_@ + using @_op-t_@ = connect_result_t; // @_exposition only_@ + + @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token); // @_exposition only_@ + void @_complete_@() noexcept override; // @_exposition only_@ + void @_run_()[\ noexcept]{.add}@; // @_exposition only_@ + + private: + using @_alloc-t_@ = // @_exposition only_@ + typename allocator_traits::template rebind_alloc<@_spawn-state_@>; + @[using\ _assoc-t_\ =]{.add}@ @[//\ _exposition\ only_]{.add}@ + @[remove_cvref_t().try_associate())>;]{.add}@ + + @_alloc-t_@ @_alloc_@; // @_exposition only_@ + @_op-t_@ @_op_@; // @_exposition only_@ + @[Token]{.rm}[_assoc-t_]{.add}\ _[token]{.rm}[assoc]{.add}_@; // @_exposition only_@ + + @[void\ _destroy_()\ noexcept;]{.rm}@ @[//\ _exposition\ only_]{.rm}@ + }; +} +``` + +`@_spawn-state_@(Alloc alloc, Sender&& sndr, Token token);` + +[6]{.pnum} _Effects:_ [Initializes _`alloc`_ with `alloc`, _`token`_ with `token`, and _`op`_ with:\ +`@\ \ @connect(std::move(sndr), @_spawn-receiver_@(this))`]{.rm} [Initializes _`alloc`_ with `std::move(alloc)`, _`op`_ +with `connect(std::move(sndr), @_spawn-receiver_@(this))`, and _`assoc`_ with `token.try_associate()`.]{.add} + +`void @_run_()[\ noexcept]{.add}@;` + +[7]{.pnum} _Effects:_ Equivalent to: + +``` + if (@_token_@.try_associate()) + start(@_op_@); + else + @_[destroy]{.rm}[complete]{.add}_@(); +``` + +`void @_complete_@() noexcept override;` + +[8]{.pnum} _Effects:_ Equivalent to: + +``` + @[auto\ token\ =\ std::move(this->_token_);]{.rm}@ + + @[_destroy_();]{.rm}@ + + @[token.disassociate();]{.rm}@ +``` + +[`void @_destroy_@() noexcept;`]{.rm} + +[[9]{.pnum} _Effects:_ Equivalent to:]{.rm} + +``` + @[auto\ assoc\ =\ std::move(this->_assoc_);]{.add}@ + auto alloc = std::move(this->@_alloc_@); + + allocator_traits<@_alloc-t_@>::destroy(alloc, this); + allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); +``` + +## Async scope concepts + +At the beginning of subsection [exec.scope.concepts]{.sref}, make the following changes + +[[1]{.pnum} The `scope_assocation` concept defines the requirements on a type `Assoc` that, when engaged, owns an +association with an async scope.]{.add} + +``` +@[namespace\ std::execution\ {]{.add}@ + @[template\ ]{.add}@ + @[concept\ scope_association\ =]{.add}@ + @[movable\ &&]{.add}@ + @[default_initializable\ &&]{.add}@ + @[requires(const\ Assoc\ assoc)\ {]{.add}@ + @[{\ static_cast(assoc)\ }\ noexcept;]{.add}@ + @[{\ assoc.try_associate()\ }\ ->\ same_as;]{.add}@ + @[};]{.add}@ +@[}]{.add}@ +``` + +[2]{.pnum} The `scope_token` concept defines the requirements on a type `Token` that can be used +to create associations between senders and an async scope. + +[3]{.pnum} Let _`test-sender`_ and _`test-env`_ be unspecified types such that +`sender_in<@_test-sender_@, @_test-env_@>` is modeled. + +``` +namespace std::execution { + template + concept scope_token = + copyable && + requires(const Token token) { + @[{\ token.try_associate()\ }\ ->\ same_as;]{.rm}@ + @[{\ token.disassociate()\ }\ noexcept\ ->\ same_as;]{.rm}@ + @[{\ token.try_associate()\ }\ ->\ scope_association;]{.add}@ + { token.wrap(declval<@_test-sender_@>()) } -> sender_in<@_test-env_@>; + }; +} +``` + +## `execution::simple_counting_scope` and `execution::counting_scope` + +To the subsection [exec.scope.simple.counting.general]{.sref}, make the following change: + +``` +namespace std::execution { + class simple_counting_scope { + public: + // [exec.simple.counting.token], token + struct token; + + @[//\ [exec.simple.counting.assoc],\ assoc]{.add}@ + @[struct\ assoc;]{.add}@ + + static constexpr size_t max_associations = @_implementation-defined_@; + + // [exec.simple.counting.ctor], constructor and destructor + simple_counting_scope() noexcept; + simple_counting_scope(simple_counting_scope&&) = delete; + ~simple_counting_scope(); + + // [exec.simple.counting.mem], members + token get_token() noexcept; + void close() noexcept; + sender auto join() noexcept; + + private: + size_t @_count_@; // exposition only + @_scope-state-type_@ @_state_@; // exposition only + + @[bool]{.rm}[assoc]{.add}@ @_try-associate_@() noexcept; // exposition only + void @_disassociate_@() noexcept; // exposition only + template + bool @_start-join-sender_@(State& state) noexcept; // exposition only + }; +} +``` + +To the subsection [exec.simple.counting.mem]{.sref}, make the following changes: + +`@[bool]{.rm}[assoc]{.add}@ @_try-associate_@() noexcept;` + +[5]{.pnum} _Effects:_ If _`count`_ is equal to `max_associations`, then no effects. Otherwise, if _`state`_ is + +- [5.1]{.pnum} _`unused`_, then increments _`count`_ and changes _`state`_ to _`open`_; +- [5.2]{.pnum} _`open`_ or _`open-and-joining`_, then increments _`count`_; +- [5.3]{.pnum} otherwise, no effects. + +[6]{.pnum} _Returns:_ [`true` if _`count`_ was incremented, `false` otherwise.]{.rm} [An object `a` of type +`simple_counting_scope::assoc` such that `a.@_scope_@` is `this` if _`count`_ was incremented, `nullptr` +otherwise.]{.add} + +To the subsection [exec.simple.counting.token]{.sref}, make the following changes: + +``` +namespace std::execution { + struct simple_counting_scope::token { + template + Sender&& wrap(Sender&& snd) const noexcept; + @[bool]{.rm}[assoc]{.add}@ try_associate() const noexcept; + + @[void\ disassociate()\ const\ noexcept;]{.rm}@ + + private: + simple_counting_scope* @_scope_@; // exposition only + }; +} +``` + +``` +template + Sender&& wrap(Sender&& snd) const noexcept; +``` + +[1]{.pnum} _Returns:_ `std::forward(snd)`. + +`@[bool]{.rm}[assoc]{.add}@ try_associate() const noexcept;` + +[2]{.pnum} _Effects:_ Equivalent to: `return @_scope_@->@_try-associate_@();` + +[`void disassociate() const noexcept;`]{.rm} + +[[3]{.pnum} _Effects:_ Equivalent to `@_scope_@->@_disassociate_@()`.]{.rm} + +Add the following new section immediately after [exec.simple.counting.token]{.sref}: + +[__Association [exec.simple.counting.assoc]__]{.add} + +``` +@[namespace\ std::execution\ {]{.add}@ + @[struct\ simple_counting_scope::assoc\ {]{.add}@ + @[explicit\ operator\ bool()\ const\ noexcept;]{.add}@ + + @[assoc\ try_associate()\ const\ noexcept;]{.add}@ + + @[private:]{.add}@ + @[using\ _handle_\ =\ //\ _exposition\ only_]{.add}@ + @[unique_ptr_disassociate_();]{.add}@ + @[})>;]{.add}@ + + @[_handle_\ _scope_;\ //\ _exposition\ only_]{.add}@ + @[};]{.add}@ +@[}]{.add}@ +``` + +[`explicit operator bool() const noexcept;`]{.add} + +[[1]{.pnum} _Returns:_ `@_scope_@ != nullptr`]{.add} + +[`assoc try_associate() const noexcept;`]{.add} + +[[2]{.pnum} _Returns:_ A default-initialized `assoc` if _`scope`_ is `nullptr`, `@_scope_@->@_try-associate_@()` +otherwise.]{.add} + +To the subsection [exec.scope.counting]{.sref}, make the following changes: + +``` +namespace std::execution { + class counting_scope { + public: + @[struct\ assoc\ {]{.add}@ + @[explicit\ operator\ bool()\ const\ noexcept;]{.add}@ + + @[assoc\ try_associate()\ const\ noexcept;]{.add}@ + + @[private:]{.add}@ + @[using\ @_handle_@\ =\ //\ @_exposition\ only_@]{.add}@ + @[unique_ptr@_disassociate_@();]{.add}@ + @[})>;]{.add}@ + + @[@_handle_@ @_scope_@;\ //\ @_exposition\ only_@]{.add}@ + @[};]{.add}@ + + struct token { + template + sender auto wrap(Sender&& snd) const noexcept(see below); + @[bool]{.rm}[assoc]{.add}@ try_associate() const noexcept; + + @[void\ disassociate()\ const\ noexcept;]{.rm}@ + + private: + counting_scope* @_scope_@; // exposition only + }; + + static constexpr size_t max_associations = implementation-defined; + + counting_scope() noexcept; + counting_scope(counting_scope&&) = delete; + ~counting_scope(); + + token get_token() noexcept; + void close() noexcept; + sender auto join() noexcept; + void request_stop() noexcept; + + private: + size_t @_count_@; // exposition only + @_scope-state-type_@ @_state_@; // exposition only + inplace_stop_source @_s_source_@; // exposition only + + @[bool]{.rm}[assoc]{.add}@ @_try-associate_@() noexcept; // exposition only + void @_disassociate_@() noexcept; // exposition only + + template + bool @_start-join-sender_@(State& state) noexcept; // exposition only + }; +} +```