前言
在 std::bind
和 std::thread
中可能会遇到引用失效的问题,解决办法是通过 std::ref
传递引用。这通常不是什么难的技术问题,但是知其然知其所以然,为什么这样设计,也许值得深入探讨一下。
引用失效的情况
在以下两种情况下,引用可能失效,仍然导致拷贝操作
std::bind
std::thread
在 std::bind
中的场景:
#include <iostream>
class TestClass {
public:
TestClass() {
std::cout << "TestClass()" << std::endl;
}
TestClass(const TestClass&) {
std::cout << "TestClass(const TestClass&)" << std::endl;
}
void func() const {
std::cout << "func()" << std::endl;
}
};
void test(TestClass& t) {
t.func();
}
int main() {
TestClass t;
std::bind(test, t); // 发生拷贝
std::bind(test, std::ref(t)); // 不会发生拷贝
return 0;
}
在 std::thread
中的场景:
// TestClass 定义如上
void test(TestClass& t) {
t.func();
}
int main() {
TestClass t;
thread th1(test, t); // 编译不通过
th1.join();
thread th2(test, std::ref(t)); // 不会发生拷贝
th2.join();
return 0;
}
例外
在 lambda 中的场景:
// TestClass 定义如上
void test(TestClass& t) {
t.func();
}
int main() {
TestClass t;
std::bind([&t](){
t.func(); // 不会发生拷贝
});
thread th([&t] {
test(t); // 不会发生拷贝
});
th.join();
return 0;
}
原理
要搞明白为什么会这样,我们就得回到对象生命周期的问题上。在 std::bind
和 std::thread
中,传递的参数在什么时候被访问是不确定的,也许在该对象消亡之后,也许该对象还存在。
所以 std::bind
和 std::thread
就无法保证程序的正常执行,除非用户明确知道这两个类存在生命周期的问题,并加以管理。但这样显然对用户的负担太重,所以 std::bind
和 std::thread
默认是帮用户处理掉生命周期的问题——也就是复制。
在 std::bind
的参数中,默认是值语义传递的,所以即使是引用也会发生拷贝行为,这样就不会导致对象失效而发生错误。当我们需要引用时,必须加以明确说明,使用 std::ref
并自行负责声明周期的管理。
std::ref
的实现方式实际上就是返回一个 wrapper(std::reference_wrapper
),其中存放了对象的引用和值类型,在 std::bind
模板中会对 wrapper 进行特化,对其解包获得引用,虽然 wrapper 传递仍然是值语义,但其值内部存放的是引用,复不复制也就无所谓了,通过这样的方式来实现传引用。
这也能解释为什么 lambda 是个例外,因为 lambda 本身就可以看作是一个 wrapper,虽然其会发生复制,但复制一个包装类并没有什么所谓,其内部存的是引用。
在 std::thread
中也是相同的原理,新开一个线程默认情况下就是值语义,将参数复制到线程内部存储中。如果明确需要引用,需要用户显式调用 std::ref
,并自行明确生命周期。只不过此时如果传引用不用 std::ref
就会报错。
lambda 在 std::thread
中的例外也一样,就不赘述了,只不过此时生命周期的强调稍微有些弱化。如果不了解这么做的原因,无脑使用 lambda 或 std::ref
进行所谓的性能优化,有可能因生命周期的管理不当导致错误。
同样的道理,当使用一个 wrapper 进行参数传递时,wrapper 内部存的是引用,就该时刻注意该对象的生命周期,如果该对象被其管理者释放,另一线程或函数仍然进行访问,就会产生错误。