一、万能引用
有的时候我们需要既能引用左值,又能引用右值的情况。并且不想使用常量左值引用(既能引用左值,又能引用右值),因为其具有常量性。这时,我们就需要实现一个万能引用即可。
请见如下2组例子
void func1(int &&m) {} //m为右值引用
template<typename T>
void func2(T &&n){} //n为万能引用
int getVal() { return 8; }
int &&x = getVal(); //x为右值引用
auto &&y = getVal(); //y为万能引用,当前引用了右值
上面代码中m和x为右值引用毋庸置疑,而非常相似的n和y都是万能引用。因为万能引用都发生了类型推导,在推导过程中,如果初始化的对象是左值,则其被推导为左值引用;初始化的对象是右值,则其被推导为右值引用。
万能引用能够实现上诉的工作,是因为应用到了引用折叠。下面简单介绍下引用折叠规则
类模板型 | T实际类型 | 最终类型 |
T& | R | R& |
T& | R& | R& |
T& | R&& | R& |
T&& | R | R&& |
T&& | R& | R& |
T&& | R&& | R&& |
从上表的总结来看,可以得出一个规律:在整个推导过程中,只要有左值引用参与进来,最后的推导结果就是左值引用。
请见如下代码
template<class T>
void func(T&& param)
{
std::cout << "T type: " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
std::cout << "param type: " << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name()
<< std::endl;
}
int main()
{
int a = 1;
std::cout << "a:\n";
func(a);
std::cout << "6:\n";
func(6);
}
输出结果
a:
T type: int&
param type: int&
6:
T type: int
param type: int&&
根据上述代码,我们可以知道
1、模板函数func传入int型左值,T被推导为int&,形参param的类型为T&&,T代入后为 int& &&,根据引用折叠规则(类模板型为T&&,T实际类型为int&,R=int,最终类型为R&,即int&),param的类型为int&,和输出结果一致;
2、传入左值引用,T被推导为int&,结果和传入左值一致,param的类型最终推导为int&;
3、传入右值,T被推导为int,根据引用折叠规则(类模板型为T&&,T实际类型为int,R=int,最终类型为R&&,即int&&),param的推导结果为int&&。
所以,上诉函数模板func,无论传入的是左值还是右值,最终形参param都是以引用形式执行func函数体,并且保留了左值和右值的属性。
二、完美转发
我们刚刚了解到了万能引用,下面我们介绍下万能引用比较重要的一个用途——实现完美转发。
template<class T>
void showType(T t) { std::cout << typeid(t).name() << std::endl; }
template<class T>
void func(T t) { showType(t); }
int main() {
std::string str = "hello c++";
normalForwarding(str);
}
上面代码中的函数 func实现的是一个转发的功能(即把传入的参数的信息转发给showType函数),虽然使用了模板可以转发各种类型的参数,但是因为其是按值传入的方式,在转发过程中会额外发生临时对象的复制,所以我们还是需要想办法寻求传入引用的方式实现转发。
我们的目标十分明确——传入引用,实现转发函数模板。
然而,我们知道如果传入了左值,我们需要使用左值引用;如果传入右值,我们应该使用右值引用。
这时,我们很容易就能想到,我们使用万能引用既可以转发左值引用,又可以转发右值引用。万能引用用在这里简直太合适了!
所以,转发可以这样实现:
首先,重载两个showType函数,可以根据重载特性区分传入参数的左值右值属性
//接收左值属性参数
template<class T>
void showType(T& param)
{
std::cout << "get l-value\n";
}
//接收右值属性参数
template<class T>
void showType(T&& param)
{
std::cout << "get r-value\n";
}
转发函数我们使用上述func模板函数,这个函数是万能引用实现的
template<class T>
void func(T&& param)
{
std::cout << "T type: " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
std::cout << "param type: " << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name()
<< std::endl;
showType(static_cast<T&&>(param)); //增加转发
}
上述代码,我们使用func将传入参数转发给了showType。
输出结果如下
a:
T type: int&
param type: int&
get l-value
6:
T type: int
param type: int&&
get r-value
需要注意的是,我们在将t转发给showType的时候使用static_cast<T&&>做了一步转化。因为C++规定:具名的右值引用是左值,不具名的右值引用是右值。无论传入的是什么类型的引用,param都是具名对象,即是左值。如果不做任何处理,即showType(param); 则a和6都会输出get l-value的结果,传入参数的左值右值属性没有得到保留。
那么,static_cast<T&&>是怎么实现保留参数属性的呢?
我们之前分析过,如果传入参数是int型左值a,则T会被推导为int&,则T&&即int& &&使用引用折叠会推导为int&,即左值引用类型的实参,传入showType会得到get l-value的结果;
如果传入参数是int型右值6,则T被推导为int,则T&&就是int&&,即右值引用类型的实参,传入showType会得到get r-value的结果。
【参考&致谢】
《现代C++语言核心特性解析》---谢丙堃