您的位置:首页 > 编程语言 > C语言/C++

C++右值引用

2016-04-09 17:07 309 查看

需求

通读完了C++Primer,有很多问题困扰着我。这其中,除了万年老大难的指针外,还有一个被多次提到的知识点:左值引用和右值引用。

虽然在C++98中左值引用和右值引用对程序员来说处于透明状态,基本无需太过操心,但在C++11的新特性中,它们的区别却不得不引起我们的注意。

是什么

左值和右值

在谈左值引用和右值引用之前,我们先来看看另外两个与之紧密相关的知识点:左值和右值。

网上这一篇讲得不错:http://book.2cto.com/201306/25366.html

当一个对象被用作右值的时候,用的是对象的值;当对象被用作左值的时候,用的是对象的身份。

一个重要的原则是:在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的:

赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。

取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。

内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。

内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。

如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。举个例子,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(p)的结果是int&。另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int*,也就是说,结果是一个指向整形指针的指针。

一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”。比如:

a = b + c;


在这个赋值表达式中,a就是一个左值,而b + c则是一个右值。这种识别左值、右值的方法在C++中依然有效。不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b + c)这样的操作则不会通过编译。因此a是一个左值,(b + c)是一个右值。

这些判别方法通常都非常有效。更为细致地,在C++11中,右值是由两个概念构成的,一个是将亡值(xvalue,eXpiring Value),另一个则是纯右值(prvalue,Pure Rvalue)。

其中纯右值就是C++98标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关联的值。比如非引用返回的函数返回的临时变量值(我们在前面多次提到了)就是一个纯右值。一些运算表达式,比如1 + 3产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如:2、‘c’、true,也是纯右值。此外,类型转换函数的返回值、lambda表达式(见7.3节)等,也都是右值。

而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值(稍后解释),或者转换为T&&的类型转换函数的返回值(稍后解释)。而剩余的,可以标识函数、对象的值都属于左值。在C++11的程序中,所有的值必属于左值、将亡值、纯右值三者之一。

左值引用和右值引用

所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值的引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。

一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

int i=42;
int &r=i;               //正确:r引用i
int &&rr=i;         //错误:不能将一个右值引用绑定到一个左值上
int &r2=i*42;           //错误:i*42是一个右值
const int &r3=i*42      //正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2=i*42      //正确:将rr2绑定到乘法结果上


返回左值引用的函数,联通赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

返回非引用类型的函数,连同算数、关系、位以及后置递增/递减运算符,都生成右值。

左值持久;右值短暂

左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

由于右值引用智能绑定到临时对象,我们得知:

所引用的对象将要被销毁

该对象没有其他用户

我们不能将一个右值引用绑定到一个右值引用类型的变量上:

int &&rr1=42;       //正确:字面常量是右值
int &&rr2=rr1;      //错误:表达式rr1是左值!


怎么用

看起来右值的这些特性并没有什么太大的作用,但是C++中既然增加了右值,还专门增加了一个move方法用来将右值转换成左值,就一定有它的道理:右值是即将消亡的值,转换成左值以后可以将右值的生命延长,利用好这个特性我们可以在某些情况下避免内存的分配和删除,从而提升性能。

关于右值引用,这一篇讲的不错:http://wangzhe3224.github.io/c++/2015/08/28/lvalue-and-rvalue.html

核心思想是通过对更改右值所指向的内存的所有权来延长一个即将消亡的数据(右值)的生命,从而达到提升性能的效果,通常的应用在移动构造函数和移动赋值语句中,这一部分不详述,下面以一个简单的例子看在普通方法中如何使用:

使用利用std::move延长右值的生命

简单的类

struct Temp{
Temp(){ t = 0; cout << "new Temp 0" << endl; }
Temp(int i):t(i){cout << "new Temp 1" << endl; }
Temp(const Temp& temp) :t(temp.t){ cout << "Temp拷贝构造函数" << endl; }
Temp(Temp&& temp) :t(temp.t){ cout << "Temp移动构造函数" << endl; }
~Temp(){ cout << "delete Temp" << endl; }
int t;
};
struct A{
A() :temp(1){ cout << "new A" << endl; }
A(const A& o) :temp(o.temp){ cout << "A拷贝构造函数" << endl; }
A(A&& o) :temp(std::move(o.temp)){ cout << "A移动构造函数" << endl; }
~A(){ cout << "delete A" << endl; }
Temp temp;
};


自定义类型Temp和A都定义了拷贝构造函数和移动构造函数,在堆这两个类的初始化操作中,如果使用拷贝构造函数将会创造两份数据造成浪费,如果使用移动构造函数则仅仅创造一份数据,性能会有所提升(关于移动构造函数网上很多,不详述)。

方法一:

A func1(A a){
cout << "11111" << endl;
a.temp.t++;
return a;
}


使用:

A a;
cout << "————b————" << endl;
A b=func1(a);
b.temp.t++;
cout << "b输出:" << b.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;


结果:



从结果可以看出来,方法A通过直接传入传入实参的方式来实现,整个过程A的构造函数被调用了两次,一次是拷贝构造函数(向方法传入值时),另一次是移动构造函数(方法中的临时量通过return返回值时)。

方法二:

你也许会说我直接传左值引用不也可以不调用拷贝构造函数,不但不调用拷贝构造函数,连构造函数都不调用:

A& func2(A& a){
cout << "22222" << endl;
return a;
}


使用:

cout << "————c————" << endl;
A& c = func2(a);
c.temp.t++;
cout << "c输出:" << c.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;


结果:



确实,通过引用传递,我们不但没有调用构造函数,连析构函数也没有调用,但是修改了返回值以后,返回值所引用的原始值也随着发生了变化,有时候这是我们不想看到的结果。这个时候就需要移动构造函数来让整个过程仅仅复制一份数据,从而达到原数据与拷贝数据分离,又不会因为调用拷贝构造函数造成性能的浪费。

方法三:

回过头来看方法一,如果A和Temp没有移动构造函数,将会造成两次调用拷贝构造函数(注释掉A中的移动构造函数即可测试得到)。从这里可以得出两点:

向实参传递实参的时候优先调用拷贝构造函数

返回值优先调用移动构造函数

通过这两点我们可以同样得出两个结论:

返回值如果定义了移动构造函数将会节省一次拷贝开销

传入值如果能够让他调用拷贝构造函数则也能节省一次拷贝开销。

虽然有了这两个结论,但是目前仍然不知道如何在传入实参的时候调用移动构造函数。看来我们得弄懂上述两个构造过程调用不同构造函数的原因。

上面推荐的那篇关于右值引用的文章中也有一个调用拷贝构造函数的例子,这个例子中向一个类构造函数传入一个左值,结果造成了调用拷贝构造函数,而本例子中的a实例明显是个左值,所以会调用拷贝构造函数。另一方面,由于func1中的a是个即将消亡的临时值(作用域结束后会自动销毁),所以是个右值,return用一个右值来初始化一个返回值自然会调用移动构造函数了。

实际上还是调用构造函数的知识,只不过隐藏在方法调用过程里面。过程分析有点绕,实际上使用起来并不会太复杂,只需要在传值的时候使用标准库的move函数对参数进行处理即可:

使用:

A d = func1(std::move(a));
d.temp.t++;
cout << "d输出:" << d.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;


结果:



由于方法二的运行,a中temp的t的值变为了2,。

从结果可以看到两次调用都是移动构造函数提升了构造函数性能,并且返回值和原始值也实现了分离。

完美转发

使用右值引用的另一个作用是进行完美转发,在上面的链接里有过分析,这一篇有详细的分析:http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html

在此不详述

转载请注明出处:http://blog.csdn.net/ylbs110/article/details/51106164

全部代码

#include <iostream>

using namespace std;
struct Temp{ Temp(){ t = 0; cout << "new Temp 0" << endl; } Temp(int i):t(i){cout << "new Temp 1" << endl; } Temp(const Temp& temp) :t(temp.t){ cout << "Temp拷贝构造函数" << endl; } Temp(Temp&& temp) :t(temp.t){ cout << "Temp移动构造函数" << endl; } ~Temp(){ cout << "delete Temp" << endl; } int t; }; struct A{ A() :temp(1){ cout << "new A" << endl; } A(const A& o) :temp(o.temp){ cout << "A拷贝构造函数" << endl; } A(A&& o) :temp(std::move(o.temp)){ cout << "A移动构造函数" << endl; } ~A(){ cout << "delete A" << endl; } Temp temp; };

A func1(A a){
cout << "11111" << endl;
return a;
}
A& func2(A& a){ cout << "22222" << endl; return a; }

int _tmain(int argc, _TCHAR* argv[])
{
A a;
cout << "————b————" << endl;
A b=func1(a);
b.temp.t++;
cout << "b输出:" << b.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;
cout << "————c————" << endl;
A& c = func2(a);
c.temp.t++;
cout << "c输出:" << c.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;
cout << "————d————" << endl;
A d = func1(std::move(a));
d.temp.t++;
cout << "d输出:" << d.temp.t << endl;
cout << "a输出:" << a.temp.t << endl;

system("pause");
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: