c++stdasync

What's the difference between passing a function directly to std::async and using std::bind?


I recently started adding async support to a library I'm working on, but I hit a slight problem. I started off with something like this (full context later):

return executeRequest<int>(false, d, &callback, false);

That was before adding async support. I attempted to change it to:

return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);

But it failed to compile.

MCVE:

#include <iostream>
#include <future>

int callback(const int& t) {
    std::cout << t << std::endl;   
    return t;
}
class RequestData {
private:
    int x;
public:
    int& getX() {
        return x;   
    }
};
class X {
    public:
        template <typename T>
        T executeRequest(bool method, RequestData& requestData,
                       std::function<T(const int&)> parser, bool write) {
            int ref = 42;
            std::cout << requestData.getX() << std::endl;
            return parser(ref);
        }
        int nonAsync() {
            // Compiles 
            RequestData d;
            return this->executeRequest<int>(false, d, &callback, false);    
        }
        std::future<int> getComments() {
            RequestData d;
            // Doesn't compile 
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
        }
};

int main() {
    X x;
    auto fut = x.getComments();
    std::cout << "end: " << fut.get() << std::endl;
}

And it fails with:

In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1505:56: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
      typedef typename result_of<_Callable(_Args...)>::type result_type;
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:1709:49: note: in instantiation of template class 'std::_Bind_simple<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>' requested here
          __state = __future_base::_S_make_async_state(std::__bind_simple(
                                                       ^
main.cpp:33:25: note: in instantiation of function template specialization 'std::async<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool), X *, bool, RequestData &, int (*)(const int &), bool>' requested here
            return std::async(std::launch::async, &X::executeRequest<int>, this, false, d, &callback, false);
                        ^
In file included from main.cpp:2:
In file included from /usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/future:38:
/usr/bin/../lib/gcc/x86_64-linux-gnu/5.5.0/../../../../include/c++/5.5.0/functional:1525:50: error: no type named 'type' in 'std::result_of<std::_Mem_fn<int (X::*)(bool, RequestData &, std::function<int (const int &)>, bool)> (X *, bool, RequestData, int (*)(const int &), bool)>'
        typename result_of<_Callable(_Args...)>::type
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
2 errors generated.

Live example.

The only actual difference between the two (at least that I can see visibly) is that I need to explicitly pass this, because I'm referencing a member function

I played a little around with it, and managed to find that if I replace it with a const RequestData&, it's suddenly allowed. But it instead results in issues elsewhere, because the getter isn't const. At least from what I could find, I need to make it a const function, which is fine for the getter itself, but I also have some setters meaning I can't go with that.

Anyway, I figured I could try std::bind instead. I replaced the async call with:

auto func = std::bind(&X::executeRequest<int>, this, false, d, &callback, false);
return std::async(std::launch::async, func);

And, for some reason, it worked.

The thing that confuses me here, is that it uses the same arguments both times (all three times if you count the non-async variant), and takes the this argument into consideration, given the function I'm calling is a member function.

I dug deeper, and found some alternative solutions (referencing std::thread though), that used std::ref. I know std::async runs std::thread under the hood, so I dug up the documentation:

The arguments to the thread function are moved or copied by value. If a reference argument needs to be passed to the thread function, it has to be wrapped (e.g. with std::ref or std::cref). (emphasis mine)

That makes sense, and explains why it failed. I assume std::async is limited by this as well, and explains why it failed.

However, digging up std::bind:

The arguments to bind are copied or moved, and are never passed by reference unless wrapped in std::ref or std::cref. (emphasis mine)

I don't use std::ref (or if I replace with a const, std::cref) in either, but at least if I understood the documentation right, both of these should fail to compile. The example on cppreference.com also compiles without std::cref (tested in Coliru with Clang and C++ 17).

What's going on here?

If it matters, aside the coliru environment, I originally reproduced the issue in Docker, running Ubuntu 18.04 with Clang 8.0.1 (64 bit). Compiled against C++ 17 in both cases.


Solution

  • There is some slight differences in the standard. For std::bind:

    Requires: is_­constructible_­v<FD, F> shall be true. For each Ti in BoundArgs, is_­constructible_­v<TDi, Ti> shall be true. INVOKE(fd, w1, w2, …, wN) ([func.require]) shall be a valid expression for some values w1, w2, …, wN, where N has the value sizeof...(bound_­args). The cv-qualifiers cv of the call wrapper g, as specified below, shall be neither volatile nor const volatile.

    Returns: An argument forwarding call wrapper g ([func.require]). The effect of g(u1, u2, …, uM) shall be

    INVOKE(fd, std::forward<V1>(v1), std::forward<V2>(v2), …, std::forward<VN>(vN))
    

    Where v1, ..., vN have specific types. In your case, what matters is that the stored variable corresponding to d has type std::decay_t<RequestData&> which is RequestData. In this case, you can easily call executeRequest<int> with an lvalue RequestData.

    The requirements for std::async are much stronger:

    Requires: F and each Ti in Args shall satisfy the Cpp17MoveConstructible requirements, and

    INVOKE(decay-copy(std::forward<F>(f)),
       decay-copy(std::forward<Args>(args))...)     // see [func.require], [thread.thread.constr]
    

    The huge difference is decay-copy. For d, you get the following:

    decay-copy(std::forward<RequestData&>(d))
    

    Which is a call to the decay-copy function (exposition only), whose return type is std::decay_t<RequestData&>, so RequestData, which is why the compilation fails.


    Note that if you used std::ref, the behavior would be undefined since the lifetime of d may ends before the call to executeRequest.