这几天在复习以前的内容后发现C++有个右值引用之前一直没有了解过,它的好处是可以让运行速度变快。

怎么区分左值和右值?我的理解是带变量名,可以取地址的为左值。纯数值,不能取地址的为纯右值,在操作后就会消失的为将亡值,纯右值和将亡值统称为右值

传统引用也被称为左值引用,是绑定对象的操作,对左值进行引用,它的特点如下:

  • 必须被初始化并且无法解绑
  • 绑定后等于给原本对象取别名,所有的操作都会指向被绑对象
  • 左值引用不能指向临时变量,除了const &a = 1这种形式,因为此时是给1新开了一个临时的左值,这种操作方法称为常量左值引用

右值引用是C11引入的,是对右值进行的引用,特点如下:

  • 只能绑定在将被销毁的变量上
  • 可以理解为值的传递,原本的值会在传递后被删除
  • 右值引用不能指向一个左值,但是可以用&&a = move(b),即std::move()函数让左值传递右值,但是要尽量保证被传递的值不要再被使用了,这个函数的作用是能将一个左值转换成一个右值

那么为什么要有右值引用,原本的赋值不能满足吗?先说一下移动语义:

移动语义是指只进行移动操作的转换。传统的赋值是先创造要传递值的副本,然后赋给另一个对象,最后删除原先对象保存的值。移动语义就简单很多,将该值直接传递给另外一个对象。

可以直观的看出,右值引用理论上要比赋值快很多,那么试验一下:

#include <iostream>
#include <thread>


void print_1(int i)
{
    printf("这是普通的赋值函数,i为%d,",i);
}
void print_2(int&& i)
{
    printf("这是右值引用的函数,i为%d",i);
}

int main()
{
    double Start = clock();
    print_1(5);
    double End_1 = clock();
    printf("耗时%lf ms\n",((End_1 - Start)/CLOCKS_PER_SEC)*1000);
    print_2(5);
    double End_2 = clock();
    printf("耗时%lf ms\n",((End_2 - End_1)/CLOCKS_PER_SEC)*1000);
}

程序结果为:

这是普通的赋值函数,i为5,耗时0.019000ms
这是右值引用的函数,i为5耗时0.013000ms

可以发现,右值引用要比赋值函数快太多了,函数越复杂,效果越明显。

在说完美转发之前不能不说的是万能引用,万能引用即可以接受左值也可以接受右值的一种引用方法,下面实例说明一下:

#include <iostream>
#include <thread>


template <typename T>
void JustOutput(T&& Value)
{
    std::cout<<Value<<std::endl;
}
int main()
{
    int a = 5;
    double Start = clock();
    JustOutput(a);
    double End_1 = clock();
    printf("耗时%lfms\n",((End_1 - Start)/CLOCKS_PER_SEC)*1000);
    JustOutput(5);
    double End_2 = clock();
    printf("耗时%lfms\n",((End_2 - End_1)/CLOCKS_PER_SEC)*1000);
}

我们很高兴的发现两种传值都能够被接受,并且右值传递还是更快一点。不过仔细一看,这个万能引用为什么和右值引用长的这么像?怎么区分?

template <typename T> void JustOutput(T&& Value){}
void JustOutput(int&& Value){}

比较一下可以发现只有在出现类型判断的时候才会成为万能引用,也就是说,万能引用与模板息息相关。

那么我们是不是可以认为能够通过这个函数来判断这个是左值还是右值能?加两个函数进行说明:

#include <iostream>
#include <thread>

void Judgement(int& Value)
{
    printf("这是个左值\n");
}
void Judgement(int&& Value)
{
    printf("这是个右值\n");
}
template <typename T>
void JustOutput(T&& Value)
{
    Judgement(Value);
}
int main()
{
    int a = 5;
    double Start = clock();
    JustOutput(a);
    double End_1 = clock();
    printf("耗时%lfms\n",((End_1 - Start)/CLOCKS_PER_SEC)*1000);
    JustOutput(5);
    double End_2 = clock();
    printf("耗时%lfms\n",((End_2 - End_1)/CLOCKS_PER_SEC)*1000);
}

可以发现虽然“理论上应该是”右值引用的速度比“理论上本来就是”左值引用的速度要快很多,但是程序还是返回了他们都是左值引用的结果,这个不难理解,这个右值传递到JustOutput里,就有了一个变量名,Value,将Value传递给下一层函数,那么就一定是个左值,如果想要在不改变属性的情况下传递这个值,那么就需要用到完美转发,非常简单:

Judgement(std::forward<T>(Value));

如上,我们只需要改一行代码,就能改变输出结果,我们看看forward的源代码:

inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
_Tp&&
forward(typename remove_reference<_Tp>::type& __t) _NOEXCEPT
{
    return static_cast<_Tp&&>(__t);
}

可以发现这个代码就做了一件事情,强制转换,所以我们需要先了解一下引用折叠,引用折叠不难理解,就是&+任何值等于&,&&加&&等于&&。为什么要说这个,可以看到这个源码传入的_Tp是函数调用时的T的类型,如果是右值引用,return时和&&进行折叠为&&,如果为左值引用就和&&折叠为&,如果为纯右值,则不进行折叠,直接输出为&&。

那么完美转发是不是一定要有万能引用呢?我的理解为是的,因为如果没有万能引用就不会发生类型判断,也就只有重载,没有完美转发的事情了。