// Inspired by examples in section 2.2 of Anthony Williams C++ Concurrency in Action
void f(int &i) { i = 2; /* ... */ }
int i;
f(i); // i passed by reference, f can change i
thread tf = thread(f, i); // i not passed by reference, so changes to i are silently discarded
While they can always force a pass by reference,e.g., by
thread t(f, ref(i));
this is entirely different from how functions arguments are ordinarily passed, and omitting the
ref is likely to result in a silent failure.
We can even have silent errors introduced for parameters that are passed by value
// Another variation on an example in section 2.2 of Anthony Williams C++ Concurrency in Action
void f(int i,std::string s); // Unlike in the book, s is passed by value.
void my_oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
// Intermittent failures as buffer may not be converted to a string during the lifetime of buffer
std::thread t(f,3,buffer);
t.detach();
}
I believe requiring most C++ programmers to be conversant with these kinds of silent change of signature is a pretty high bar. This is borne out by my experience teaching C++ to Masters' students, possibly because the reason for this change isn't apparent until one is comfortable writing templates, instead they naturally expect asynchronous functions to obey the same parameter passing rules as ordinary functions.
Note: All of the above discussion applies equally to bind.
The underlying problem for this and similar problems is that we cannot deduce the function signature of a callable template. std::result_of can give us the return type but not the parameter types.
We propose addressing this with a (compiler-supported) type trait std::signature whose type typedef is the function type of the callable object when called with the given parameter types.
For example, if we define C as
struct C {
int operator()(double d, int i);
int operator()(double d1, double d2);
};
then signature<C(int, int)>::type would be int(double, double).
As an extended example, we can create a new async that avoids the above problems by taking its arguments with the same signature as its callable does:
// more_perfect_forwarding_async.h
#include<iostream>
#include<utility>
#include<future>
#include<type_traits>
// Uses "apply" template for unpacking tuple members into arguments written by DRayX at
// http://stackoverflow.com/questions/687490/how-do-i-expand-a-tuple-into-variadic-template-functions-arguments
#include"apply.h"
template<typename Callable, typename Signature> struct Caller;
template<typename Callable, typename Result, typename...Args>
struct Caller<Callable, Result(Args...)> {
Caller(Callable c, Args&&... args) : callable(c), saved_args(std::forward<Args...>(args...)) {}
Result operator()() { return apply(callable, saved_args); }
std::tuple<Args...> saved_args;
Callable callable;
};
template<typename Callable, typename... Args>
future<typename result_of<Callable(Args...)>::type>
more_perfect_forwarding_async(Callable c, Args&&... args)
{
return std::async(Caller<Callable, typename signature<Callable(Args...)>::type>(c, std::forward<Args>(args)...));
}
Now, we can run asynchronous functions without worrying about implicit signature changes
#include"more_perfect_forwarding_async.h"
#include<iostream>
int func(int &i) { i = 2; return i; }
int main()
{
int i = 5;
auto f = more_perfect_forwarding_async(func, i);
f.get();
cout << "i = " << i << endl; // Correctly prints 2
}