std::bind 和 std::thread 传引用的那些事

前言

std::bindstd::thread 中可能会遇到引用失效的问题,解决办法是通过 std::ref 传递引用。这通常不是什么难的技术问题,但是知其然知其所以然,为什么这样设计,也许值得深入探讨一下。

引用失效的情况

在以下两种情况下,引用可能失效,仍然导致拷贝操作

  1. std::bind
  2. 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::bindstd::thread 中,传递的参数在什么时候被访问是不确定的,也许在该对象消亡之后,也许该对象还存在。

所以 std::bindstd::thread 就无法保证程序的正常执行,除非用户明确知道这两个类存在生命周期的问题,并加以管理。但这样显然对用户的负担太重,所以 std::bindstd::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 内部存的是引用,就该时刻注意该对象的生命周期,如果该对象被其管理者释放,另一线程或函数仍然进行访问,就会产生错误。

上一篇 下一篇

评论 | 0条评论