1

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?

cigien
  • 57,834
  • 11
  • 73
  • 112
  • Please post an [MCVE], the code you claim works, can't possibly compile. – jwdonahue Jun 04 '23 at 00:48
  • @jwdonahue, Done. Edited. – PhiloRobotist Jun 04 '23 at 00:51
  • The dupe: [When is the use of std::ref necessary?](https://stackoverflow.com/questions/11833070/when-is-the-use-of-stdref-necessary) – 273K Jun 04 '23 at 00:54
  • 1
    The whole problem dissapears if you learn about [lambda expressions](https://en.cppreference.com/w/cpp/language/lambda). Then you can write `std::thread worker([&x]{thread_executor(x);});` and let the lambda do all the capture work. Also note that using global variables is not recommended. You also should NOT use `#include`, where are you learning that from? – Pepijn Kramer Jun 04 '23 at 06:13
  • @PepijnKramer Yeah, i know I shouldn't use global variables. I'm learning multi-threading. So I used Global variables for the simplicity of non-multi-threading things. Same goes for `#include`. – PhiloRobotist Jun 05 '23 at 02:36

2 Answers2

4

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 tuple like and thrn invoking it on another thread. Because it is definitely unsafe to capture the 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 thread function as rvalues not as lvalues.

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_executor, X);

here, X is copied into a temporary tuple, then the copy is passed to thread_exrcutor as a rvalue. Rvalues of type int cannot bind to int& (aka an lvalue reference to int) so the compiler rejects your code.

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

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.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312