深入思考右值引用
Posted cheng-liu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入思考右值引用相关的知识,希望对你有一定的参考价值。
一般来说引用指的是左值引用,它存在的目的是为了给左值起个别名。在 C++ 新版本里面出了一个新的概念——右值引用。类比前面对左值引用的理解,右值引用是应该是对右值起的别名。不过这个所谓的右值引用已经不能用右值的方式来理解,往常对右值的认识一般以常量居多,但是这个被起别名的引用居然能做一些变量才有的操作,比如说对值本身的修改。这时候称它为变量吧,变量的诸多操作与特性它也不具备。所以说比起用右值来说,另一个更加形象的说法应该是——被阉割的变量。使用常量的这步操作我只能说是借腹生子了。
同类型的变量与阉割变量,虽然类型是相同的,但是从形式上分离了,变的可以区分了。那么这种区分是为了什么呢?是为了泛型编程,准确来说是函数重载方面的泛型编程,在往常的函数重载泛型编程中,是通过同名函数的参数类型与数量对真实调用的方法进行选择,不同类型与数量的参数意味着不同的方法。但是如果我们有这么一种需要,我们希望有着相同的数据类型与数量的同名函数也能够实现不同的调用方法(这种需求下文统称第二类函数重载),那么这时候我们就需要一个与同类型变量平行的变量来对调用方法进行区分,这个平行的变量就是我们上面提到的右值引用或者也可以称作是阉割变量。
对于以上泛型编程需要的一个熟知的例子就是移动语义。
不完美的泛型编程
通过这种被阉割的变量可以实现第二类函数重载,但是这种方法实现的第二类函数重载只有两种可能,因为只有一般变量与阉割变量这两种形式来对调用的方法进行区分,如果我们希望一个函数 `void fun(int n)` 能够实现对三种或者以上方法的重载则就没办法实现。
C++11——右值引用
目录
前言
在C++98中也有一个引用,为左值引用,但是左值引用有一些不足。而C++11的右值引用的提出弥补了左值引用的不足。
左值引用和右值引用都是别名。
说明:下面说引用都代表C++98的左值引用。
一.右值引用的概念
右值引用也是一块空间的别名。只能对右值进行引用。使用是在类型后面加两个&&。
#include<iostream>
using namespace std;
int Add(int x, int y){
return x + y;
}
int main(){
const int&& ra = 10;//右值引用
//函数返回值为一个临时变量,是右值
int&& ret = Add(2, 3);//右值引用
return 0;
}
1.1 左值和右值的概念
左值和右值是C语言的概念,但是C语言没有给出严格的标准,一般认为,可以放在等号"="左边的是左值,可以放在等号右边的是右值。但是这个说法是错误的。
比如:
#include<iostream>
using namespace std;
int main(){
//a,b是左值,10和20是右值
int a = 10;
int b = 20;
//此时b也可以放在等号右边
//a也可以放在等号右边
a = b;
b = a;
return 0;
}
左值和右值是不好区分的,这里我们一般这样认为:
- 左值:一般是可以修改的值,可以取地址的,通常是变量。
- 右值:一般是常量(除const 修饰的),表达式或者函数的传值返回(生成临时变量)。
注意:传引用返回是右值。
C++11有对右值进行了严格的区分:
- 纯右值:比如常量,表达式值a+b
- 将亡值:比如函数传值返回,表达式的中间结果。顾名思义,将亡值的空间马上就要被释放了。
1.2 引用和右值引用比较
- 引用,只能引用左值,不能引用右值。但是const引用既可以引用左值,又可以引用右值。
int main(){
//a为左值,10为右值
int a = 10;
int& ra1 = a;
const int& ra2 = a;
//int& ra3 = 10;//编译错误,10为右值
const int& ra3 = 10;
return 0;
}
- 右值引用:只能引用右值,不能引用左值。但是右值引用可以引用move之后的左值。move在后面有介绍,可以认为是改变了左值的属性,变成了右值。
int main(){
//a为左值,10为右值
int a = 10;
int&& ra1 = 10;
//int&& ra2 = a;//a为左值,编译错误
int&& ra2 = move(a);//move后就可以了
return 0;
}
二.右值引用的作用
右值引用和引用都是别名,为什么还要提出右值引用呢?
2.1引用的缺陷
#include<iostream>
using namespace std;
class String{
private:
char *_str;
public:
//构造
String(char *str = " "){
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造
String(const String& s){
cout << "String(const String& s)——拷贝构造" << endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
};
String Fun(String& s){
String ret(s);
return ret;
}
~String()
{
if(_str)
delete[] _str;
}
int main(){
String s1("左值");
String s2 = Fun(s1);
getchar();
return 0;
}
我们知道引用做参数和返回值是可以减少拷贝构造,特别是对于深拷贝的,可以提高效率。但是,当返回值是函数的局部对象时,不能引用返回,需要传值返回。
//需要传值返回
String Fun(String& s){
String ret(s);
return ret;
}
当将返回值函数返回值赋给另外一个对象s2时。 会调用String类的拷贝构造函数,进行深拷贝。
String s1("左值");
String s2 = Fun(s1);
ret在按照值返回时,必须拷贝构造一个临时对象,需要进行深拷贝。将Fun函数返回值赋值给s2时,也就是将临时对象拷贝构造s2,也需要进行深拷贝。仔细观察发现:s2和临时对象空间里的内容是相同的,而它们三个都有独立的空间,相当于创建了三个完全相同的对象。这样对于空间来说是一种浪费,程序效率也会降低。
下面来说如何优化。
2.1 移动语义
- 移动语义:将一个对象中的资源移动到另外一个对象中。
如上缺陷,我们可以这样优化:
我们知道临时变量是内容和s2的内容相同,而临时对象是一个将亡值。我们可以在临时对象拷贝构造s2时,不进行深拷贝,而是将临时对象空间的资源换给对象s2。
临时对象是将亡值,也就是右值,我们可以重载一个参数为右值引用的拷贝构造函数。我们将这个拷贝构造函数称为移动构造。
参数s右值引用的是临时变量。将临时变量的资源移动到this指针,也就是s2中。而临时对象在构造完s2后,就会被销毁。
//移动构造
String(String&& s){
_str = s._str
s._str = nullptr;
}
整个过程:
由于ret是左值,在拷贝构造临时对象时,调用的是拷贝构造函数,而临时对象是右值(将亡值),在构造s2时,会调用移动构造,将临时对象的资源换给了s2。这样减少了一次深拷贝的过程。
#include<iostream>
using namespace std;
class String{
private:
char *_str;
public:
String(char *str = " "){
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造
String(const String& s){
cout << "String(const String& s)——拷贝构造" << endl;
_str = new char(strlen(s._str) + 1);
strcpy(_str, s._str);
}
//移动构造
String(String&& s){
cout << "String(const String& s)——移动构造" << endl;
_str = s._str;
s._str = nullptr;
}
~String()
{
if (_str)
delete[] _str;
}
};
//需要传值返回
String Fun(String& s){
String ret(s);//调用拷贝构造
return ret;
}
int main(){
String s1("左值");
String s2(Fun(s1));//调用移动构造
return 0;
}
这里编译器做了优化,ret调用拷贝构造函数构造临时对象的过程省略了。
注意:
- 在移动构造函数的参数一定不能设置成const类型的右值引用,否则不能修改,资源无法转移。
- 在C++11中,编译器会为了类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理,必须显示自己的移动构造。
2.2 右值引用的具体应用
右值引用的主要应用就是重载了移动构造函数,利用了将亡值,将将亡值的空间内容交换到要拷贝的对象中。减少了深拷贝。
- 右值引用做函数的参数
由于右值引用引用的是右值(将亡值),当函数体里需要对该参数进行拷贝构造时,会调用移动拷贝构造。减少深拷贝。提高效率。
- 函数传值返回,用对象接收。
函数传值返回,返回一个临时对象,是一个将亡值。再用对象接收,临时对象拷贝构造对象。会调用移动拷贝构造函数,减少深拷贝。
2.3 对比引用总结
引用和右值引用本质的作用都是减少拷贝。右值引用弥补了引用的不足。右值引用提高了传值返回的效率。
引用:
引用做参数和返回值可以减少拷贝构造。但是,当返回的对象出了作用域就不在了,只能传值返回。
如果没有右值引用,用对象接收,会调用拷贝构造,对于string/vector等容器,需要进行深拷贝。效率低。
右值引用:
右值引用的主要应用就是重载了移动构造函数,利用了将亡值,将将亡值的空间内容交换到要拷贝的对象中。减少了深拷贝。
当函数传值返回时,用对象接收,会调用移动构造函数,对于string/vector等容器,不需要进行深拷贝,只需要将右值引用交换到构造的对象中即
右值引用做参数,函数体里有需要拷贝构造右值引用的,会调用移动构造,不会进行深拷贝。
三.右值引用引用左值(move)
按照语法,右值引用只能引用右值。但是在有些场景下,需要用到右值去引用左值来实现移动语义。
当右值引用一个左值,需要通过move函数将左值转化为右值。可以理解成将一个左值的属性改成右值返回。
int main(){
//a为左值,10为右值
int a = 10;
int&& ra1 = 10;
int&& ra2 = move(a);//move后就可以了
return 0;
}
注意:被转化的左值,其生命周期并没有随着左值的转化而改变,move并不会销毁左值。但是,move后,会改变左值的内容。如果后序还会使用到左值,要慎用move。
在STL中也有一个move函数,作用时将一个范围中的元素搬到另外一个位置。
如下:用的上面这个类。
四.完美转化
转发是:按照模板参数的类型,将参数传递给函数模板中调用的另外一个函数。
#include<iostream>
using namespace std;
void Fun(int &x){
cout << "lvalue ref" << endl;
}
void Fun(int &&x){
cout << "rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t){
Fun(t);
}
int main()
{
PerfectForward(10); //10是右值
return 0;
}
下面PerfectForward()是转化的模板函数,Func为实际目标函数。但是这里有一个问题。如下:10是右值,但是调用的确是左值引用的函数。说明在PerfectForward()模板函数转发过程中,10的右值属性丢失了。
完美转发:是目标函数总希望参数的实际类型不会因为转化函数而发生改变。就好像转化函数不存在。也就是,当函数模板在向其它函数传递自身的形参时,如果相应实参时左值,它转化的就是左值,如果相应实参是右值,它转化的就是右值。
完美转化需要通过forward函数来实现。
void Fun(int &x){
cout << "lvalue ref" << endl;
}
void Fun(int &&x){
cout << "rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t){
Fun(forward<T>(t)); //在需要转化函数的的目标函数参数调用forward函数
}
int main()
{
PerfectForward(10); // rvalue ref
system("pause");
return 0;
}
以上是关于深入思考右值引用的主要内容,如果未能解决你的问题,请参考以下文章