文章

传递参数

std::thread构造函数拷贝参数,引用需用std::ref,移动语义用std::move避免拷贝或悬空。

传递参数

传递参数

在 C++ 中,创建线程时可以向函数传递参数。std::thread 构造函数接受函数指针或可调用对象作为第一个参数,后续参数会传递给该函数(或可调用对象)。

std::thread 的参数传递机制

  1. 拷贝(或移动)所有传入的参数;
  2. 然后,在新线程启动时,把拷贝(或移动)后的对象当作实参传给目标函数。

这意味着:

1
std::thread t(f, arg1, arg2);

等价于:

1
2
3
auto copied_arg1 = arg1;  // 拷贝/移动
auto copied_arg2 = arg2;
launch_thread([=] { f(copied_arg1, copied_arg2); });

值传递:自动拷贝

1
2
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
  • "hello" 是字符串字面值(const char*),但 f 需要 std::string
  • 线程构造时,自动将其转换为 std::string,并拷贝到新线程内存空间。
    • 传给 std::thread 构造函数的参数是“值传递”(拷贝或移动)进去的。
    • 线程函数中的引用参数,是对 std::thread 内部拷贝的参数的引用。
    • 不是直接引用调用处的变量,而是引用了线程内部拷贝的那个副本。
  • 所有参数都会被拷贝进新线程。

悬空指针风险:局部变量传递

1
2
3
4
5
6
7
8
void f(int i, std::string const& s);

void oops(int some_param) {
    char buffer[1024];  // 局部变量
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, buffer);  // 传入局部变量
    t.detach();  // 主线程提前结束,buffer 可能已销毁
}
  • buffer 是局部变量,生命周期受限于 oops 函数。
  • std::thread 创建新线程时并不能保证立刻执行。
  • 若线程延迟执行,可能访问已销毁的 buffer,引发 未定义行为

正确写法:先转换为 std::string

1
2
3
4
5
6
void not_oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, std::string(buffer));  // 显式构造 string
    t.detach();
}

整个过程等价于这样:

1
2
std::string temp = std::string(buffer);  // 构造一个字符串副本
std::thread t(f, 3, temp);               // 把 temp 拷贝进 thread 的上下文

线程函数里接收的是 const std::string&,引用的是线程内部保存的 temp 副本。

引用传递:需要 std::ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数声明:第二个参数是非常量左值引用,必须传入一个真正的变量(不能是临时值)
// 该函数会在线程中尝试修改 data 的内容
void update_data_for_widget(widget_id w, widget_data& data);

void oops_again(widget_id w) {
    // 创建一个本地 widget_data 对象,用于在主线程和子线程之间共享
    widget_data data;

    // 尝试创建线程,目标函数是 update_data_for_widget,传入 w 和 data
    // 错误点:
    //    std::thread 的构造函数会拷贝(或移动)所有传入的参数
    //    所以它试图将 data 拷贝到线程的内部上下文中,然后再传给函数
    //    但函数参数要求是 widget_data&(非常量左值引用):
    //    —— 无法用临时值(右值)绑定到非常量左值引用,会导致编译错误
    std::thread t(update_data_for_widget, w, data);  // ❌ 编译失败

    t.join();  // 等待线程结束

    // 处理 data(如果上面的线程能修改成功,这里将看到修改结果)
    // 但由于传参失败,这里不会发生预期中的修改
    process_widget_data(data);
}
  • update_data_for_widget 期望引用,但传入的是一个拷贝。
  • 第二个参数是非常量左值引用,函数只接受「左值变量」。而 std::thread 构造时:
    • 它把 data 拷贝成一个临时对象;
    • 然后尝试将这个临时对象(右值)绑定给 widget_data& data
    • 非法!因为非常量左值引用不能绑定到右值。
  • 编译器会报错(不能用右值初始化非常量引用)。

正确写法:使用 std::ref

1
std::thread t(update_data_for_widget, w, std::ref(data));
  • 使用 std::ref(data) 告诉线程构造函数按引用传递。

调用类的成员函数

无参成员函数

1
2
3
4
5
6
7
class X {
public:
    void do_lengthy_work();
};

X my_x;
std::thread t(&X::do_lengthy_work, &my_x);  // 等效于 my_x.do_lengthy_work()

带参数成员函数

1
2
3
4
5
6
7
8
class X {
public:
    void do_lengthy_work(int val);
};

X my_x;
int num = 42;
std::thread t(&X::do_lengthy_work, &my_x, num);  // 等效于 my_x.do_lengthy_work(42)
  • 第一个参数是成员函数指针,第二个参数是对象指针,之后是参数列表。

只支持移动的对象:用 std::move

有些对象(如 std::unique_ptr)不可拷贝,只能移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 声明一个函数,接收一个 std::unique_ptr 拥有的 big_object
// 注意:参数是按值(通过移动语义)传入的,不是引用
void process_big_object(std::unique_ptr<big_object>);

// 创建一个 std::unique_ptr,指向动态分配的 big_object 对象
std::unique_ptr<big_object> p(new big_object);

// 调用成员函数,准备数据
p->prepare_data(42);

// 使用 std::move 将 p 的所有权转移给线程
// 注意:std::thread 会拷贝/移动所有参数到它的内部(即线程上下文)
//      所以必须 std::move(p),否则不能把 unique_ptr 传进去(它不可拷贝)
//      线程内部再将该 unique_ptr 移交给 process_big_object 函数
std::thread t(process_big_object, std::move(p));
  • std::move(p)p 所有权转移到线程中。
  • 原对象 p 在主线程中会变成空指针。

关于 std::thread 自身的移动语义

  • std::thread 本身是 可移动但不可拷贝 的对象。

    1
    2
    
    std::thread t1(func);
    // std::thread t2 = t1;  // ❌ 编译错误!不能复制线程对象
    
    • 一个 std::thread 实例代表一个具体的线程执行权(ownership)。 如果允许复制,那两个对象都指向同一个线程,谁负责回收资源?会出混乱!
    • 虽然不能复制,但可以std::move() 转移一个线程对象的所有权
    1
    2
    
    std::thread t1(func);           // t1 拥有线程
    std::thread t2 = std::move(t1); // t2 拥有线程,t1 被置空
    
  • 每个线程对象只能拥有一个线程的执行权。
  • 可以使用 std::movestd::thread 实例的所有权转交给其他线程变量。

参数传递方式对比

场景默认行为正确写法风险或注意事项示例代码片段
普通值类型拷贝直接传入std::thread t(f, 42);
字符串字面值拷贝指针std::string("hello")指针本身是静态安全,但指向局部数组就悬空std::thread t(f, std::string("hello"));
局部变量指针拷贝指针提前构造为 std::string函数返回后局部数据销毁,指针悬空char buf[100]; std::thread t(f, std::string(buf));
非 const 引用参数拷贝使用 std::ref(...)默认拷贝而非引用,修改无效std::thread t(f, std::ref(data));
成员函数调用函数 + 对象指针成员函数 + 对象指针 + 参数顺序语法不同于普通函数指针调用std::thread t(&X::func, &x, arg1);
只可移动对象编译失败使用 std::move(...)所有权转移,源对象被置空std::thread t(f, std::move(p));
本文由作者按照 CC BY 4.0 进行授权