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);
}
4

1 回答 1

2

有人离线向我建议,另一种方法是args在 a 中捕获包std::tuple,然后将该元组重新扩展到task使用类似std::experimental::apply(即将出现在您附近的 C++17 标准库!)的参数列表中。

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

这干净多了。我们已将所涉及的库代码数量从“仅”减少bind到“仅” tuple。但这仍然是一个很大的依赖,我希望能够摆脱它!

于 2015-07-24T00:45:51.013 回答