From 8f600aa35fbc7aec28d62e29030e8cedd5b9bb4d Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Thu, 22 May 2025 21:11:20 -0700 Subject: [PATCH 1/8] Tony tables showing async_scope_association The result of our afternoon pairing on what `nest`'s implementation would look like if we brought back `async_scope_association`. --- asyncscope.md | 278 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 211 insertions(+), 67 deletions(-) diff --git a/asyncscope.md b/asyncscope.md index 4010d6b..1eaa363 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2261,17 +2261,11 @@ To the `` synopsis [execution.syn]{.sref}, make the following additio Add the following as a new subsection at the end of [exec.adapt]{.sref}: -::: add -__`std::execution::nest` [exec.nest]__ - -[1]{.pnum} `nest` tries to associate a sender with an async scope such that the scope can track the lifetime of any -async operations created with the sender. - -[2]{.pnum} Let _`nest-data`_ be the following exposition-only class template: +:::cmptable +### Before ```cpp namespace std::execution { -// TODO: add a paragraph that describes the class' class invariant: "engaged optional" means association is owned template struct @_nest-data_@ { // exposition only @@ -2289,11 +2283,13 @@ struct @_nest-data_@ { // exposition only noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && noexcept(other.@_token_@.try_associate())); - @_nest-data_@(@_nest-data_@&& other) noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); + @_nest-data_@(@_nest-data_@&& other) + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); ~@_nest-data_@(); - optional> release() && noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); + optional> release() && + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>); private: Token @_token_@; // exposition only @@ -2306,6 +2302,211 @@ template } ``` +### After + +```cpp +namespace std::execution { + + +template +struct @_nest-data_@ { // exposition only + using @_wrap-sender_@ = // exposition only + remove_cvref_t().wrap(declval()))>; + using @_assoc-t_@ = // exposition only + remove_cvref_t().try_associate())>; + + explicit @_nest-data_@(Token t, Sender&& s) { + ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(t.wrap(std::forward(s))); + + try { + @_assoc_@ = t.try_associate(); + } + catch (...) { + @_sndr_@.~@_wrap-sender_@(); + throw; + } + + if (!@_assoc_@) + @_sndr_@.~@_wrap-sender_@(); + } + + @_nest-data_@(const @_nest-data_@& other) + noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && + is_nothrow_copy_constructible_v<@_assoc_t_@>) + : @_assoc_@(other.@_assoc_@) { + if (@_assoc_@) + ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(other.@_sndr_@); + } + + @_nest-data_@(@_nest-data_@&& other) + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>) { + if (other.@_assoc_@) { + ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(std::move(other.@_sndr_@)); + + using std::swap; + swap(@_assoc_@, other.@_assoc_@); + + other.@_sndr_@.~@_wrap-sender_@(); + } + } + + ~@_nest-data_@() { + if (@_assoc_@) + @_sndr_@.~@_wrap-sender_@(); + } + + explicit operator bool() const noexcept { + return !!@_assoc_@; + } + + const @_wrap-sender_@& get_sender() const& noexcept { + return @_sndr_@; + } + + @_wrap-sender_@&& get_sender() && noexcept { + return std::move(@_sndr_@); + } + + const @_assoc-t_@& get_assoc() const& noexcept { + return @_assoc_@; + } + + @_assoc-t_@ get_assoc() && noexcept { + @_sndr_@.~@_wrap-sender_@(); + return std::move(@_assoc_@); + } + +private: + @_assoc-t_@ @_assoc_@; // exposition only + union { + @_wrap-sender_@ @_sndr_@; // exposition only + }; +}; + +template +@_nest-data_@(Token, Sender&&) -> @_nest-data_@; + +} +``` + +:::: + +:::cmptable +### Before +```cpp +[](Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { + auto [_, data] = std::forward(sndr); + + auto dataParts = std::move(data).release(); + + using scope_token = decltype(dataParts->first); + using wrap_sender = decltype(dataParts->second); + using op_t = decltype(connect(std::move(dataParts->second), std::move(rcvr))); + + struct op_state { + bool @_associated_@ = false; // exposition only + union { + Rcvr* @_rcvr_@; // exposition only + struct { + scope_token @_token_@; // exposition only + op_t @_op_@; // exposition only + }; + }; + + explicit op_state(Rcvr& r) noexcept + : @_rcvr_@(addressof(r)) {} + + explicit op_state(scope_token tkn, wrap_sender&& sndr, Rcvr& r) try + : @_associated_@(true), + @_token_@(std::move(tkn)), + @_op_@(connect(std::move(sndr), std::move(r))) { + } + catch (...) { + @_token_@.disassociate(); + throw; + } + + op_state(op_state&&) = delete; + + ~op_state() { + if (@_associated_@) { + @_op_@.~op_t(); + @_token_@.disassociate(); + @_token_@.~scope_token(); + } + } + + void @_start_@() noexcept { // exposition only + if (@_associated_@) + @_op_@.start(); + else + set_stopped(std::move(*@_rcvr_@)); + } + }; + + if (dataParts) + return op_state{std::move(dataParts->first), std::move(dataParts->second), rcvr}; + else + return op_state{rcvr}; +} +``` + +### After +```cpp +[](Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { + auto&& [_, data] = std::forward(sndr); + + using nest_data_t = decltype(data); + + using assoc_t = typename remove_reference_t::@_assoc_t_@; + using op_t = decltype(connect(std::forward_like(data).get_sender(), std::move(rcvr))); + + struct op_state { + union { + Rcvr* @_rcvr_@; // exposition only + op_t @_op_@; // exposition only + }; + assoc_t @_assoc_@; // exposition only + + explicit op_state(Rcvr& r) noexcept + : @_rcvr_@(addressof(r)) {} + + explicit op_state(nest_data_t&& nd, Rcvr& r) + : @_op_@(connect(std::forward_like(nd).get_sender(), std::move(r))), + @_assoc_@(std::forward_like(nd).get_assoc()) { + } + + op_state(op_state&&) = delete; + + ~op_state() { + if (@_assoc_@) + @_op_@.~op_t(); + } + + void @_start_@() noexcept { // exposition only + if (@_assoc_@) + @_op_@.start(); + else + set_stopped(std::move(*@_rcvr_@)); + } + }; + + if (assoc) + return op_state{std::forward_like(data), rcvr}; + else + return op_state{rcvr}; +} +``` +:::: + +::: add +__`std::execution::nest` [exec.nest]__ + +[1]{.pnum} `nest` tries to associate a sender with an async scope such that the scope can track the lifetime of any +async operations created with the sender. + +[2]{.pnum} Let _`nest-data`_ be the following exposition-only class template: + ```c++ @_nest-data_@(const @_nest-data_@& other) noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && @@ -2387,63 +2588,6 @@ struct @_impls-for_@ : @_default-impls_@ { [13]{.pnum} The member `@_impls-for_@::@_get-state_@` is initialized with a callable object equivalent to the following lambda: -```cpp -[](Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { - auto [_, data] = std::forward(sndr); - - auto dataParts = std::move(data).release(); - - using scope_token = decltype(dataParts->first); - using wrap_sender = decltype(dataParts->second); - using op_t = decltype(connect(std::move(dataParts->second), std::move(rcvr))); - - struct op_state { - bool @_associated_@ = false; // exposition only - union { - Rcvr* @_rcvr_@; // exposition only - struct { - scope_token @_token_@; // exposition only - op_t @_op_@; // exposition only - }; - }; - - explicit op_state(Rcvr& r) noexcept - : @_rcvr_@(addressof(r)) {} - - explicit op_state(scope_token tkn, wrap_sender&& sndr, Rcvr& r) try - : @_associated_@(true), - @_token_@(std::move(tkn)), - @_op_@(connect(std::move(sndr), std::move(r))) { - } - catch (...) { - @_token_@.disassociate(); - throw; - } - - op_state(op_state&&) = delete; - - ~op_state() { - if (@_associated_@) { - @_op_@.~op_t(); - @_token_@.disassociate(); - @_token_@.~scope_token(); - } - } - - void @_start_@() noexcept { // exposition only - if (@_associated_@) - @_op_@.start(); - else - set_stopped(std::move(*@_rcvr_@)); - } - }; - - if (dataParts) - return op_state{std::move(dataParts->first), std::move(dataParts->second), rcvr}; - else - return op_state{rcvr}; -} -``` [14]{.pnum} The expression in the `noexcept` clause of `@_impls-for_@::@_get-state_@` is From 2541bb5c4923eeead6f2d5e883fbb2ae1219f9d7 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 25 May 2025 15:17:09 -0700 Subject: [PATCH 2/8] More Tony tables --- asyncscope.md | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/asyncscope.md b/asyncscope.md index 1eaa363..05034a3 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2499,6 +2499,103 @@ template ``` :::: +:::cmptable +### Before +```cpp +namespace std::execution { + +template +concept async_scope_token = + copyable && + requires(Token token) { + { token.try_associate() } -> same_as; + { token.disassociate() } noexcept -> same_as; + { token.wrap(declval<@_test-sender_@>()) } -> sender_in<@_test-env_@>; + }; + +struct simple_counting_scope::token { + template + Sender&& wrap(Sender&& snd) const noexcept; + + bool try_associate() const noexcept; + + void disassociate() const noexcept; + +private: + simple_counting_scope* @_scope_@; // exposition only +}; + +struct counting_scope::token { + template + sender auto wrap(Sender&& snd) const noexcept; + bool try_associate() const noexcept; + void disassociate() const noexcept; + +private: + counting_scope* @_scope_@; // @_exposition only_@ +}; + +} +``` +### After +```cpp +namespace std::execution { + +template +concept async_scope_assocation = + copyable && + default_initializable && + requires(Assoc assoc) { + { static_cast(assoc) } noexcept; + }; + +template +concept async_scope_token = + copyable && + requires(Token token) { + { token.try_associate() } -> async_scope_association; + { token.wrap(declval<@_test-sender_@>()) } -> sender_in<@_test-env_@>; + }; + +struct simple_counting_scope::assoc { + explicit operator bool() const noexcept; + +private: + simple_counting_scope* @_scope_@; // exposition only +}; + +struct simple_counting_scope::token { + template + Sender&& wrap(Sender&& snd) const noexcept; + + assoc try_associate() const noexcept; + +private: + simple_counting_scope* @_scope_@; // exposition only +}; + + +struct counting_scope::assoc { + explicit operator bool() const noexcept; + +private: + counting_scope* @_scope_@; // @_exposition only_@ +}; + +struct counting_scope::token { + template + sender auto wrap(Sender&& snd) const noexcept; + + assoc try_associate() const noexcept; + +private: + counting_scope* @_scope_@; // @_exposition only_@ +}; + +} +``` +:::: + ::: add __`std::execution::nest` [exec.nest]__ From 7231df304a9fca38b2b2a06169f2f9aceffe59f9 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Sun, 25 May 2025 16:09:17 -0700 Subject: [PATCH 3/8] A potential correction to nest I *think* this makes _`nest-data`_ and its opstate for both rvalue and lvalue connections with and without an association. Things to consider: - what if `connect` throws? - what if the _`wrap-sender`_'s move-constructor throws? - what if the _`wrap-sender`_'s copy-constructor throws? - what if the association's copy-constructor throws? - what if the association's copy-constructor fails? Icky things: - you *must* call `nest-data::get_assoc` before `nest-data::get_sender` - I'm not entirely sure the rvalue case is safe if the sender's move-constructor throws Some of the problems would be solved if _`nest-data`_ stored an optional instead of a possibly-uninitialized wrap-sender. --- asyncscope.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/asyncscope.md b/asyncscope.md index 05034a3..98ba198 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2363,16 +2363,17 @@ struct @_nest-data_@ { // exposition only return @_sndr_@; } - @_wrap-sender_@&& get_sender() && noexcept { - return std::move(@_sndr_@); + @_wrap-sender_@ get_sender() && noexcept { + @_wrap-sender_@ ret(std::move(@_sndr_@); + @_sndr_@.~@_wrap-sender_@(); + return ret; } const @_assoc-t_@& get_assoc() const& noexcept { return @_assoc_@; } - @_assoc-t_@ get_assoc() && noexcept { - @_sndr_@.~@_wrap-sender_@(); + @_assoc-t_@&& get_assoc() && noexcept { return std::move(@_assoc_@); } @@ -2456,24 +2457,32 @@ template [](Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { auto&& [_, data] = std::forward(sndr); - using nest_data_t = decltype(data); + using nest_data_t = remove_cvref_t; - using assoc_t = typename remove_reference_t::@_assoc_t_@; + using assoc_t = typename nest_data_t::@_assoc-t_@; using op_t = decltype(connect(std::forward_like(data).get_sender(), std::move(rcvr))); struct op_state { + assoc_t @_assoc_@; // exposition only union { Rcvr* @_rcvr_@; // exposition only op_t @_op_@; // exposition only }; - assoc_t @_assoc_@; // exposition only - - explicit op_state(Rcvr& r) noexcept - : @_rcvr_@(addressof(r)) {} explicit op_state(nest_data_t&& nd, Rcvr& r) - : @_op_@(connect(std::forward_like(nd).get_sender(), std::move(r))), - @_assoc_@(std::forward_like(nd).get_assoc()) { + : @_assoc_@(std::move(nd).get_assoc()) { + if (@_assoc_@) + ::new (@_voidify_@(@_op_@)) op_t{connect(std::move(nd).get_sender(), std::move(r))}; + else + @_rcvr_ = &r; + } + + explicit op_state(const nest_data_t& nd, Rcvr& r) + : @_assoc_@(nd.get_assoc()) { + if (@_assoc_@) + ::new (@_voidify_@(@_op_@)) op_t{connect(nd.get_sender(), std::move(r))}; + else + @_rcvr_ = &r; } op_state(op_state&&) = delete; @@ -2491,10 +2500,7 @@ template } }; - if (assoc) - return op_state{std::forward_like(data), rcvr}; - else - return op_state{rcvr}; + return op_state{std::forward_like(data), rcvr}; } ``` :::: From 3e24a89b2fdee26fcec2ff0b1f5fa52e61999113 Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 26 May 2025 12:03:42 -0700 Subject: [PATCH 4/8] A better nest-data implementation This diff introduces what I think is a good interface for the association-enhanced _`nest-data`_. The central change is that _`nest-data`_ now has this method: ```cpp using scope_ref = unique_ptr; pair release() && noexcept; ``` where _`deleter`_ is a deleter that invokes the destructor on the to-be-deleted pointer when it's not-null. This gives us a way to move the association out of the source _`nest-data`_ and a reference to its sender that we can also move from, with an attached guarantee that the source sender will have its destructor invoked in all cases. The one weird thing about this interface is that the returned `pair` is only valid so long as the source _`nest-data`_ instance still exists since the `scope_ref`'s destructor will reach into the source instance to destroy the sender therein, which is clearly not allowed if the source instance has already been destroyed. --- asyncscope.md | 112 +++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 61 deletions(-) diff --git a/asyncscope.md b/asyncscope.md index 98ba198..50e67f0 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2266,7 +2266,6 @@ Add the following as a new subsection at the end of [exec.adapt]{.sref}: ```cpp namespace std::execution { - template struct @_nest-data_@ { // exposition only using @_wrap-sender_@ = // exposition only @@ -2307,74 +2306,56 @@ template ```cpp namespace std::execution { - template struct @_nest-data_@ { // exposition only - using @_wrap-sender_@ = // exposition only - remove_cvref_t().wrap(declval()))>; using @_assoc-t_@ = // exposition only remove_cvref_t().try_associate())>; + using @_wrap-sender_@ = // exposition only + remove_cvref_t().wrap(declval()))>; - explicit @_nest-data_@(Token t, Sender&& s) { - ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(t.wrap(std::forward(s))); - - try { - @_assoc_@ = t.try_associate(); - } - catch (...) { - @_sndr_@.~@_wrap-sender_@(); - throw; + struct @_deleter_@ { // exposition only + void operator()(@_wrap-sender_@* p) const noexcept { + if (p) + destroy_at(p); } + }; + + using @_sender-ref_@ = // exposition only + unique_ptr<@_wrap-sender_@, @_deleter_@>; + + explicit @_nest-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_@) - @_sndr_@.~@_wrap-sender_@(); + if (@_assoc_@) + (void)guard.release(); } @_nest-data_@(const @_nest-data_@& other) noexcept(is_nothrow_copy_constructible_v<@_wrap-sender_@> && is_nothrow_copy_constructible_v<@_assoc_t_@>) - : @_assoc_@(other.@_assoc_@) { - if (@_assoc_@) - ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(other.@_sndr_@); + requires copy_constructible<@_wrap-sender_@> + : @_assoc_@(other.@_assoc_@) { + if (@_assoc_@) + construct_at(addressof(@_sndr_@), other.@_sndr_@); } @_nest-data_@(@_nest-data_@&& other) - noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>) { - if (other.@_assoc_@) { - ::new (@_voidify_@(@_sndr_@)) @_wrap-sender_@(std::move(other.@_sndr_@)); - - using std::swap; - swap(@_assoc_@, other.@_assoc_@); - - other.@_sndr_@.~@_wrap-sender_@(); - } - } + noexcept(is_nothrow_move_constructible_v<@_wrap-sender_@>) + : @_nest-data_@(std::move(other).release()) {} ~@_nest-data_@() { if (@_assoc_@) - @_sndr_@.~@_wrap-sender_@(); - } - - explicit operator bool() const noexcept { - return !!@_assoc_@; + destroy_at(addressof(@_sndr_@)); } - const @_wrap-sender_@& get_sender() const& noexcept { - return @_sndr_@; - } - - @_wrap-sender_@ get_sender() && noexcept { - @_wrap-sender_@ ret(std::move(@_sndr_@); - @_sndr_@.~@_wrap-sender_@(); - return ret; - } - - const @_assoc-t_@& get_assoc() const& noexcept { - return @_assoc_@; - } - - @_assoc-t_@&& get_assoc() && noexcept { - return std::move(@_assoc_@); + pair<@_assoc-t_@, @_sender-ref_@> release() && noexcept { + @_wrap-sender_@* p = @_assoc_@ ? addressof(@_sndr_@) : nullptr; + return pair{std::move(@_assoc_@), @_sender-ref_@{p}}; } private: @@ -2382,6 +2363,12 @@ private: union { @_wrap-sender_@ @_sndr_@; // exposition only }; + + @_nest-data_@(pair<@_assoc-t_@, @_sender-ref_@> parts) + : @_assoc_@(std::move(parts.first)) { + if (@_assoc_@) + construct_at(addressof(@_sndr_@), std::move(other.@_sndr_@)); + } }; template @@ -2454,36 +2441,39 @@ template ### After ```cpp -[](Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { +[] + requires constructible_from, Sndr> +(Sndr&& sndr, Rcvr& rcvr) noexcept(/* @_see below_@ */) { auto&& [_, data] = std::forward(sndr); using nest_data_t = remove_cvref_t; using assoc_t = typename nest_data_t::@_assoc-t_@; - using op_t = decltype(connect(std::forward_like(data).get_sender(), std::move(rcvr))); + using sender_ref_t = typename nest_data_t::@_sender-ref_@; + using op_t = connect_result_t; - struct op_state { + class op_state { assoc_t @_assoc_@; // exposition only union { Rcvr* @_rcvr_@; // exposition only op_t @_op_@; // exposition only }; - explicit op_state(nest_data_t&& nd, Rcvr& r) - : @_assoc_@(std::move(nd).get_assoc()) { + explicit op_state(pair parts, Rcvr& r) + : @_assoc_@(std::move(parts.first)) { if (@_assoc_@) - ::new (@_voidify_@(@_op_@)) op_t{connect(std::move(nd).get_sender(), std::move(r))}; + ::new (@_voidify_@(@_op_@)) op_t{connect(std::move(*parts.second), std::move(r))}; else @_rcvr_ = &r; } + public: + explicit op_state(nest_data_t&& nd, Rcvr& r) + : op_state(std::move(nd).release(), r) {} + explicit op_state(const nest_data_t& nd, Rcvr& r) - : @_assoc_@(nd.get_assoc()) { - if (@_assoc_@) - ::new (@_voidify_@(@_op_@)) op_t{connect(nd.get_sender(), std::move(r))}; - else - @_rcvr_ = &r; - } + requires copy_constructible + : op_state(nest_data_t(nd).release(), r) {} op_state(op_state&&) = delete; From 508ad6d71b875366cf18fff1bbf547a2998c0e9c Mon Sep 17 00:00:00 2001 From: Ian Petersen Date: Mon, 26 May 2025 13:47:57 -0700 Subject: [PATCH 5/8] Fix typo in from-parts nest-data constructor --- asyncscope.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asyncscope.md b/asyncscope.md index 50e67f0..a72b6aa 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2367,7 +2367,7 @@ private: @_nest-data_@(pair<@_assoc-t_@, @_sender-ref_@> parts) : @_assoc_@(std::move(parts.first)) { if (@_assoc_@) - construct_at(addressof(@_sndr_@), std::move(other.@_sndr_@)); + construct_at(addressof(@_sndr_@), std::move(parts.second)); } }; From 88f22b22a8473df61da3788bf3a4dc9c4afb6995 Mon Sep 17 00:00:00 2001 From: "Jessica Wong (Seattle)" Date: Mon, 26 May 2025 15:18:30 -0700 Subject: [PATCH 6/8] Add before/after for spawn --- asyncscope.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/asyncscope.md b/asyncscope.md index a72b6aa..3dfb64c 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -3063,6 +3063,97 @@ private: } ``` +:::cmptable +### Before +```cpp +namespace std::execution { + +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); + + @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(...) + : alloc(alloc), + op(connect(std::move(sndr), @_spawn-receiver_@{this})), + token(std::move(token)) {} + void @_run_@() { + if (@_token_@.try_associate()) + op.start(); + else + @_destroy_@(); + } + void @_complete_@() override { + auto token = std::move(this->@_token_@); + + @_destroy_@(); + + token.disassociate(); + } + +private: + using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; + + @_alloc-t_@ alloc; + @_op-t_@ op; + Token @_token_@; + + void @_destroy_@() noexcept { + auto alloc = std::move(this->alloc); + + allocator_traits<@_alloc-t_@>::destroy(alloc, this); + allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + } + +}; + +} +``` + +### After +```cpp +namespace std::execution { + +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); + using @_assoc-t_@ = remove_cvref_t().try_associate())>; + + @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(...) + : alloc(alloc), + op(connect(std::move(sndr), @_spawn-receiver_@{this})), + assoc(token.try_associate()) {} + void @_run_@() { + if (assoc) + op.start(); + else + @_destroy_@(); + } + void @_complete_@() override { + auto assoc = std::move(this->assoc); + @_destroy_@(); + } + +private: + using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; + + @_alloc-t_@ alloc; + @_op-t_@ op; + @_assoc-t_@ assoc; + + void @_destroy_@() noexcept { + auto alloc = std::move(this->alloc); + + allocator_traits<@_alloc-t_@>::destroy(alloc, this); + allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + } + +}; + +} +``` + +:::: + `@_spawn-state_@(Alloc alloc, Sender&& sndr, Token token);` [7]{.pnum} _Effects_: Equivalent to: From 2fc55b89318c559208f04671465d46aae42c073c Mon Sep 17 00:00:00 2001 From: "Jessica Wong (Seattle)" Date: Mon, 26 May 2025 17:36:24 -0700 Subject: [PATCH 7/8] Add noexcept clause and exposition only comments to spawn --- asyncscope.md | 194 +++++++++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 91 deletions(-) diff --git a/asyncscope.md b/asyncscope.md index 3dfb64c..fbe2c50 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2592,6 +2592,109 @@ private: ``` :::: +:::cmptable +### Before +```cpp +namespace std::execution { + +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); // @_exposition only_@ + + @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(is_nothrow_copy_constructible_v && + is_nothrow_constructible_v>, Sender> && + is_nothrow_move_constructible_v && + is_nothrow_constructible_v<@_spawn_receiver_@, @_spawn-state_@> && + @_nothrow_callable_@ && + is_nothrow_move_constructible_v) + : alloc(alloc), + op(connect(std::move(sndr), @_spawn-receiver_@{this})), + token(std::move(token)) {} + + void @_run_@() { + if (@_token_@.try_associate()) + op.start(); + else + @_destroy_@(); + } + void @_complete_@() noexcept(is_nothrow_move_constructible_v) override { + auto token = std::move(this->@_token_@); + + @_destroy_@(); + + token.disassociate(); + } + +private: + using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; // @_exposition only_@ + + @_alloc-t_@ alloc; // @_exposition only_@ + @_op-t_@ op; // @_exposition only_@ + Token @_token_@; // @_exposition only_@ + + void @_destroy_@() noexcept { + auto alloc = std::move(this->alloc); + + allocator_traits<@_alloc-t_@>::destroy(alloc, this); + allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + } + +}; + +} +``` + +### After +```cpp +namespace std::execution { + +template +struct @_spawn-state_@ : @_spawn-state-base_@ { + using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); // @_exposition only_@ + using @_assoc-t_@ = remove_cvref_t().try_associate())>; // @_exposition only_@ + + @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(is_nothrow_copy_constructible_v && + is_nothrow_constructible_v>, Sender> && + is_nothrow_move_constructible_v && + is_nothrow_constructible_v<@_spawn_receiver_@, @_spawn-state_@> && + @_nothrow_callable_@ && + noexcept(token.try_associate())) + : alloc(alloc), + op(connect(std::move(sndr), @_spawn-receiver_@{this})), + assoc(token.try_associate()) {} + + void @_run_@() { + if (assoc) + op.start(); + else + @_destroy_@(); + } + void @_complete_@() noexcept(is_nothrow_move_constructible_v<@_assoc_t_@>) override { + auto assoc = std::move(this->assoc); + @_destroy_@(); + } + +private: + using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; // @_exposition only_@ + + @_alloc-t_@ alloc; // @_exposition only_@ + @_op-t_@ op; // @_exposition only_@ + @_assoc-t_@ assoc; // @_exposition only_@ + + void @_destroy_@() noexcept { + auto alloc = std::move(this->alloc); + + allocator_traits<@_alloc-t_@>::destroy(alloc, this); + allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + } + +}; + +} +``` + +:::: + ::: add __`std::execution::nest` [exec.nest]__ @@ -3063,97 +3166,6 @@ private: } ``` -:::cmptable -### Before -```cpp -namespace std::execution { - -template -struct @_spawn-state_@ : @_spawn-state-base_@ { - using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); - - @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(...) - : alloc(alloc), - op(connect(std::move(sndr), @_spawn-receiver_@{this})), - token(std::move(token)) {} - void @_run_@() { - if (@_token_@.try_associate()) - op.start(); - else - @_destroy_@(); - } - void @_complete_@() override { - auto token = std::move(this->@_token_@); - - @_destroy_@(); - - token.disassociate(); - } - -private: - using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; - - @_alloc-t_@ alloc; - @_op-t_@ op; - Token @_token_@; - - void @_destroy_@() noexcept { - auto alloc = std::move(this->alloc); - - allocator_traits<@_alloc-t_@>::destroy(alloc, this); - allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); - } - -}; - -} -``` - -### After -```cpp -namespace std::execution { - -template -struct @_spawn-state_@ : @_spawn-state-base_@ { - using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); - using @_assoc-t_@ = remove_cvref_t().try_associate())>; - - @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(...) - : alloc(alloc), - op(connect(std::move(sndr), @_spawn-receiver_@{this})), - assoc(token.try_associate()) {} - void @_run_@() { - if (assoc) - op.start(); - else - @_destroy_@(); - } - void @_complete_@() override { - auto assoc = std::move(this->assoc); - @_destroy_@(); - } - -private: - using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; - - @_alloc-t_@ alloc; - @_op-t_@ op; - @_assoc-t_@ assoc; - - void @_destroy_@() noexcept { - auto alloc = std::move(this->alloc); - - allocator_traits<@_alloc-t_@>::destroy(alloc, this); - allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); - } - -}; - -} -``` - -:::: - `@_spawn-state_@(Alloc alloc, Sender&& sndr, Token token);` [7]{.pnum} _Effects_: Equivalent to: From 909125476824e4e1eb3d046b779f6a13892c7938 Mon Sep 17 00:00:00 2001 From: "Jessica Wong (Seattle)" Date: Mon, 26 May 2025 18:03:30 -0700 Subject: [PATCH 8/8] Remove noexcept on spawn-state and simplify code --- asyncscope.md | 77 +++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/asyncscope.md b/asyncscope.md index fbe2c50..c84d2b6 100644 --- a/asyncscope.md +++ b/asyncscope.md @@ -2599,44 +2599,40 @@ namespace std::execution { template struct @_spawn-state_@ : @_spawn-state-base_@ { - using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); // @_exposition only_@ - - @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(is_nothrow_copy_constructible_v && - is_nothrow_constructible_v>, Sender> && - is_nothrow_move_constructible_v && - is_nothrow_constructible_v<@_spawn_receiver_@, @_spawn-state_@> && - @_nothrow_callable_@ && - is_nothrow_move_constructible_v) - : alloc(alloc), - op(connect(std::move(sndr), @_spawn-receiver_@{this})), - token(std::move(token)) {} + using @_op-t_@ = connect_result_t; // @_exposition only_@ + + @_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_@() { if (@_token_@.try_associate()) - op.start(); + @_op_@.start(); else @_destroy_@(); } void @_complete_@() noexcept(is_nothrow_move_constructible_v) override { - auto token = std::move(this->@_token_@); + auto t = std::move(@_token_@); @_destroy_@(); - token.disassociate(); + t.disassociate(); } private: - using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; // @_exposition only_@ + using @_alloc-t_@ = // @_exposition only_@ + typename allocator_traits::template rebind_alloc<@_spawn-state_@>; - @_alloc-t_@ alloc; // @_exposition only_@ - @_op-t_@ op; // @_exposition only_@ + @_alloc-t_@ @_alloc_@; // @_exposition only_@ + @_op-t_@ @_op_@; // @_exposition only_@ Token @_token_@; // @_exposition only_@ - void @_destroy_@() noexcept { - auto alloc = std::move(this->alloc); + void @_destroy_@() noexcept { // @_exposition only_@ + auto a = std::move(@_alloc_@); - allocator_traits<@_alloc-t_@>::destroy(alloc, this); - allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + allocator_traits<@_alloc-t_@>::destroy(a, this); + allocator_traits<@_alloc-t_@>::deallocate(a, this, 1); } }; @@ -2650,42 +2646,39 @@ namespace std::execution { template struct @_spawn-state_@ : @_spawn-state-base_@ { - using @_op-t_@ = decltype(connect(declval(), @_spawn-receiver_@{nullptr})); // @_exposition only_@ + using @_op-t_@ = connect_result_t; // @_exposition only_@ using @_assoc-t_@ = remove_cvref_t().try_associate())>; // @_exposition only_@ - @_spawn-state_@(Alloc alloc, Sender&& sndr, Token token) noexcept(is_nothrow_copy_constructible_v && - is_nothrow_constructible_v>, Sender> && - is_nothrow_move_constructible_v && - is_nothrow_constructible_v<@_spawn_receiver_@, @_spawn-state_@> && - @_nothrow_callable_@ && - noexcept(token.try_associate())) - : alloc(alloc), - op(connect(std::move(sndr), @_spawn-receiver_@{this})), - assoc(token.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_@() { - if (assoc) - op.start(); + if (@_assoc_@) + @_op_@.start(); else @_destroy_@(); } void @_complete_@() noexcept(is_nothrow_move_constructible_v<@_assoc_t_@>) override { - auto assoc = std::move(this->assoc); + auto a = std::move(@_assoc_@); @_destroy_@(); } private: - using @_alloc-t_@ = typename allocator_traits::template rebind_alloc<@_spawn-state_@>; // @_exposition only_@ + using @_alloc-t_@ = // @_exposition only_@ + typename allocator_traits::template rebind_alloc<@_spawn-state_@>; - @_alloc-t_@ alloc; // @_exposition only_@ - @_op-t_@ op; // @_exposition only_@ - @_assoc-t_@ assoc; // @_exposition only_@ + @_assoc-t_@ @_assoc_@; // @_exposition only_@ + @_alloc-t_@ @_alloc_@; // @_exposition only_@ + @_op-t_@ @_op_@; // @_exposition only_@ - void @_destroy_@() noexcept { - auto alloc = std::move(this->alloc); + void @_destroy_@() noexcept { // @_exposition only_@ + auto a = std::move(@_alloc_@); - allocator_traits<@_alloc-t_@>::destroy(alloc, this); - allocator_traits<@_alloc-t_@>::deallocate(alloc, this, 1); + allocator_traits<@_alloc-t_@>::destroy(a, this); + allocator_traits<@_alloc-t_@>::deallocate(a, this, 1); } };