Skip to content

when_all() should return just() #482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 15, 2022
Merged

when_all() should return just() #482

merged 2 commits into from
Oct 15, 2022

Conversation

bustercopley
Copy link
Contributor

@bustercopley bustercopley commented Mar 18, 2022

If I write a variadic function template that takes a pack of senders and passes them to when_all, I need to either (a) constrain it to take at least one sender, or (b) handle the case of zero senders specially.

It would be more convenient if when_all() returned the moral equivalent of just(tuple{}). [Correction: just(). A sender which, when connected to a receiver and started, calls set_value(tuple{}) on the receiver.]

Patch supplied, but I don't seriously believe it can be as easy as this.

@bustercopley bustercopley changed the title when_all() should return just(tuple{}) when_all() should return just() Mar 21, 2022
@lucteo
Copy link
Contributor

lucteo commented Mar 28, 2022

The idea doesn't sound bad to me. But, currently this is prevented by the spec (see 10.8.5.12./2). We need to change the spec first.

@bustercopley
Copy link
Contributor Author

  • Rebase on main
  • Remove the '>0' constraints and complete immediately on start if no senders (instead of an overload that returns 'just()')
  • Add tests for transfer_when_all and when_all_with_variant
  • Remove the '>0' constraint in the spec and mention the case of zero senders in the description of 'op_state' (10.8.5.12.2.1.1)

@bustercopley
Copy link
Contributor Author

Rebased (and resolved conflicts).

@bustercopley
Copy link
Contributor Author

Rebased and resolved conflicts.

I'm not sure what to do with my suggested change to the specification. Here it is, for the record:

diff --git a/std_execution.bs b/std_execution.bs
index ee8a16a..61f1507 100644
--- a/std_execution.bs
+++ b/std_execution.bs
@@ -5736,10 +5736,7 @@ template <class E>  // arguments are not associated entities ([lib.tmpl-heads
 1. `execution::when_all` is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders that only send a single set of values. `execution::when_all_with_variant`
     is used to join multiple sender chains and create a sender whose execution is dependent on all of the input senders, each of which may have one or more sets of sent values.
 
-2. The name `execution::when_all` denotes a customization point object. For some subexpressions <code>s<i><sub>i</sub></i>...</code>, let <code>S<i><sub>i</sub></i>...</code> be <code>decltype((s<i><sub>i</sub></i>))...</code>. The expression <code>execution::when_all(s<i><sub>i</sub></i>...)</code> is ill-formed if any of the following is true:
-
-    * If the number of subexpressions <code>s<i><sub>i</sub></i>...</code> is 0, or
-    * If any type <code>S<i><sub>i</sub></i></code> does not satisfy `execution::sender`.
+2. The name `execution::when_all` denotes a customization point object. For some subexpressions <code>s<i><sub>i</sub></i>...</code>, let <code>S<i><sub>i</sub></i>...</code> be <code>decltype((s<i><sub>i</sub></i>))...</code>. The expression <code>execution::when_all(s<i><sub>i</sub></i>...)</code> is ill-formed if any type <code>S<i><sub>i</sub></i></code> does not satisfy `execution::sender`.
 
     Otherwise, the expression <code>execution::when_all(s<i><sub>i</sub></i>...)</code> is expression-equivalent to:
 
@@ -5758,7 +5755,7 @@ template &lt;class E>  // arguments are not associated entities ([lib.tmpl-heads
 
         1. For each sender <code>s<i><sub>i</sub></i></code>, constructs a receiver <code>r<i><sub>i</sub></i></code> such that:
 
-            1. If <code>execution::set_value(r<i><sub>i</sub></i>, t<i><sub>i</sub></i>...)</code> is called for every <code>r<i><sub>i</sub></i></code>, `op_state`'s associated stop callback optional is reset and <code>execution::set_value(out_r, t<i><sub>0</sub></i>..., t<i><sub>1</sub></i>..., ..., t<i><sub>n-1</sub></i>...)</code> is called, where `n` the number of subexpressions in <code>s<i><sub>i</sub></i>...</code>.
+            1. If <code>execution::set_value(r<i><sub>i</sub></i>, t<i><sub>i</sub></i>...)</code> is called for every <code>r<i><sub>i</sub></i></code>, or if the number of subexpressions in <code>s<i><sub>i</sub></i>...</code> is 0, `op_state`'s associated stop callback optional is reset and <code>execution::set_value(out_r, t<i><sub>0</sub></i>..., t<i><sub>1</sub></i>..., ..., t<i><sub>n-1</sub></i>...)</code> is called, where `n` is the number of subexpressions in <code>s<i><sub>i</sub></i>...</code>.
 
             2. Otherwise, `execution::set_error` or `execution::set_stopped` was called for at least one receiver <code>r<i><sub>i</sub></i></code>. If the first such to complete did so with the call <code>execution::set_error(r<i><sub>i</sub></i>, e)</code>, `request_stop` is called on `op_state`'s associated stop source. When all child operations have completed, `op_state`'s associated stop callback optional is reset and `execution::set_error(out_r, e)` is called.
 
-- 

@@ -44,6 +44,11 @@ TEST_CASE("transfer_when_all simple example", "[adaptors][transfer_when_all]") {
auto op = ex::connect(std::move(snd1), expect_value_receiver<double>{3.1415});
ex::start(op);
}
TEST_CASE("transfer_when_all with no senders", "[adaptors][transfer_when_all]") {
auto snd = ex::transfer_when_all(inline_scheduler{});
auto op = ex::connect(std::move(snd), expect_void_receiver{});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, but I would expect when_all() to be equivalent to just(tuple{}) instead of just(). Can you comment on why you opted for the current semantics?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when_all concats the values from all the senders, without wrapping them onto tuples. So an empty list of values seems correct?

Copy link
Contributor Author

@bustercopley bustercopley Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. In libunifex, just is special-cased for 0 arguments, and ends up calling set_value(). But here in stdexec things are more regular, and just completes by calling set_value(tuple{}), which is the desired end result for when_all, allowing a generic program like the following:

#include <iostream>
#include <tuple>

#include <stdexec/execution.hpp>

auto print = [](const auto &...args) {
  (std::cout << "[" << ... << args) << "]\n";
};

auto do_test(const auto &...args) {
  auto sender = stdexec::when_all(stdexec::just(args)...);
  auto results = stdexec::this_thread::sync_wait(sender);
  std::apply(print, *results);
}

int main() {
  do_test("a", "b", "c");
  do_test("x");
  do_test();
}

This program compiles on the current branch, but if when_all added an extra layer of 'tuple', this program would need its own special-case code to work around it.

Note that my current suggestion for when_all with no senders isn't implemented in terms of just -- by removing even more special cases, everything just takes care of itself. (Sort of! I did introduce a new if constexpr test for the case of 0 senders. But that's only an optimization.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just completes by calling set_value(tuple{})

huh? When the just() sender completes, it calls set_value() (or rather set_value(rcvr) since set_value always takes a receiver).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when_all concats the values from all the senders, without wrapping them onto tuples. So an empty list of values seems correct?

Oh yeah.
/me pours himself another coffee

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh? When the just() sender completes, it calls set_value() (or rather set_value(rcvr) since set_value always takes a receiver).

Right, sorry. I'll just say that adding tuple{} here would make the case of zero senders the odd-one-out.

bustercopley and others added 2 commits October 14, 2022 10:16
'when_all()' with zero input senders completes immediately on start
@ericniebler
Copy link
Collaborator

ericniebler commented Oct 14, 2022

Thanks for your work on this @bustercopley, and apologies for the loooong wait. As you've probably noticed, we've moved the wording of P2300 into its own repo at brycelelbach/wg21_p2300_execution. The wording changes that you had proposed in an earlier draft of this PR would need to be applied there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants