5
\$\begingroup\$

Working with futures and .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<class... Types>
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<std::tuple<>> when_all()
{
    promise<std::tuple<>> p;
    p.set_value({});
    return p.get_future();
}

template<class F, class... Rest>
future<std::tuple<F, Rest...>> 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<decltype(as)>(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();
    });
}
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

(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());
                }
            );
        })
    );
}
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.