万能引用、完美转发及其关系

万能引用

为什么需要万能引用。首先看下面三个函数模板,分别输入左值和右值,有哪些可以通过编译。

// 左值引用
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>()函数,才能实现完美转发。

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值