Tony tables showing async_scope_association#49
Draft
ispeters wants to merge 8 commits into
Draft
Conversation
The result of our afternoon pairing on what `nest`'s implementation would look like if we brought back `async_scope_association`.
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<wrap-sender> instead of a possibly-uninitialized wrap-sender.
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<wrap_sender, deleter>; pair<assoc_t, scope_ref> 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.
jesswong
reviewed
May 26, 2025
ispeters
commented
May 26, 2025
Comment on lines
+3072
to
+3106
| struct @_spawn-state_@ : @_spawn-state-base_@ { | ||
| using @_op-t_@ = decltype(connect(declval<Sender>(), @_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<Alloc>::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); | ||
| } | ||
|
|
Owner
Author
There was a problem hiding this comment.
To match below, there are a few things that need cleaning up:
- more
noexcept - more expo-only.
Suggested change
| struct @_spawn-state_@ : @_spawn-state-base_@ { | |
| using @_op-t_@ = decltype(connect(declval<Sender>(), @_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<Alloc>::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); | |
| } | |
| struct @_spawn-state_@ : @_spawn-state-base_@ { | |
| using @_op-t_@ = connect_result_t<Sender, @_spawn-receiver_@>; | |
| @_spawn-state_@(Alloc a, Sender&& sndr, Token t) noexcept(...) | |
| : @_alloc_@(std::move(a)), | |
| @_op_@(connect(std::move(sndr), @_spawn-receiver_@{this})), | |
| @_token_@(std::move(t)) {} | |
| void @_run_@() { | |
| if (@_token_@.try_associate()) | |
| op.start(); | |
| else | |
| @_destroy_@(); | |
| } | |
| void @_complete_@() noexcept override { | |
| auto t = std::move(@_token_@); | |
| @_destroy_@(); | |
| t.disassociate(); | |
| } | |
| private: | |
| using @_alloc-t_@ = // exposition only | |
| typename allocator_traits<Alloc>::template rebind_alloc<@_spawn-state_@>; | |
| @_alloc-t_@ @_alloc_@; // exposition only | |
| @_op-t_@ @_op_@; // exposition only | |
| Token @_token_@; // exposition only | |
| void @_destroy_@() noexcept { | |
| auto a = std::move(@_alloc_@); | |
| allocator_traits<@_alloc-t_@>::destroy(a, this); | |
| allocator_traits<@_alloc-t_@>::deallocate(a, this, 1); | |
| } |
ispeters
commented
May 27, 2025
Comment on lines
+3116
to
+3150
| template <class Alloc, async_scope_token Token, sender Sender> | ||
| struct @_spawn-state_@ : @_spawn-state-base_@ { | ||
| using @_op-t_@ = decltype(connect(declval<Sender>(), @_spawn-receiver_@{nullptr})); | ||
| using @_assoc-t_@ = remove_cvref_t<decltype(declval<Token&>().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<Alloc>::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); | ||
| } | ||
|
|
||
| }; |
Owner
Author
There was a problem hiding this comment.
I few thoughts:
- I think we can simplify even a little more,
- we should add some
noexcepts to both sides of the comparison, and - the members should be in italics and expo-only.
Suggested change
| template <class Alloc, async_scope_token Token, sender Sender> | |
| struct @_spawn-state_@ : @_spawn-state-base_@ { | |
| using @_op-t_@ = decltype(connect(declval<Sender>(), @_spawn-receiver_@{nullptr})); | |
| using @_assoc-t_@ = remove_cvref_t<decltype(declval<Token&>().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<Alloc>::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); | |
| } | |
| }; | |
| template <class Alloc, async_scope_token Token, sender Sender> | |
| struct @_spawn-state_@ : @_spawn-state-base_@ { | |
| using @_op-t_@ = connect_result_t<Sender, @_spawn-receiver_@>; | |
| using @_assoc-t_@ = remove_cvref_t<decltype(declval<Token&>().try_associate())>; | |
| @_spawn-state_@(Alloc a, Sender&& sndr, Token t) noexcept(...) | |
| : @_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_@(); | |
| } | |
| private: | |
| using @_alloc-t_@ = // exposition only | |
| typename allocator_traits<Alloc>::template rebind_alloc<@_spawn-state_@>; | |
| @_assoc-t_@ @_assoc_@; // exposition only | |
| @_alloc-t_@ @_alloc_@; // exposition only | |
| @_op-t_@ @_op_@; // exposition only | |
| void @_destroy_@() noexcept { // exposition only | |
| auto a = std::move(@_alloc_@); | |
| allocator_traits<@_alloc-t_@>::destroy(a, this); | |
| allocator_traits<@_alloc-t_@>::deallocate(a, this, 1); | |
| } | |
| }; |
ispeters
commented
May 27, 2025
| else | ||
| @_destroy_@(); | ||
| } | ||
| void @_complete_@() noexcept(is_nothrow_move_constructible_v<Token>) override { |
Owner
Author
There was a problem hiding this comment.
The token concept requires that tokens be no-throw move-constructible so we can leave this off.
Suggested change
| void @_complete_@() noexcept(is_nothrow_move_constructible_v<Token>) override { | |
| void @_complete_@() noexcept override { |
| @_assoc_@ = t.try_associate(); | ||
| } | ||
|
|
||
| void @_run_@() { |
Owner
Author
There was a problem hiding this comment.
This can be noexcept because all the constituent ops are. Also, it's expo-only.
Suggested change
| void @_run_@() { | |
| void @_run_@() noexcept { // exposition only |
| else | ||
| @_destroy_@(); | ||
| } | ||
| void @_complete_@() noexcept(is_nothrow_move_constructible_v<@_assoc_t_@>) override { |
Owner
Author
There was a problem hiding this comment.
Same as above, the association has to be no-throw move-constructible, and this is expo-only.
Suggested change
| void @_complete_@() noexcept(is_nothrow_move_constructible_v<@_assoc_t_@>) override { | |
| void @_complete_@() noexcept override { // exposition only |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The result of our afternoon pairing on what
nest's implementation would look like if we brought backasync_scope_association.