patterncppMinor
`std::move_as_tuple` and `std::when_all`
Viewed 0 times
stdwhen_allandmove_as_tuple
Problem
Working with futures and
However, we can capture a whole parameter pack "by forward", using
How does this code look? And is there a simpler way to accomplish my goal?
Sample usage (which is also fair game for critique):
.then, I keep running into the problem of needing to "capture by move" in a lambda. In fact, not only that, but when implementing e.g. when_all, I find myself needing to capture a whole parameter-pack. Neither of these things are supported out of the box in C++17.However, we can capture a whole parameter pack "by forward", using
std::forward_as_tuple and std::apply. So I decided to write a std::move_as_tuple that would do the same thing as forward_as_tuple but for moving instead of forwarding.How does this code look? And is there a simpler way to accomplish my goal?
template
constexpr auto move_as_tuple(Types&&... args)
{
return std::forward_as_tuple(std::move(args)...);
}Sample usage (which is also fair game for critique):
future> when_all()
{
promise> p;
p.set_value({});
return p.get_future();
}
template
future> when_all(F f, Rest... rest)
{
return f.then([rest = move_as_tuple(rest...)](auto f) {
auto w = [](auto&&... as) { return when_all(std::forward(as)...); };
return 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());
}).get();
});
}Solution
(Self-answering for posterity.)
There's a major flaw in my above implementation of
The reason I was calling
is
is
Turns out that
In Boost's case, this is currently provided as a constructor
In the Concurrency TS,
So my solution was simply to add a call to
There's another showstopping problem with the above code: it doesn't compile for more than one argument, because I'm missing the
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
My final code:
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 ofauto 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>, which means that the type ofauto EXPRESSION =
f.then([...](auto f) {
return SUBEXPRESSION;
});
});is
std::future, which is to say std::future>>. 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>, then decltype(f.unwrap()) is future.In Boost's case, this is currently provided as a constructor
explicit 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>.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> p;
p.set_value({});
return p.get_future();
}
template
auto when_all(F f, Rest... rest) {
return future>(
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());
}
);
})
);
}Code Snippets
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());
})auto EXPRESSION =
f.then([...](auto f) {
return SUBEXPRESSION;
});
});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());
}
);
})
);
}Context
StackExchange Code Review Q#156388, answer score: 5
Revisions (0)
No revisions yet.