Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is ref & cref needed for reference arguments to a function passed to std::thread?

Tags:

c++

c++11

What do std::ref and std::cref do here in simple terms? I read the documentation here but couldn't understand. It'd be great if someone can explain in the context of below code!

I have this piece of code which works

#include<bits/stdc++.h>

int X=0;
void thread_executor(int &X) {
    //do nothing
}

int main() {
  std::thread worker1(thread_executor, std::ref(X));
  return 0;
}

But if I change the line of thread creation to

std::thread worker1(thread_executor, X);

it doesn't work. What's going on here?

like image 801
PhiloRobotist Avatar asked Sep 11 '25 03:09

PhiloRobotist


2 Answers

std::ref makes a std::reference_wrapper of the appropriate type.

Reference wrappers are pointers to the referred to thing and act as value types, but also implicitly convert to (or be explicitly converted to) references.

So a reference wrapper can be stored in a vector.

In your case, std thread is storing the parameters in something "std::tuple"-like and then invoking the callable with those parameters on another thread. Because it is definitely unsafe to capture the local arguments as parameters by reference when the invoking could occur long after local scope is dead, the parameters are captured by value. In addition, because the values are discarded after the thread starts, they are passed to the threaded-off function as rvalues (readable values) not as lvalues (writable values), because writing to something that is immediately discarded is probably a bug.

Rvalues are stuff safe to read from but no guarantee they stick around. Lvalues are stuff it makes sense to assign to, and assigning to something that is being discarded is usually nonsense. L and R here come from left and right sides of the assignment operator (Left = Right) from C.

So

std::thread worker1(thread_function, X);

here, X is copied into a helper "tuple", then the tuple-wrapped copy is passed to thread_function as a rvalue. Rvalues of type int cannot bind to int& (aka an lvalue reference to int) so the compiler rejects your code. This is a good thing, because functions taking int& usually expect to write to those values and the caller getting the result; here, the int passed in is not the one the programmer passed to worker1, and in fact ceases to exist immediately after the thread_function finishes.

When you create a std::reference_wrapper<int> and pass it in, rvalues of that type can convert to int&. So the code is accepted.

std::ref(X) is equivalent to std::reference_wrapper<int>(X); it just deduces int for you.

std::cref just injects a const; sts::cref(X) is std::reference_wrapper<int const>(X).

By using std::ref you promise to manage lifetime properly here. The language and library was designed to make the lifetime errors you'd get without it less common.

Using std::ref on a local variable is probably an error, but C++ won't stop you. Using it on a member of a struct or object you know will outlast the thread, however, can be useful - you'll have to carefully manage race conditions (ensure your never look at it until you know that the thread is finished writing to it, for example - ie, you could .join the thread before reading the variable, or use a condition variable and mutex to signal, or something else).

like image 54
Yakk - Adam Nevraumont Avatar answered Sep 12 '25 16:09

Yakk - Adam Nevraumont


The fundamental problem here is that std::thread's constructor doesn't "know" that the initial function of the thread has an int& parameter.

You might ask why it isn't smart enough to see that the signature of thread_executor is void (int&). Well, in this particular case it could detect it, but in the more general case it wouldn't be possible. The reason for this is that, instead of passing thread_executor to the std::thread constructor, you could have passed some class object that has multiple overloads of operator(). The particular function that would actually be called would be determined by overload resolution. In current C++ there is no way to determine programmatically which overload will get called. So, in general, the std::thread constructor (assuming that it is implemented using something resembling ordinary C++ and is not deeply magic) doesn't know the signature of the function that will actually be called.

Yet the std::thread constructor has to make a decision of whether to accept the argument X by value or by reference, without knowing what the initial function wants. So it does the safest thing: it always takes its arguments by value. That means X, being an lvalue, will be copied, and if you had passed an rvalue instead, it would be moved. The result of the copy or move is then passed to the initial function of the thread.

As a result, the code without std::ref is buggy. If it did compile, then the reference argument would refer to the copy, not the original X. In order to prevent this situation, the std::thread constructor passes the copy as an rvalue to the initial function of the thread. Now int& X cannot bind to that copy.

By using std::ref(X) you create a std::reference_wrapper<int> object that refers to X. This is a special type that is sort of like a pointer (in this case, a pointer to X) so when it is copied, it still points to the original X. And it can be converted to int&, so the reference parameter of thread_executor can be initialized from the reference wrapper, and refers to the object that the reference wrapper points to.

like image 29
Brian Bi Avatar answered Sep 12 '25 16:09

Brian Bi