万能引用
为什么需要万能引用。首先看下面三个函数模板,分别输入左值和右值,有哪些可以通过编译。
// 左值引用
template <typename T>
void fun(T& x){return ;}
// 常量引用
template <typename T>
void fun1(const T& x){return ;}
// 万能引用
template <typename T>
void fun2(T&& x){return ;}
int main()
{
int x = 1;
fun(1); // 错误,不接受右值
fun(x); // 正确,接受左值
fun1(1); // 正确,接受右值
fun1(x); // 正确,接受左值
fun2(1); // 正确,接受右值
fun2(x); // 正确,接受左值
return 0;
}
可以得出以下结论:
- 左值引用只能接受左值,不能接受右值;
- 常量引用,既可以接受左值,又能接受右值。但是无法对引用对象进行修改。
- 万能引用既可以接受左值,又能接受右值;可以对引用对象进行修改。
由此可以看出万能引用的灵活性。
那么万能引用推导出来的T是什么类型呢,请看以下代码。
#include <iostream>
using namespace std;
// 用于打印T的类型
template <typename T>
std::string type_name()
{
typedef typename std::remove_reference<T>::type TR;
std::string r = typeid(TR).name();
if (std::is_const<TR>::value)
r += " const";
if (std::is_volatile<TR>::value)
r += " volatile";
if (std::is_lvalue_reference<T>::value)
r += "&";
else if (std::is_rvalue_reference<T>::value)
r += "&&";
return r;
}
// 万能引用,打印推导结果T
template <typename T>
void printType(T&& x)
{
cout << "Type of T is: " << type_name<T>() << endl;
}
int main()
{
cout << "------Test the type_name funciton------" << endl;
cout << "Type is: " << type_name<int>() << endl;
cout << "Type is: " << type_name<int&>() << endl;
cout << "Type is: " << type_name<int&&>() << endl;
int a = 1;
int &lra = a;
int &&rra = 1;
cout << "------Input is r_value------" << endl;
printType(1);
printType(move(a));
cout << "------Input is l_value------" << endl;
printType(a);
printType(lra);
printType(rra);
return 0;
}
输出如下:
------Test the type_name funciton------
Type is: i
Type is: i&
Type is: i&&
------Input is r_value------
Type of T is: i
Type of T is: i
------Input is l_value------
Type of T is: i&
Type of T is: i&
Type of T is: i&
第一部分说明type_name()这个函数能够正确打印出T的类型。
第二部分说明输入右值时,T被推导为原始类型,即T是int;这样printType(T&& x) 就变成了printType(int&& x),即x是右值引用。
第三部分说明输入左值时,T被推导为左值引用,即T是int&;这样printType(T&& x) 就变成了printType(int&&& x),折叠之后就是printType(int& x),即x是左值引用。
但需要注意的是,无论输入是左值还是右值,由于所输入内容已经绑定到了x上,也就是有了名字,因此x本身已经变成了左值。
重新阐述重点:万能引用中,输入右值,T被推导为不带引用的数据类型,如int;输入左值,T被推导为左值引用,如int&。
move函数的实现
在聊完美转发之前,先稍微介绍以下move函数的实现。
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_case<typename remove_reference<T>::type&&>(t);
}
可见这就是一个万能引用,根据上一部分得出的规则,即当输入左值时,T被推导为左值引用类型(如int&),当输入右值,T被推导为不带引用的类型(如int)。
而typename remove_reference<T>::type 的作用是返回T去除引用后的类型,即无论输入左值还是右值,也即无论T被推导为是否带引用,该表达式得到的都是不带引用的数据类型。
从而move函数等价于(以int为例):
//将typename remove_reference<T>::type替换为int
template <typename T>
int&& move(T&& t)
{
return static_case<int&&>(t);
}
也就是说无论输入左值还是右值,都将其转换为右值,这就是move函数的作用。为什么需要move函数呢,因为只有输入右值时,才会触发移动构造函数和移动赋值函数,所以就有了将左值变成右值的需求,而move函数就是干这个的。
完美转发及其实现
什么是转发:一个对象,传进第一个函数后,第一个函数又将其传入第二个函数进行处理,这就是对对象的转发。
什么是完美转发:传入第一个函数的是左值,那么被转发至第二个函数的也依然是左值;同理,传入第一个函数的是右值时,被转发至第二个函数的也仍然是右值。
完美转发必须依赖万能引用:假如没有万能引用,那么右值都无法传进第一个函数,更别提转发了。而且完美转发依赖于万能引用对T的推导。
如果实现完美转发:
// fun2是第二个函数
template<typename T>
void fun2(T& x){
std::cout << "Lvalue ref" << std::endl;
}
template<typename T>
void fun2(T&& x){
std::cout << "Rvalue ref" << std::endl;
}
// 万能引用,fun1这是第一个函数
template <typename T>
void fun1(T&& x)
{
fun2(x);
fun2(move(x));
fun2(forward<T>(x));
}
int main()
{
int x = 1;
cout <<"------Input Rvalue------"<<endl;
fun1(1);
cout <<"------Input Lvalue------"<<endl;
fun1(x);
return 0;
}
/* output:
------Input Rvalue------
Lvalue ref
Rvalue ref
Rvalue ref
------Input Lvalue------
Lvalue ref
Rvalue ref
Lvalue ref
*/
上面尝试了三种方法:
第一种直接传递x。之前提到过,无论输入是左值还是右值,由于所输入内容已经绑定到了x上,也就有了名字,因此x本身已经变成了左值。所以当第一个函数传入右值时,直接传递的是左值,不是完美转发。
第二种是直接转递move(x)。然后此种情况下,当输入左值时,传递给第二个函数的变成了右值,不是完美转发。
第三种是使用forward<T>(x)函数,这是可以实现完美转发的。forward<T>()的原型有两个重载版本:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
这里的T是在调用过程中显式指定的,又根据之前对typename std::remove_reference<T>::type的描述,无论T是啥,我们直接将其等价为无引用的原始数据类型,如int。
假如第一个函数输入的是右值,那么T就被推导为int,那么调用的就是forward<int>(x),对T和typename std::remove_reference<T>::type进行相应替换后,上面两个函数变成了下面的样子。又因为转发过程中,我们输入的x一定是左值,所以此时调用的是第一个函数,将输入强制转换为int&&并返回,即返回的仍然是右值。
// 将T替换为int
// 将typename std::remove_reference<T>::type替换为int
int&& forward(int& param)
{
return static_cast<int&&>(param);
}
int&& forward(int&& param)
{
return static_cast<int&&>(param);
}
假如第一个函数输入的是左值,那么T就被推导为int&,那么调用的就是forward<int&>(x)。同理可以得到函数的真实样子。x仍然一定是左值,还是调用第一个函数,将输入强制转换为int&&&,也即int&并返回,即返回的左值。
// 将T替换为int&
// 将typename std::remove_reference<T>::type替换为int
int&&& forward(int& param)
{
return static_cast<int&&&>(param);
}
int&&& forward(int&& param)
{
return static_cast<int&&&>(param);
}
综上,对于forward<T>(),其本质是根据T的推导对数据进行强制类型转换。
而完美转发的思路总结下来就是:
输入左值,万能引用中T推导为int&,则调用forward<int&>()把输入转换成了左值。
输入右值,万能引用中T推导为int,则调用forward<int>()把输入转换了右值。
这也就是为什么说完美转发需要依赖万能引用对T的推导的原因,只有结合万能引用和forward<T>()函数,才能实现完美转发。