Search code examples
c++multithreadingparameterscompilationref

std::thread constructor: passing a value by reference needs to call ref(), why?


(1) I've this code snippet:

void tByRef(int& i) {
    ++i;
}

int main() {
    int i = 0;
    tByRef(i); // ok
    thread t1(tByRef, i); // fail to compile
    thread t2(tByRef, (int&)i); // fail to compile
    thread t3(tByRef, ref(i)); // ok
    return 0;
}

As you could see, function tByRef accepts a lvalue reference as parameter to change the i value. So calling it directly tByRef(i) passes compilation.

But when I try to do same thing for thread function call, e.g. thread t1(tByRef, i), it fails to compile. Only when I added ref() around i, then it gets compiled.

Why need extra ref call here? If this is required for passing by reference, then how to explain that tByRef(i) gets compiled?

(2) I then changed tByRef to be template function with && parameter, this time, even t3 fails to compile:

template<typename T>
void tByRef(T&& i) {
    ++i;
}

This && in template parameter type is said to be reference collapse which could accept both lvalue and rvalue reference. Why in my sample code, t1, t2, t3 all fails to compile to match it?

Thanks.


Solution

  • Threads execute asynchronously from the code that started them. That's kind of the point. This means that, when a thread function actually gets called, the code that started the thread may well have left that callstack. If the user passed a reference to a local variable, that variable may be off the stack by the time the thread function gets called. Basically, passing by reference to a thread function is highly dangerous.

    However, in C++, passing a variable by reference is trivial; you just provide the name to the function that takes its parameter by reference. Since it is so dangerous in this particular case, std::thread takes steps to prevent you from doing it.

    All arguments to the thread function are copied/moved into internal storage when the thread object is created, and your thread function's parameters are initialized from those copies.

    Now, thread could initialize non-const lvalue reference parameters with a reference to the internal object for that parameter. However, a function which specifically takes a non-const lvalue reference is almost always a function that is expected to modify this value in a way that will be visible to others. But... it won't be visible to anyone, because it will be given a reference to an object stored internally in the thread that is accessible to no one else.

    In short, whatever you thought was going to happen will not happen. Hence the compile error: thread is specifically designed to detect this circumstance and assume that you've made some kind of mistake.

    However, while non-const lvalue reference parameters are inherently dangerous, they can still be useful. So std::ref is used as a way for a user to explicitly ask to pass a reference parameter.


    As for why it fails to compile in your second example, tByRef in this case is not the name of a function. It is the name of a template. std::thread expects to be given a value which it can call. A template is not a value, nor is it convertible to a value.

    A function template is a construct which generates a function when provided with template parameters. The template name alone is not a function.