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.
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
orstd::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
orstd::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.
There is some slight differences in the standard. For std::bind
:
Requires:
is_constructible_v<FD, F>
shall betrue
. For eachTi
inBoundArgs
,is_constructible_v<TDi, Ti>
shall betrue
.INVOKE(fd, w1, w2, …, wN)
([func.require]) shall be a valid expression for some valuesw1
,w2
, …,wN
, whereN
has the valuesizeof...(bound_args)
. The cv-qualifiers cv of the call wrapperg
, as specified below, shall be neither volatile nor const volatile.Returns: An argument forwarding call wrapper
g
([func.require]). The effect ofg(u1, u2, …, uM)
shall beINVOKE(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 eachTi
inArgs
shall satisfy the Cpp17MoveConstructible requirements, andINVOKE(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
.