关于C++右值引用的参考文档里面有明确提到,右值引用可以延长临时变量的周期。如:

std::string&& r3 = s1 + s1; // okay: rvalue reference extends lifetime

看到这里的时候,Binfun有点崩溃,就这就能延长生命周期?这个和以下的这样的命令有啥本质的区别吗?

std::string r3 = s1 + s1

所以Binfun写了一段小代码来测试一下右值引用的延长生命周期的特性,如:

#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和右值引用

其实是有区别的,先听我解释一下RVO这个概念:返回值优化

返回值优化(Return value optimization,缩写为RVO)是C++的一项编译优化技术。即删除保持函数返回值的临时对象。这可能会省略多次复制构造函数

在调用process函数的时候竟然没有临时变量产生(可以看到构造函数只运行了一次),那应该是被RVO了。既然是编译优化技术,那么应该有编译选项关闭,RVO优化在C++里面也叫copy_elision(复制消除)优化。使用以下命令即可取消RVO:

g++ new_move.cpp -std=c++11 -fno-elide-constructors -O2 && ./a.out

编译后打印如下:

---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的光环下,只需要构造一次就已经到位了,就没必要去延长生命周期了-。-|||

std::move和右值引用的坑

另外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

参考链接

  1. https://www.cnblogs.com/tianfang/archive/2013/01/26/2878356.html
  2. https://zhuanlan.zhihu.com/p/107445960
  3. https://www.yhspy.com/2019/09/01/C-编译器优化之-RVO-与-NRVO/
  4. https://zhuanlan.zhihu.com/p/54050093
  5. http://stupefydeveloper.blogspot.com/2008/10/c-rvo-and-nrvo.html
  6. https://en.cppreference.com/w/cpp/language/reference
  7. https://zh.wikipedia.org/wiki/返回值优化
  8. https://en.wikipedia.org/wiki/Copy_elision
  9. https://zh.wikipedia.org/wiki/值_(電腦科學)
  10. 极客时间专栏《现代C++实战30讲》 03 | 右值和移动究竟解决了什么问题?

一段小代码秒懂C++右值引用和RVO(返回值优化)的误区的更多相关文章

  1. C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解

    前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇 C#进阶系列——WebApi接口传参不再困惑:传参详解  ...

  2. (转)C# WebApi 接口返回值不困惑:返回值类型详解

    原文地址:http://www.cnblogs.com/landeanfen/p/5501487.html 正文 前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi ...

  3. [转]C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解

    本文转自:http://www.cnblogs.com/landeanfen/p/5501487.html 阅读目录 一.void无返回值 二.IHttpActionResult 1.Json(T c ...

  4. JavaScript 在函数中使用Ajax获取的值作为函数的返回值

    解决:JavaScript 在函数中使用Ajax获取的值作为函数的返回值,结果无法获取到返回值 原因:ajax默认使用异步方式,要将异步改为同步方式 案例:通过区域ID,获取该区域下所有的学校 var ...

  5. asp 获取url 返回值 和 对json 返回值的处理

    Function GetHttpPage(HttpUrl,endoce) If endoce = "" Then endoce = "GB2312" If Is ...

  6. 【Java 小实验】重写(覆写 Override)返回值类型能不能相同

    背景 每次看到重写那里写着: 重写机制是指子类的方法的方法名.参数表.返回值与父类中被重写的方法都相同,而方法体不同. 而重载是: 方法名与父类中的相同,而参数表不同,则属于同名方法的重载. 本来的感 ...

  7. C++引用和函数返回值

    这是老师上课讲的内容,现在把它写下来,一方面当做复习,另一方面真的想学点东西.废话不多说,先贴上测试的代码: #include <iostream.h> float temp; float ...

  8. WebApi接口返回值不困惑:返回值类型详解

    前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.作为程序猿,我们都知道参数和返回值是编程领域不可分割的两大块,此前分享了 ...

  9. WebApi 接口返回值不困惑:返回值类型详解。IHttpActionResult、void、HttpResponseMessage、自定义类型

    首先声明,我还没有这么强大的功底,只是感觉博主写的很好,就做了一个复制,请别因为这个鄙视我,博主网址:http://www.cnblogs.com/landeanfen/p/5501487.html ...

随机推荐

  1. 关于steam平台“wallpaper engine”软件出现界面黑屏,但壁纸能播放的问题

    前阵子重装电脑后,在使用wallpaper engine这款软件时发现了以下令人疑惑的画面: 点击"设置"和"壁纸选择"界面全是黑的......这还没完,更气人 ...

  2. .net core quartz job作业调度管理组件

    定时作业对于多数系统来说,都会用到,定时作业的实现方式可以有多种方式,简单的方式用Timer就可以实现,但是达不到通用的效果,本文采用Quartz基础组件来搭建一套企业通用的作业调度管理服务,希望对于 ...

  3. 如何使用ABBYY FineReader 处理无法识别的字符?

    在识别PDF文档时,我们可能会遇到文档中存在多种语言.多种不同类型文字字符的情况.在ABBYY FineReader 15(Windows系统)OCR文字识别软件的默认语言数据下,可能无法识别PDF文 ...

  4. Vim常用按键

  5. 听说高手都用记事本写C语言代码?那你知道怎么编译运行吗?

    坊间传闻高手都喜欢用记事本写代码,那么问题来了,我们以C语言为例,如何用记事本编译运行呢?其实最简单的方式就是安装GCC编译器,在记事本编写C语言程序,然后再在命令行用GCC编译运行,下面我简单介绍一 ...

  6. TCP/IP网络协议基础入门及实验

    TCP/IP简介 提到网络协议栈结构,最著名的当属 OSI 七层模型,但是 TCP/IP 协议族的结构则稍有不同,它们之间的层次结构有如图对应关系: 可见 TCP/IP 被分为 4 层,每层承担的任务 ...

  7. 蒲公英 · JELLY技术周刊 Vol.32: 前端的自我进化之路

    蒲公英 · JELLY技术周刊 Vol.32 前端开发不过是切图仔在那儿瞎鼓捣,没啥门槛,不需要懂计算机原理,不需要懂算法,微服务.高并发这些都不需要懂,拖组件写个页面就好了,数据.逻辑.测试都可以交 ...

  8. 肝了75天,五万五千字,《Spring Boot 进阶》专栏文章整理成册,分享~

    前言 Spring Boot 这个专栏从早期的体系构建到写完,总共花费了七十五天,期间由于工作及个人原因停更了一段时间,没办法,工作实在太忙了. 很多人疑惑了,为什么源码介绍过了就结束了?高级的部分不 ...

  9. 16_Android的数据存储_ SharedPreference、XML和JSON

    1. Android读写首选项 1.1 SharedPreferences SharedPreferences 是一种轻型的数据存储方式,它的本质是基于XML文件存储Key-Value键值对数据,通常 ...

  10. 冲刺随笔——Day_Nine

    这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 团队作业第五次--Alpha冲刺 这个作业的目标 团队进行Alpha冲刺 作业正文 正文 其他参考文献 无 ...