std::bind
and std::thread
share a few design principles. Since both of them store local objets corresponding to the passed arguments, we need to use either std::ref
or std::cref
if reference semantics is desired:
void f(int& i, double d) { /*...*/ }
void g() {
int x = 10;
std::bind(f, std::ref(x), _1) (3.14);
std::thread t1(f, std::ref(x), 3.14);
//...
}
But I'm intrigued by a recent personal discovery: std::bind
will allow you to pass a value in the case above, even though this is not what one usually wants.
std::bind(f, x, _1) (3.14); // Usually wrong, but valid.
However, this is not true for std::thread
. The following will trigger a compile error.
std::thread t2(f, x, 3.14); // Usually wrong and invalid: Error!
At first sight I thought this was a compiler bug, but the error is indeed legitimate. It seems that the templated version of std::thread
's constructor is not able to correctly deduce the arguments due to the copy decaying requirement (tranforming int&
in int
) imposed by 30.3.1.2.
The question is: Why not require something similar to std::bind
's arguments? Or is this apparent inconsistency intended?
Note: Explained why it's not a duplicated in the comment below.
If you have used std::thread or std::bind , you probably noticed that even if you pass a reference as parameter, it still creates a copy instead. From cppreference, The arguments to the thread function are moved or copied by value.
In c++11 to pass a referenceto a thread, we have std::ref(). std::thread t3(fun3, std::ref(x)); In this statement we are passing reference of x to thread t3 because fun3() takes int reference as a parameter.
An std::reference_wrapper is a copyable and assignable object that emulates a reference. Contrary to its name, it does not wrap a reference. It works by encapsulating a pointer (T*) and by implicitly converting to a reference (T&).
The function object returned by bind
is designed for reuse (i.e., call be called multiple times); it therefore must pass its bound arguments as lvalues, because you don't want to move from said arguments or later calls would see a moved-from bound argument. (Similarly, you want the function object to be called as an lvalue, too.)
This concern is inapplicable to std::thread
and friends. The thread function will only be called once, with the provided arguments. It's perfectly safe to move from them because nothing else is going to be looking at them. They are effectively temporary copies, made just for the new thread. Thus the function object is called as an rvalue and the arguments are passed as rvalues.
std::bind
was mostly obsolete when it arrived due to the existence of lambdas. With C++14 improvements and C++17 std::apply
, the remaining use cases for bind
are pretty much gone.
Even in C++11 the cases where bind
solved a problem that a lambda did not solve where relatively rare.
On the other hand, std::thread
was solving a slightly different problem. It doesn't need the flexibility of bind
to "solve every issue", instead it could block what would usually be bad code.
In the bind
case The reference passed to f
won't be x
but rather a reference to an internal stored copy of x
. This is extremely surprising.
void f(int& x) {
++x;
std::cout << x << '\n';
};
int main() {
int x = 0;
auto b = std::bind(f, x);
b();
b();
b();
std::cout << x << '\n';
}
prints
1
2
3
0
where the last 0
is the original x
, while 1
2
and 3
are the incremented copy of x
stored within f
.
With lambda, the difference between a mutable stored state and an external reference can be made clear.
auto b = [&x]{ f(x); };
vs
auto b = [x]()mutable{ f(x); };
one of which copies x
then invokes f
repeatedly on it, the other passes a reference to x
into f
.
There really isn't a way to do this with bind
without allowing f
to access the stored copy of x
as a reference.
For std::thread
, if you want this mutable local copy behavior you just use a lambda.
std::thread t1([x]()mutable{ f(x); });
In fact, I would argue most of the INVOKE syntax in C++11 seems to be a legacy of not having C++14 power lambdas and std::apply
in the language. There are very few cases not solved by lambda and std::apply
(apply is needed as lambdas don't easily support moving packs into them then extracing them inside).
But we don't have a time machine, so we have these multiple parallel ways to express the idea of invoking something in a specific context in C++.
From what I can tell, thread
started off with basically the same rules as bind
, but was modified in 2010 by N3090 to take on the constraint you've identified.
Using that to bisect the various contributions, I believe you're looking for LWG issue 929.
Ironically, the intention seems to have been to make the thread
constructor less constrained. Certainly there's no mention of bind
, although this wording was later also applied to async
("Clean-up" section after LWG 1315), so I would say bind
got left behind.
It's quite hard to be sure, though, so I would recommend asking the committee itself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With