关于C++右值引用的参考文档里面有明确提到,右值引用能够延长临时变量的周期。如:html
std::string&& r3 = s1 + s1; // okay: rvalue reference extends lifetime
看到这里的时候,Binfun有点崩溃,就这就能延长生命周期?这个和如下的这样的命令有啥本质的区别吗?c++
std::string r3 = s1 + s1
因此Binfun写了一段小代码来测试一下右值引用的延长生命周期的特性,如:ide
#include <stdio.h> #include <utility>//std::move class result { public: int val; result() { printf("constructor() [%p]\n", this); } result(result& r): val(r.val) { printf("copying from [%p] to [%p]\n", &r, this); } result(result&& r): val(r.val) { printf("moving from [%p] to [%p]\n", &r, this); } result(int i): val(i) { printf("constructor(%d) [%p]\n", val, this); } ~result() { printf("destructor() [%p]\n", this); } }; result process(int i) { printf("In process function\n"); return result(i); } int main() { printf("---step1---\n"); result s1 = process(1); printf("---step2---\n"); result &&s2 = process(2); printf("---vals:---\n"); printf("s1 addr:[%p], val:[%d]\n", &s1, s1.val); printf("s2 addr:[%p], val:[%d]\n", &s2, s2.val); }
而后Binfun自信满满地敲了编译并执行命令:函数
g++ new_move.cpp -std=c++11 -O2 && ./a.out
看到打印的时候Binfun再一次崩溃了:测试
---step1--- In process function constructor(1) [0x7ffd94c8aca0] ---step2--- In process function constructor(2) [0x7ffd94c8acb0] ---vals:--- s1 addr:[0x7ffd94c8aca0], val:[1] s2 addr:[0x7ffd94c8acb0], val:[2] destructor() [0x7ffd94c8acb0] destructor() [0x7ffd94c8aca0]
这……没有任何区别啊,C++国际标准委员会逗我玩呢?优化
实际上是有区别的,先听我解释一下RVO这个概念:返回值优化。this
返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。即删除保持函数返回值的临时对象。这可能会省略屡次复制构造函数c++11
在调用process函数的时候居然没有临时变量产生(能够看到构造函数只运行了一次),那应该是被RVO了。既然是编译优化技术,那么应该有编译选项关闭,RVO优化在C++里面也叫copy_elision(复制消除)优化。使用如下命令便可取消RVO:code
g++ new_move.cpp -std=c++11 -fno-elide-constructors -O2 && ./a.out
编译后打印以下:htm
---step1--- In process function constructor(1) [0x7ffe849b8a70] moving from [0x7ffe849b8a70] to [0x7ffe849b8ab0] destructor() [0x7ffe849b8a70] moving from [0x7ffe849b8ab0] to [0x7ffe849b8aa0] destructor() [0x7ffe849b8ab0] ---step2--- In process function constructor(2) [0x7ffe849b8a70] moving from [0x7ffe849b8a70] to [0x7ffe849b8ab0] destructor() [0x7ffe849b8a70] ---vals:--- s1 addr:[0x7ffe849b8aa0], val:[1] s2 addr:[0x7ffe849b8ab0], val:[2] destructor() [0x7ffe849b8ab0] destructor() [0x7ffe849b8aa0]
能够看到在step1中调用process函数的时候,构造产生了一个变量(地址为0x7ffe849b8a70),而后函数返回时将这个变量移动构造到了另外一个临时变量(地址为0x7ffe849b8ab0),接着赋值给s2(地址为0x7ffe849b8aa0)时再一次调用了移动构造函数。
之因此以上调用的都是移动构造,这是由于编译器识别出这些变量都是“将亡值”,也就是说编译器知道这个变量接下来都会离开它的做用域,即将会被析构掉,此时认定它是一个右值&&,因此也就调用的是移动构造函数。打印中也体现了这一点,0x7ffe849b8a70和0x7ffe849b8ab0被move constructor以后立马就被析构掉了。
而step2就不同了,咱们看到了移动构造函数只被调用了一次。而这一次0x7ffe849b8ab0并无被析构掉,这一次它被保留了下来,它的生命周期被延长了,直到main函数结束时它才会析构掉。
以上能够看到右值引用的确能够延长右值变量的生命周期。固然尽管在RVO的光环下,只须要构造一次就已经到位了,就不必去延长生命周期了-。-|||
另外Binfun进一步理解移动语义的时候,发现了一个坑,但愿你们关注一下。这个坑会致使奇怪的问题发生……也会致使很隐蔽的bug……
在说这个例子的时候,咱们先介绍一下std::move(懂的能够略过-。-)
文章最下面的参考连接里面的文章有一段写得特别棒,以下:
关于move函数内部究竟是怎么实现的,其实std::move函数并不“移动”,它仅仅进行了类型转换。下面给出一个简化版本的std::move:
template <typename T> typename remove_reference<T>::type&& move(T&& param) { using ReturnType = typename remove_reference<T>::type&&; return static_cast<ReturnType>(param); }
代码很短,可是估计很难懂。首先看一下函数的返回类型,remove_reference在头文件中,remove_reference
有一个成员type,是T去除引用后的类型,因此remove_reference ::type&&必定是右值引用,对于返回类型为右值的函数其返回值是一个右值(准确地说是xvalue)。因此,知道了std::move函数的返回值是一个右值。而后,咱们看一下函数的参数,使用的是通用引用类型(&&),意味者其能够接收左值,也能够接收右值。可是无论怎么推导,ReturnType的类型必定是右值引用,最后std::move函数只是简单地调用static_cast将参数转化为右值引用。
仍是一样的result类和一样的process函数,咱们修改一下main函数为:
int main() { printf("---step1---\n"); result &&s1 = std::move(process(1)); printf("---step2---\n"); process(2); printf("---vals:---\n"); printf("s1 addr:[%p], val:[%d]\n", &s1, s1.val); }
猜猜最后打印中s1.val的值是1仍是2?
g++ new_move.cpp -std=c++11 -O2 && ./a.out
以上的编译选项的打印以下:
---step1--- In process function constructor(1) [0x7ffdf1352db0] destructor() [0x7ffdf1352db0] ---step2--- In process function constructor(2) [0x7ffdf1352db0] destructor() [0x7ffdf1352db0] ---vals:--- s1 addr:[0x7ffdf1352db0], val:[2]
而
g++ new_move.cpp -std=c++11 -fno-elide-constructors -O2 && ./a.out
以上的编译选项,打印结果以下:
---step1--- In process function constructor(1) [0x7ffe7d59f350] moving from [0x7ffe7d59f350] to [0x7ffe7d59f380] destructor() [0x7ffe7d59f350] destructor() [0x7ffe7d59f380] ---step2--- In process function constructor(2) [0x7ffe7d59f350] moving from [0x7ffe7d59f350] to [0x7ffe7d59f380] destructor() [0x7ffe7d59f350] destructor() [0x7ffe7d59f380] ---vals:--- s1 addr:[0x7ffe7d59f380], val:[2]
很惋惜都是2,无论有没有RVO优化都是2。
Binfun的理解是,由于std::move的显式声明的关系,result &&s1 = 这种让右值生命值延长的方法失效了,最终s1指向的是已经被析构掉的右值地址(如上的0x7ffe7d59f380),而由于编译器优化等级的关系,编译器会从新回收并利用这个地址。因此在调用process(2)的时候会从新使用0x7ffe7d59f380这个地址。
若是编译选项的优化等级没那么高的话(如下把优化等级降为O1),会暂时避免这个问题:
g++ new_move.cpp -std=c++11 -O1 -fno-elide-constructors && ./a.out
打印以下:
---step1--- In process function constructor(1) [0x7ffca4276e50] moving from [0x7ffca4276e50] to [0x7ffca4276e80] destructor() [0x7ffca4276e50] destructor() [0x7ffca4276e80] ---step2--- In process function constructor(2) [0x7ffca4276e50] moving from [0x7ffca4276e50] to [0x7ffca4276e90] destructor() [0x7ffca4276e50] destructor() [0x7ffca4276e90] ---vals:--- s1 addr:[0x7ffca4276e80], val:[1]
能够看到以上0x7ffca4276e80虽然被析构了,可是没有那么快被从新利用起来,第二次使用的是0x7ffca4276e90,因此结果是正确的1。可是result &&s1 = std::move(process(1))这样的使用方式应该坚定不要用!
因此记住啊,这样能够:
result s1 = std::move(process(1)); //OK
这样也能够:
result &&s1 = process(1); //OK
这样不能够哦!:
result &&s1 = std::move(process(1)); //Error sometimes