1

I'm attempting to implement std::async from scratch, and have run into a hiccup with arguments of move-only type. The gist of it is, C++14 init-captures allow us to capture single variables "by move" or "by perfect forwarding", but they do not appear to let us capture parameter packs "by move" nor "by perfect forwarding", because you can't capture a parameter pack by init-capture — only by named capture.

I've found what appears to be a workaround, by using std::bind to capture the parameter pack "by move", and then using a wrapper to move the parameters out of the bind object's storage into the parameter slots of the function I really want to call. It even looks elegant, if you don't think too much about it. But I can't help thinking that there must be a better way — ideally one that doesn't rely on std::bind at all.

(Worst case, I'd like to know how much of std::bind I'd have to reimplement on my own in order to get away from it. Part of the point of this exercise is to show how things are implemented all the way down to the bottom, so having a dependency as complicated as std::bind really sucks.)

My questions are:

  • How do I make my code work, without using std::bind? (I.e., using only core language features. Generic lambdas are fair game.)

  • Is my std::bind workaround bulletproof? That is, can anybody show an example where the STL's std::async works and my Async fails?

  • Pointers to discussion and/or proposals to support parameter-pack capture in C++1z will be gratefully accepted.

Here's my code:

template<typename UniqueFunctionVoidVoid>
auto FireAndForget(UniqueFunctionVoidVoid&& uf)
{
    std::thread(std::forward<UniqueFunctionVoidVoid>(uf)).detach();
}

template<typename Func, typename... Args>
auto Async(Func func, Args... args)
     -> std::future<decltype(func(std::move(args)...))>
{
    using R = decltype(func(std::move(args)...));
    std::packaged_task<R(Args...)> task(std::move(func));
    std::future<R> result = task.get_future();

#ifdef FAIL
    // sadly this syntax is not supported
    auto bound = [task = std::move(task), args = std::move(args)...]() { task(std::move(args)...) };
#else
    // this appears to work
    auto wrapper = [](std::packaged_task<R(Args...)>& task, Args&... args) { task(std::move(args)...); };
    auto bound = std::bind(wrapper, std::move(task), std::move(args)...);
#endif

    FireAndForget(std::move(bound));

    return result;
}

int main()
{
    auto f3 = [x = std::unique_ptr<int>{}](std::unique_ptr<int> y) -> bool { sleep(2); return x == y; };
    std::future<bool> r3 = Async(std::move(f3), std::unique_ptr<int>{});
    std::future<bool> r4 = Async(std::move(f3), std::unique_ptr<int>(new int));
    assert(r3.get() == true);
    assert(r4.get() == false);
}
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • you should prefer forwarding references with `std::forward` to taking parameters by value and using `std::move` – Piotr Skotnicki Jul 22 '15 at 07:44
  • Tuples can give you a lot of leeway when it comes to storing packs of things (contrast `std::tie`, `std::make_tuple`, `std::forward_as_tuple` or using a constructor directly) and when it comes to 'restoring' the original pack elements as well. On the other hand, this begs the question on how to implement a tuple easily enough for didactic purposes and/or how to 'expand' tuple elements into a call. Would taking `std::tuple` for granted prevent an answer from being useful? – Luc Danton Jul 22 '15 at 07:53
  • @PiotrS I don't believe it's *possible* to implement `async` without capturing the arguments by value. Otherwise you end up with dangling references by the time you get around to executing the task. (Feel free to prove me wrong by writing some code in an answer!) @LucDanton sure, I'm happy to use `tuple` in a solution, but I don't know how to "expand tuple elements into a call" without a lot of lines of code, even in C++14. – Quuxplusone Jul 22 '15 at 08:17

1 Answers1

2

It was suggested to me offline that another approach would be to capture the args pack in a std::tuple, and then re-expand that tuple into the argument list of task using something like std::experimental::apply (coming soon to a C++17 standard library near you!).

    auto bound = [task = std::move(task), args = std::make_tuple(std::move(args)...)]() {
        std::experimental::apply(task, args);
    };

This is much cleaner. We've reduced the amount of library code involved, down from bind to "merely" tuple. But that's still a big dependency that I'd love to be able to get rid of!

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159