When using std::thread class, why exactly can I pass lambda expression that capture variables by reference?

252 views Asked by At

I'm having a hard time with the following std::thread's note (from cppref):

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 or std::cref).

Okay - since there's no guarantee that the argument will remain alive up until the end of the thread's execution, it makes sense that the thread itself should own it. Now, consider the following code snippet:

#include <iostream>
#include <thread>

void
tellValue(int& value)
{
    std::cout << "The value is: " << value << std::endl;
}

int
main()
{
    int mainThreadVariable{0};

    std::thread thr0{tellValue, mainThreadVariable};

    thr0.join();

    return 0;
}

Clang does not accept this, neither should it - the thread doesn't own mainThreadVariable. Why, however, does it accept this:

#include <iostream>
#include <thread>

int
main()
{
    int mainThreadVariable{0};

    std::thread thr0{[&]()
                     {
                         std::cout << "The value is: " << mainThreadVariable
                                   << std::endl;
                         ++mainThreadVariable;
                     }};

    thr0.join();

    std::cout << "The value is: " << mainThreadVariable << std::endl;

    return 0;
}

The latter outputs the following:

./PassingArgumentsToAThread 
The value is: 0
The value is: 1

The thread neither copied mainThreadVariable nor moved it, because it is safe and sound at the end of the main thread. The callable object captured it by reference, so why was this allowed?

4

There are 4 answers

0
paddy On BEST ANSWER

Okay - since there's no guarantee that the argument will remain alive up until the end of the thread's execution, it makes sense that the thread itself should own it.

That's not actually what cppreference is trying to tell you. It really comes down to how bindings work. The solution is already described to you: use a reference wrapper std::ref.

This works:

std::thread thr0{tellValue, std::ref(mainThreadVariable)};

The callable object captured it by reference, so why was this allowed?

Under the hood, that reference-wrapper approach is essentially what your lambda with auto capture-by-reference is doing, too. Probably. The compiler might be able to improve on that in certain cases, but that's an implementation detail.

Clang does not accept this, neither should it - the thread doesn't own mainThreadVariable.

In neither case does the thread "own" the referenced object. It simply has a means of accessing it, regardless of whether or not your program uses it safely.

0
xaxxon On

Your first example doesn't fail because of the compiler knowing the lifetime of variables. It fails for this very specific reason:

https://godbolt.org/z/aYWaTzMT7

**/opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/bits/std_thread.h:129:72: error: static assertion failed: std::thread arguments must be invocable after conversion to rvalues **

The second version doesn't have this problem, so it's allowed.

The compiler cannot track variable scopes across threads and know what your program is supposed to do. That would essentially be solving the halting problem.

0
HolyBlackCat On

Because it's impossible for a template to analyze what kind of captures a lambda has.

1
Vishal On

In your first example, while calling from main, you are passing mainThreadVariable to function as a value and and your function void tellValue(int& value) expect argument as an reference. Basically, arguments passed in constructor of std::thread are forwarded as rvalues to the function tellValue. If you expect arguments as reference in your tellValue. then c++ provides a way to make it as reference using std::ref().

Modified version of your first example

#include <iostream>
#include <thread>

using namespace std;


void tellValue(int& value)
{
    std::cout << "\nThe value inside thread: " << value << std::endl;
    ++value;
    value  = 2000;
}

int main()
{
    int mainThreadVariable{0};

    std::thread thr0{tellValue, ref(mainThreadVariable)};

    thr0.join();
    cout << "\nThe value in main: " << mainThreadVariable;

    return 0;
}

It Prints

The value inside thread: 0

The value in main: 2000

And your second example captures all variables from the main thread as reference in your lambda function and this values you can modify in your lambda which is running inside thread.

You can read about lambda references captures here: https://en.cppreference.com/w/cpp/language/lambda