传递参数
在 C++ 中,创建线程时可以向函数传递参数。std::thread
构造函数接受函数指针或可调用对象作为第一个参数,后续参数会传递给该函数(或可调用对象)。
std::thread
的参数传递机制
- 拷贝(或移动)所有传入的参数;
- 然后,在新线程启动时,把拷贝(或移动)后的对象当作实参传给目标函数。
这意味着:
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 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)); |