本文共 2630 字,大约阅读时间需要 8 分钟。
读此篇文章,你得先了解变量在寄存器中的赋值过程和生命周期。
左值的意思很简单,就是左边的值,在编程语言中,左边的值一般来说是变量
int var = 10;
此时 var 就是左值。准确的来说左值就是可以被赋值的值,也就是变量。
还是上面那段代码,在右边的就是右值,所以10就是右值,这么解释其实并不完全对。因为可能有些人会认为 这种情况也是右值:
int var = 10; int param = var;
有些人会理解成var 此时变成了右值。实际上右值很好理解,可以理解为常量,内存中实际的值。简单的来说,右值是不能被赋值的,比如 10 不能写成 10 = 11 。
说到右值,就得说右值引用,那么可以用来做什么? 看下面这个例子
int GetInt(){ return 10;} int&& var = GetInt(); return 0;
如果没有用右值引用,那么GetInt会返回int后会出现一个临时变量,临时变量用来给var赋值。但是此时使用了右值引用,那么var使用的其实就是GetInt中的10,会延长临时变量的生命周期。
我们都知道,在clone的时候,如果类中有指针成员,那么在做拷贝的时候通常会进行深拷贝,像这种:
class Parent { private: Son* son;public: ~Parent() { delete son; } Parent() :son(new Son()) { }; Parent(const Parent& parent) :son(new Son(parent.son)) { }; //深拷贝};
这么做没有什么不对,但是此时,你不想让用户了解这个类具体的构造过程,所以你写了个Factory,像这样:
Parent GetParent(){ return Parent(); //返回一个右值} //用户使用Parent p = GetParent();
这种情况下,资源会被浪费到什么地步?关闭编译器优化我们理一下过程
- GetParent中会构造一次Parent
- GetParent在返回时会进行析构,此时还没有析构
- 因为析构会导致无法赋值,所以在GetParent()会产生一个临时对象,所以又调用了一次拷贝构造
- GetParent返回析构。
- Parent p = 这里又调用一次对临时对象的拷贝构造
- 临时对象析构. 总的来说 进行了1次构造,2次深度拷贝,2次析构,这里更不用说深拷贝中son的拷贝同样是2次,如果Son的资源占用很大,那么这种浪费是很可怕的。
但是我就想用Factory,不想让用户知道这个类具体的构造过程,怎么办?了解一下移动构造。
在上面深拷贝造成的资源浪费后,我们想到了一种办法,使用右值引用来调用GetParent;
Parent&& p = GetParent();
现在的过程就会是这样:
- GetParent 中触发构造
- 由于延长了右值生命周期,不会出现临时对象,Parent&& p = 只会触发一次拷贝构造
- GetParent 中的对象析构
现在只会触发一次构造、一次深拷贝、一次析构。这里要说一下常量左值这个通用
引用类型。,universal references
既可以接受左值也可以接收右值,const Parent& p = GetParent(); 是相同的效果。
还有没有办法优化呢?
因为我们使用右值引用后,GetParent中的对象被延长生命周期,可以让我们只进行一次深拷贝
但是GetParent中的对象,析构还是会触发son的delete,事实上GetParent中的对象我们根本用不上,延长生命周期也只是为了减少深拷贝次数,能不能省去它成员(son)的delete和深拷贝中的new呢?
于是 我们优化一下Parent这个类:
class Parent{ private: Son *son;public: ~Parent(){ if (son!=nullptr) delete son;} Parent() :son(new Son()) { }; Parent(const Parent& parent) :son(new Son(parent.son)) { }; //深拷贝 //如果是右值引用,我们直接将son指针赋值,不进行深拷贝,只是浅拷贝,并且将原来的son的指针置空 Parent(Parent&& parent):son(parent.son) { parent.son = nullptr; } //或者这么写 //Parent(Parent&& parent) { std::swap(son, parent.son); }}
使用:
Parent p = GetParent();
按照惯例,还是写一下过程:
- GetParent 触发构造,然后返回右值,因为析构后会无法赋值,所以出现一个临时变量
- 临时变量 会触发一次移动构造
- GetParent 中的对象析构
- Parent p = 又触发一次移动构造
- 临时对象析构 总的来说 一次构造,2次移动构造,2次析构
如果想更节约 可以 const Parent& p = GetParent();
移动构造会让我们省去成员中指针的堆创建和释放,极大的提高了性能。事实上目前的编译器都会进行优化,所以上面的例子其实只会进行一次构造,连拷贝构造都用不上
写这个原因是让大家清楚右值引用的作用,既然编译器会优化那么就毫无用处了吗?并不是,可以这么用,下面的例子在编译器优化后是这样的:
Parent temp = GetParent(); //只进行一次构造函数 //todo something //move其实是将左值转为右值,move只是表明语义,进行移动构造。 Parent p = std::move(temp); //移动构造
如果是T&
这既可以是左值
也可以是右值
,因为这个T
是不确定
的,const Class&
(universal references
)也同样既可以是左值
也可以是右值
,因为const
语义本身就是不可变
,一个不可变的左值
也可以是右值
。
转载地址:http://ixlsi.baihongyu.com/