右值引用不过是C++的一种新语法,重要的是基于右值引用引申处理的两种C++编程技巧:移动语义和完美转发

右值引用

C++98/03标准中就有引用,用&表示。但是此种引用方式有一个缺陷,即正常情况下只能操作C++中的左值,无法对右值添加引用。举个例子:

int num = 10;int &b = num; //正确int &c = 10; //错误

如上所示,编辑器允许我们为num左值建立一个引用,但是不可以为10这个右值建立引用。因此,C++93/03标准中的引用又叫做“左值引用

注意,虽然C++98/03标准不支持右值建立非常量左值引用,但是允许常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值。例如:

int num = 10;const int &b = num;const int &c = 10;

我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的

为此,C++11标准引入了另一种引用方式,称为右值引用,用&&表示

C++标准委员会在选定右值引用符号时,既希望能选用现有 C++ 内部已有的符号,还不能与 C++ 98 /03 标准产生冲突,因此最终选定了 2 个 ‘&’ 表示右值引用。

需要注意的是,和声明左值引用一样,右值引用也必须立即进行初始化操作,而且只能使用右值进行初始化,比如:

int num = 10;//int && a = num; //右值引用不能初始化为左值int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。比如:

int && a = 10;a = 100;cout << a << endl;

程序输出结果为 100。

另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如:

const int&& a = 10;//编译器不会报错

但这种定义出来的右值引用并无实际用处。
一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。

引入右值引用的原因除了我们可能需要对右值进行修改,还有另外一个原因,我们来看一个例子:

std::vector<int>foo(){std::vector<int> temp = {1, 2, 3, 4};return temp;}std::vector<int> v = foo();

在这样的代码中,就传统的理解而言,函数foo的返回值temp在内部创建然后被赋值给v,然而v获得这个对象时,会将整个temp拷贝一份,然后把temp销毁。如果这个temp非常大,这将造成大量额外的开销(这也是传统C++一直无限的墨镜的问题)。在最后一行中,v是左值,foor()返回的值就是右值(也就是纯右值),但是,v可以被别的变量俘获到,而foo()尝试的那个返回值作为一个临时值,一旦被v复制后,将立即被销毁,无法获取,也不能修改。而将亡值就定义了这一行为:临时的值能够被识别、同时也能够被移动

将亡值,是C++11为了引入右值引用而提出的概念(因此传统C++中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值

从C++11起,编译器为我们做了一些工作,此处的temp会被进行此隐式右值转换,等价于static_cast<std::vector<int>&&>(temp),进而此处的v会将foo局部返回的值进行移动(移动语义

要拿到一个将亡值,就需要用到右值引用:T&&,其中T是类型右值引用的声明让这个临时值的生命周期得以延长,只要变量还活着,那么将网址将继续存活

C++11提供了std::move这个方法将左值参数无条件的转换为右值,有了它我们就可以方便的获得一个右值临时对象

rv2虽然引用了一个右值,但由于它是一个引用,所以rv2依旧是一个左值

注意,这里有一个历史遗留问题:

int &a = std::move(1); //不合法,非常量左引用无法引用右值 const int &b = std::move(1); // 合法,常量做引用允许引用右值

学到这里,一些读者可能无法记清楚左值引用和右值引用各自可以引用左值还是右值,这里给大家一张表格,方便大家记忆:

其实,C++11 标准中对右值做了更细致的划分,分别称为纯右值(Pure value,简称 pvalue)和将亡值(eXpiring value,简称 xvalue )。其中纯右值就是 C++98/03 标准中的右值(本节中已经做了大篇幅的讲解),而将亡值则指的是和右值引用相关的表达式(比如某函数返回的 T && 类型的表达式)。对于纯右值和将亡值,都属于右值,读者知道即可,不必深究。

使用右值引用接管数据 const引用

在C++语言中,引用是作为一种高效、安全的传递数据的方式而存在的。除了一般的引用类型,还可以声明const引用

我们有以下一个Image类。

{public: Image(int w, int h) :width(w), height(h) { data = new char[getSize()]; } int getSize(){ return width * height; } virtual ~Image(){ if(data != nullptr){ delete data; data = nullptr; width = 0; height = 0; } }private: int width = 0; int height = 0; char* data = nullptr;}

上面只是这个类的雏形,只有析构函数、构造函数和取得数据大小的功能。

接下来编写比较两个Image是否相同的函数。最简单的形式大致如下:

bool isSame(Image& img){ if(width == img.width && height == img.height){ return (memcmp(data,img.data,getSize())==0); } else{ return false; }}

这里使用引用类型的参数,避免了没有必要的拷贝动作。当然我们还可以做的更好:由于比较函数没有必要也不应该对比较对象的内容进行修改,所以还可以用下面的形式进行承诺:

bool isSame(const Image& img){ if(width == img.width && height == img.height){ char* in = static_cast<char*>(img.data); return (memcmp(data,in,getSize())==0); } else{ return false; }}

通过在参数前面增加const修饰符,向isSame方法的调用者保证,不会修改img的内容

右值引用

继续添加将一个Image的一部分merge到另一个Image上的方法。函数的内容大致如下(这里忽略处理的细节):

void merge(Image& img){ //接管img中的数据。 img.height = 0; img.width = 0; img.data = nullptr;}

类似的操作在处理在输入对象时一般有两种处理方式。有时希望只是参照而不破坏输入数据,这时可以使用前面讲到的为参数增加const修饰符的方式来承诺;有时为了提高效率或者其他的原因希望可以接管输入的数据,就像上面代码的状态。这时的行为更像是数据移动。

对于第二种方式,如果仅仅定义一般的引用类型,利用者根本没有办法声明来确定这个操作是否会接管参数中的数据,这种不确定性会造成恨到的麻烦。

这个时候就可以使用右值引用

void merge(Image&& img){ //接管img中的数据。 img.height = 0; img.width = 0; img.data = nullptr;}

我们将参数声明为右值引用,要求像一个临时变量一样的使用数据。使用这个函数的方法如下:

Image img1(100, 100);Image img2(100, 200);img1.merge(std::move(img2));

注意代码中的std::move,这是标准库中提供的方法,它可以将左值显式转换为右值引用类型,从而告诉编译器,可以向右值(临时变量)一样处理它。时也意味着接下来除了对img2赋值或销毁以外,不再使用它。

C++11通过使用右值引用提供了一种接管数据的方法

总结

如果说使用const修饰符可以对外承诺不对参数进行修改的话,那么使用右值引用就是对外要求接管参数数据的权力