(Self-answering for posterity.)
There's a major flaw in my above implementation of when_all
: It calls .get()
! This can cause the thread to block indefinitely. We need to eliminate that .get
somehow, or at least make sure that we only ever call .get
on a future that has just been passed in as the argument of a .then
callback.
The reason I was calling .get
was to work around a type incompatibility: the type of
auto SUBEXPRESSION =
std::apply(w, rest).then([f = std::move(f)](auto future_of_tuple_of_rest) {
return std::tuple_cat(std::make_tuple(std::move(f)), future_of_tuple_of_rest.get());
})
is std::future<std::tuple<F, Rest...>>
, which means that the type of
auto EXPRESSION =
f.then([...](auto f) {
return SUBEXPRESSION;
});
});
is std::future<decltype(SUBEXPRESSION)>
, which is to say std::future<std::future<std::tuple<F, Rest...>>>
. That's too many futures! So I was trying to work around this by calling .get
right before returning the "inner" future.
Turns out that boost::future
and std::experimental::future
(in the Concurrency TS) already have a solution for this problem. It's called .unwrap
, and the idea is that if decltype(f)
is future<future<T>>
, then decltype(f.unwrap())
is future<T>
.
In Boost's case, this is currently provided as a constructor explicit future(future<future>&&)
instead of as an .unwrap
member function, but there's a bug open to provide .unwrap
directly.
In the Concurrency TS, .then
also implicitly calls .unwrap
on the callback's return value if that return value is seen to be of type future<future<T>>
.
So my solution was simply to add a call to .unwrap
(well, the Boost explicit constructor), and then everything was happy.
There's another showstopping problem with the above code: it doesn't compile for more than one argument, because I'm missing the mutable
keyword on both of the lambdas involved. You can't "move out of" the captured f
unless f
is non-const, which means you need mutable
on the inner lambda; and at least GCC is unhappy with std::apply(w, rest)
unless rest
is non-const, which means you need mutable
on the outer lambda too.
Finally, I decided to stop trying to capture references to things, and just replaced everything with capture-by-value. This simplified the code to the point where I didn't see any reason to keep move_as_tuple
as its own function at all.
My final code:
auto when_all() {
promise<tuple<>> p;
p.set_value({});
return p.get_future();
}
template<class F, class... Rest>
auto when_all(F f, Rest... rest) {
return future<tuple<F, Rest...>>(
f.then([rest = make_tuple(std::move(rest)...)](auto f) mutable {
auto w = [](auto... as) { return when_all(std::move(as)...); };
return apply(w, std::move(rest)).then(
[f = std::move(f)](auto future_of_tuple_of_rest) mutable {
return tuple_cat(make_tuple(std::move(f)), future_of_tuple_of_rest.get());
}
);
})
);
}