引言
最近看到一个多线程代码如下:
typedef unsigned long long ULL;
void accumulator_function(const std::vector<int> &v, ULL &acm,
unsigned int beginIndex, unsigned int endIndex)
{
acm = 0;
for (unsigned int i = beginIndex; i < endIndex; ++i)
{
acm += v[i];
}
}
int main()
{
ULL acm1 = 0;
ULL acm2 = 0;
std::vector<int> v = { 1,2,3,4,5,6,7,8,9,10 };
std::thread t1(accumulator_function, std::ref(v),
std::ref(acm1), 0, v.size() / 2);
std::thread t2(accumulator_function2, std::ref(v),
std::ref(acm2), v.size() / 2, v.size());
t1.join();
t2.join();
std::cout << acm1 << "+" << acm2 << "=" << acm1 + acm2 << std::endl;
return 0;
}
其中创建线程的部分使用了 std::thread t1(accumulator_function2, std::ref(v), std::ref(acm1), 0, v.size() / 2);
,对应的函数实现为void accumulator_function(const std::vector<int> &v, ULL &acm, unsigned int beginIndex, unsigned int endIndex)
,该函数的参数为一个vector引用、一个计算结果acm的引用和记录vector首末位置的index。其中的std::ref()
之前在C++ Primer中看过,感觉应该和&
差不多吧,但是既然这样为什么仍然需要用ref转换成引用形式呢?立刻把std::ref()
全部删了,重新运行,结果报错了......有点意思,可以探究一波。
探究过程
引用的例子首先列举一个:
void fun(vector<int> &a, int i)
{
a[i] = 20;
}
int main()
{
std::vector<int> v = { 1,1,1,1,1,1 };
for (auto x : v)
cout << x << ' ';
cout << endl;
fun(v, 3);
for (auto x : v)
cout << x << ' ';
}
// Output:
// 1 1 1 1 1 1
// 1 1 1 20 1 1
如果是普通引用的话,只需要调用fun(v, 3)
就行了,为什么在例子中使用了fun(std::ref(v),..)
这种形式呢?说明std::ref()
和&
是不一样的么?写个例子看一下:
#include <iostream>
#include <type_traits>
#include <functional>
using namespace std;
int main()
{
int x = 5;
std::cout << boolalpha << is_same<int&, decltype(ref(x))>::value;
return 0;
}
输出答案果然是false!那么如果std::ref()
返回的不是对象的引用,返回的是什么?查一下手册可以发现:函数模板 ref
与 cref
是生成 std::reference_wrapper 类型对象的帮助函数,它们用模板实参推导确定结果的模板实参。所以std::ref()
返回的实际上是一个reference_wrapper
而不是T&
,可以从一个指向不能拷贝的类型的对象的引用生成一个可拷贝的对象。 std::reference_wrapper
的实例是对象(它们可被复制或存储于容器),但它们能隐式转换成 T& ,故能以之为以引用接收底层类型的函数的参数。
修改一下上面的例子,看看结果:
#include <iostream>
#include <type_traits>
#include <functional>
using namespace std;
int main()
{
int x = 5;
std::cout << boolalpha << is_same<int&, decltype(ref(x).get())>::value;
return 0;
}
变为true了。reference_wrapper
与&
并不一样,但是利用get()
函数就是&
类型。但是为什么在多线程那个例子中要使用std::ref()
呢?
原因是,考虑了函数式编程(如std::bind)在使用时,是对参数直接拷贝,而不是引用。具体可以参照这一句话:std::reference_wrapper
用于按引用传递对象给 std::bind 或 std::thread 的构造函数
还是通过代码理解一下:
#include <functional>
#include <iostream>
void f(int& n1, int& n2, const int& n3)
{
std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '
';
++n1; // increments the copy of n1 stored in the function object
++n2; // increments the main()'s n2
}
int main()
{
int n1 = 1, n2 = 2, n3 = 3;
std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
n1 = 4;
n2 = 5;
n3 = 6;
std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '
';
bound_f();
std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '
';
}
得到的答案为:
Before function: 4 5 6
In function: 1 5 6
After function: 4 6 6
上述代码在执行std::bind后,在函数f()中n1的值仍然是1,n2和n3改成了修改的值。说明std::bind使用的是参数的拷贝而不是引用。C++11的设计者认为bind默认应该采用拷贝,如果使用者有需求,加上std::ref()
即可。同理std::thread
也是这样。
结论
std::ref只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推导类型时,ref能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型,但是并不能被用作&
类型。
而回到刚开始的那个多线程代码,thread的方法传递引用的时候,我们希望使用的是参数的引用,而不是浅拷贝,所以必须用ref来进行引用传递。