Why std::thread() passes arguments by value (and why the reason given by Dr. Stroustrup is incorrect)?

159 views Asked by At

Quoting from The C++ Programming Language (by Bjarne Stroustrup), page 1213

The thread constructors are variadic templates (§28.6). This implies that to pass a reference to a thread constructor, we must use a reference wrapper (§33.5.1).

For example:

void my_task(vector<double>& arg);
void test(vector<double>& v)
{
    thread my_thread1 {my_task,v};           //oops: pass a copy of v
    thread my_thread2 {my_task,ref(v)};      // OK: pass v by reference
    thread my_thread3 {[&v]{ my_task(v); }}; // OK: dodge the ref() problem
    // ...
}

The problem is that variadic templates have no problem to pass arguments by reference to a target function.

For example:

void g(int& t)
{
}

template <class... T>
void f(T&&... t)
{
    g(std::forward<T>(t)...);
}

int main()
{
    int i;
    f(i);
    return 0;
}

The only reason that std::thread passes by value is because the standard requires to use std::decay on the arguments.

Am I correct?

Can somebody please explain this quote by Stroustrup?

2

There are 2 answers

5
j6t On BEST ANSWER

Passing by reference by default would be a major foot-gun: when the thread accesses local variables, they may well be out of scope by the time that the thread runs, and it would have only dangling references. To make the use of values safer, the code must specify explicitly which variables are safe to access by reference in one of the ways that you showed.

13
user17732522 On

The problem is that variadic templates have no problem to pass arguments by reference to a target function.

When you have a variadic template of this kind with a template parameter pack T, you can write the function parameter either as

T... t

or as

T&&... t

If you take the former form, then you can't pass-by-reference directly. T will always deduce to non-reference types. Also, you probably don't want this form as it always implies an extra copy/move.

If you use the second form T&& will always deduce to a reference. There is no possibility for it to deduce to a non-reference type and so it will always pass-by-reference.

However, abstractly, std::thread's constructor should be able to pass the arguments by-reference as well as by-value to the new thread. So, naively, this interface can't enable both. Either the deduced references for T&& are always passed on as references or they are always used to construct std::decay_t<T> objects from them to pass on. There is no way to distinguish that in deduction.

So the only solution to support both is to encode whether or not you want pass-by-reference or pass-by-value in the type T itself. You could either decide that the default is pass-by-value and a wrapper type std::reference_wrapper<U> indicates pass-by-reference, or you could decide that pass-by-reference is the default and pass-by-value requires some kind of value_wrapper<U> wrapper type.

The former makes much more semantic sense and is safer, so that's what's always chosen in the standard library for interfaces that need to be able to do this abstract passing by-value as well as by-reference.