在C++11中将对象通过引用传递给std::thread

110

为什么在创建 std::thread 时不能通过引用传递对象?

例如,以下代码片段会导致编译错误:

#include <iostream>
#include <thread>

using namespace std;

static void SimpleThread(int& a)  // compile error
//static void SimpleThread(int a)     // OK
{
    cout << __PRETTY_FUNCTION__ << ":" << a << endl;
}

int main()
{
    int a = 6;

    auto thread1 = std::thread(SimpleThread, a);

    thread1.join();
    return 0;
}

错误:

In file included from /usr/include/c++/4.8/thread:39:0,
                 from ./std_thread_refs.cpp:5:
/usr/include/c++/4.8/functional: In instantiation of ‘struct std::_Bind_simple<void (*(int))(int&)>’:
/usr/include/c++/4.8/thread:137:47:   required from ‘std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(int&); _Args = {int&}]’
./std_thread_refs.cpp:19:47:   required from here
/usr/include/c++/4.8/functional:1697:61: error: no type named ‘type’ in ‘class std::result_of<void (*(int))(int&)>’
       typedef typename result_of<_Callable(_Args...)>::type result_type;
                                                             ^
/usr/include/c++/4.8/functional:1727:9: error: no type named ‘type’ in ‘class std::result_of<void (*(int))(int&)>’
         _M_invoke(_Index_tuple<_Indices...>)
         ^

我已经改为传递指针,但是否有更好的解决方法?

4个回答

142

使用 std::ref 通过一个 reference_wrapper 显式地初始化线程:

auto thread1 = std::thread(SimpleThread, std::ref(a));

根据 cppreference 上关于 std:thread 的注释,需要将线程函数的参数按值移动或复制。如果需要将引用类型的参数传递给线程函数,则必须使用包装器(例如使用 std::ref 或者 std::cref)。


26
合理地,捕获操作默认采用值捕获,否则如果参数在线程读取之前消失,事情就会变得非常糟糕。你需要显式地要求这种行为,以便有某些指示表明你正在承担确保引用目标仍然有效的责任。 - David Schwartz
3
太好了!我希望有一种明确传递引用的方式。再次感谢 C++11 :) - austinmarton

38

根据这个评论,本答案详细阐述了为什么参数默认情况下不会以引用的方式传递到线程函数中。

考虑以下函数SimpleThread()

void SimpleThread(int& i) {
    std::this_thread::sleep_for(std::chrono::seconds{1});
    i = 0;
}

现在,想象一下如果以下代码被编译(实际上它不能被编译)会发生什么:

int main()
{
    {
        int a;
        std::thread th(SimpleThread, a);
        th.detach();
    }
    // "a" is out of scope
    // at this point the thread may be still running
    // ...
}
参数 a 将会被传递到 SimpleThread() 中作为引用。变量 a 已经超出了其作用域并结束了生命周期,但线程在函数 SimpleThread() 内可能仍处于睡眠状态。如果是这样,在 SimpleThread() 中的 i 实际上将会是一个 悬空引用,并且赋值操作 i = 0 将会导致 未定义的行为
通过使用类模板 std::reference_wrapper(使用函数模板 std::refstd::cref)来包装引用参数,您可以明确表达您的意图。

2
我认为这是关于“为什么参数不通过引用传递”的错误论点,因为即使在这种情况下使用std::ref(a)也会导致未定义的行为,不是吗? - ampawd
1
@ampawd 是的,使用 std::ref(a) 仍然会导致未定义行为。但是,显式地输入 std::ref(a) 会警告你不要通过引用传递 a,而只输入 a 则不会。 - JFMR
7
必须写std::ref(a)(而不仅是a)来通过引用传递a,这可以防止您误以为a是按值传递。 - JFMR
即使我们使用了 std::ref,在上面的例子中仍然会有悬空引用,所以没有任何改变,对吧?那么,你的意思是,使用 std::ref 稍微好一些,因为它明确表明正在使用一个引用? - starriet
1
@starriet:在某些类似的情况下,传递引用是安全的(例如,如果在引用超出范围之前线程已经被join,或者线程被传递了一个具有static存储期的数据的引用)。使用std::ref是在断言这是其中一种情况。它确保某人选择使用这些语义,而不是意外地这样做。它不能阻止您创建未定义行为,但可以防止您意外地这样做。 - undefined

10
std::thread会拷贝(/移动)它的参数,你可能会看到这样的说明:

线程函数的参数是按值传递的。如果需要将引用参数传递给线程函数,则需要对其进行包装(例如使用 std::refstd::cref)。

因此,你可以使用std::reference_wrapper通过std::ref/std::cref来解决这个问题。
auto thread1 = std::thread(SimpleThread, std::ref(a));

或使用lambda:

auto thread1 = std::thread([&a]() { SimpleThread(a); });

1
如果您的对象是基于堆栈分配的,请通过指向在调用线程API时创建的新对象的指针进行传递,不要通过引用传递。这样的对象将与线程一起存在,但应在线程终止之前明确删除。
示例:
void main(){
...
std::string nodeName = "name_assigned_to_thread";
std::thread nodeThHandle = std::thread(threadNODE, new std::string(nodeName));
...
}

void threadNODE(std::string *nodeName){
/* use nodeName everywhere but don't forget to delete it before the end */
delete nodeName;
}

在这里传递指针没有任何好处;如果指针像所示一样被分配,那么最好直接按值传递(因为没有其他东西会共享指针,因此不可能进行线程通信)。要么按值传递(当不需要共享时),要么按引用传递(当需要共享且保证该值在线程中使用之后仍然存在时),要么按shared_ptr传递(当您需要共享并且不知道哪个线程将首先停止使用它时)。线程假定所有权的原始指针传递与按值传递几乎相同。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接