*以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「ENG八戒」https://mp.weixin.qq.com/s/Xd_FwT8E8Yx9Vnb64h6C8w

带给现代 C++ 性能飞跃的特性很多,今天一边聊技术,一边送福利!


过去写 C/C++ 代码,大家对数据做传递时,都习惯先拷贝再赋值。比如,把数据从 t1 复制到 t2,复制完成后 t2 和 t1 的状态是一致的,t1 状态没变。这里的状态指的是对象内部的非静态成员数据集合。

在程序运行过程中,复制过程既要分配空间又要拷贝内容,对于空间和时间都是种损耗。复制操作,无疑是一门很大的开销,何况经常触发资源复制的时候。

来看看普通的函数返回值到底有哪些开销,

std::string getString()
{
std::string s;
// ... return s;
} int main()
{
std::string str = getString();
// ...
}

假设你的编译器还不支持 C++ 11,那么,在 main() 函数里调用 getString() 时,需要在调用栈里分配临时对象用于复制 getString() 的返回值 s,复制完成调用 s 的析构函数释放对象。然后,再调用 std::string 类的复制赋值运算符函数将临时对象复制到 str,同时调用临时对象的析构函数执行释放。

那么,有没有技巧可以实现上面示例代码同样的效果,同时避免复制?

有的,就是接下来重点介绍的移动(和中国移动无关)。

相对于复制,移动无须重新分配空间和拷贝内容,只需把源对象的数据重新分配给目标对象即可。移动后目标对象状态与移动前的源对象状态一致,但是移动后源对象状态被清空。

实际上,大部份的情况下,数据仅仅需要移动即可,拷贝复制显得多余。就像,你从图书馆借书,把自己手机的 SIM 卡拔出来再插到其它手机上,去商店买东西你的钱从口袋移动到收银柜等等。

那么,是不是可以对所有的数据都执行移动?

答案是否定的。在现代 C++ 中,只有右值可以被移动。

左右值概念

在 C++ 11 之前,左右值的划分比较简单,只有左值和右值两种。

但是从 C++ 11 开始,重新把值类别划分成了五种,左值(lvalue, left value),将亡值(xvalue, expiring value),纯右值(prvalue, pure right value),泛左值(glvalue, generalized left value),右值(rvalue, right value)。不过后边的两种 glvalue 和 rvalue 是基于前面的三种组合而成。从集合概念来看,glvalue 包含 lvalue 和 xvalue,rvalue 包含 xvalue 和 prvalue。

左右值划分的依据是:具名和可被移动。

具名,简单点理解就是寻址。可被移动,允许对量的内部资源移动到其它位置,并且保持量自身是有效的,但是状态不确定。

  • lvalue:具名且不可移动
  • xvalue:具名且可移动
  • prvalue:不具名且可移动

那么,可以看到泛左值(glvalue)其实就是具名的量,右值就是可移动的量。

以往在往函数传参的时候,经常有用到值引用的模式,形式如下:

function(T& obj)

T 是类型,obj 是参数。

到了现代 C++,原来的值引用就变成了左值引用,另外还出现了右值引用,形式如下:

function(T&& obj)

那么 C++ 11 是怎样实现移动操作的呢?

实现移动操作

移动操作依赖于类内部特殊成员函数的执行,但前提是该对象是可移动的。如果恰好对象是左值(lvalue)呢?

C++ 11 的标准库就提供了 std::move() 实现左右值转换操作。std::move() 用于将表达式从 lvalue(左值) 转换成 xvalue(将亡值),但不会对数值执行移动。当然,使用强制类型转换也是可以达到同样目的。

std::move(obj); // 等价于 static_cast<T&&>(obj);

在 stack overflow 上看到对 std::move() 的一段描述,与其说它是一个函数,不如说,它是编译器对表达式值评估的方式转换器。

以往惯常使用 C++ 类定义时,我们都知道有这么几个特殊的成员函数:

  • 默认构造函数(default constructor)
  • 复制构造函数(copy constructor)
  • 复制赋值运算符函数(copy assignment operator)
  • 析构函数(destructor)

来看看一个简单的例子:

class MB // MemoryBlock
{
public:
// 为下面代码演示简单起见
// 在 public 定义成员属性
size_t size;
char *buf; // 默认构造函数
explicit MB(int sz = 1024)
: size(sz), buf(new char[sz]) {}
// 析构函数
~MB() {
if (buf != nullptr) {
delete[] buf;
}
}
// 复制构造函数
MB(const MB& obj)
: size(obj.size),
buf(new char[obj.size]) {
memcpy(buf, obj.buf, size);
}
// 复制赋值运算符函数
MB& operator=(const MB& obj) {
if (this != &obj) {
if (buf != nullptr) {
delete[] buf;
}
size = obj.size;
buf = new char[size];
memcpy(buf, obj.buf, size);
}
return *this;
}
}

为了支持移动操作,从 C++ 11 开始,类定义里新增了两个特殊成员函数:

  • 移动构造函数(move constructor)
  • 移动赋值运算符函数(move assignment operator)

移动构造函数

在构造新对象时,如果传入的参数是右值引用对象,就会调用移动构造函数创建对象。如果没有自定义移动构造函数,那么编译器就会自动生成,默认实现是遍历调用成员属性的移动构造函数,并移动右值对象的成员属性数据到新对象。

定义一般声明形式如下:

T::T(C&& other);

基于上面的简单例子:

class MB // MemoryBlock
{
public:
// ... // 移动构造函数
MB(MB&& obj)
: size(0), buf(nullptr) {
// 移动源对象数据到新对象
size = obj.size;
buf = obj.buf;
// 清空源对象状态
// 避免析构函数多次释放资源
obj.size = 0;
obj.buf = nullptr;
}
}

可见,移动构造函数的执行过程,仅仅是简单赋值的过程,不涉及拷贝资源的耗时操作,自然执行效率大大提高。

移动赋值运算符函数

在调用赋值运算符时,如果右边传入的参数是右值引用对象,就会调用移动赋值运算符函数。同样,如果没有自定义移动赋值运算符函数,那么编译器也会自动生成,默认实现是遍历调用成员属性的移动赋值运算符函数并移动成员属性的数据到左边参数对象。

一般声明形式如下:

T& T::operator=(C&& other);

基于上面的简单例子:

class MB // MemoryBlock
{
public:
// ... // 移动赋值运算符函数
MB& MB::operator=(MB&& obj) {
if (this != &obj) {
if (buf != nullptr) {
delete[] buf;
}
// 移动源对象数据到新对象
size = obj.size;
buf = obj.buf;
// 清空源对象状态
// 避免析构函数多次释放资源
obj.size = 0;
obj.buf = nullptr;
}
return *this;
}
}

移动赋值运算符函数的执行过程,同样仅仅是简单赋值的过程,执行效率明显远超复制操作。

总结

回顾文首的示例代码,由于 C++ 11 加入了返回值优化 RVO(Return Value Optimization) 的特性,所以代码无需变更即可获得效率提升。对于部分编译器而言,比如 IBM Compiler、Visual C++ 2010 等,已经提前具备返回值优化的支持。

对于 RVO 的内容,暂不展开讨论,有兴趣的同学可以关注公众号【ENG八戒】了解后续更新,关注后甚至可以参与赠书活动!


现代 C++ 性能飞跃之:移动语义的更多相关文章

  1. PHP从PHP5.0到PHP7.1的性能全评测

    本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...

  2. PHP的性能演进(从PHP5.0到PHP7.1的性能全评测)

    本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...

  3. 如何提高Java并行程序性能??

    在Java程序中,多线程几乎已经无处不在.与单线程相比,多线程程序的设计和实现略微困难,但通过多线程,我们却可以获得多核CPU带来的性能飞跃,从这个角度说,多线程是一种值得尝试的技术.那么如何写出高效 ...

  4. 从PHP5.0到PHP7.1的性能全评测

    本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...

  5. C++11中右值引用和移动语义

    目录 左值.右值.左值引用.右值引用 右值引用和统一引用 使用右值引用,避免深拷贝,优化程序性能 std::move()移动语义 std::forward()完美转发 容器中的emplace_back ...

  6. java之高并发与多线程

    进程和线程的区别和联系 从资源占用,切换效率,通信方式等方面解答 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(H ...

  7. 【风雪之隅】写在PHP7发布之际一些话 2015-12-02

    做开源也有4,5年的时间了,从最初的 Yaf,到今天的 PHP7,我参与的项目越来越多,使用我代码的用户也越来越多,明天就要发布的PHP7,绝对是我从事开源以来的一个最重要里程碑,我应该纪念一下今天, ...

  8. C#下内存管理--垃圾收集

    章节安排 内存管理简介 垃圾回收机制 性能问题 C#下非托管资源的处理 要强调的几点 References 内存管理简介 对于任何一种编程语言,内存管理都是不得不提很重要的一块内容,但可惜的是目前为止 ...

  9. 2016值得关注的语言平台、JS框架

    语言和平台 Python 3.5 在今年发布了,带来了很多新特性 比如 Asyncio,,为你带来了类似 node.js 的事件机制,还有type hints. 鉴于Python 3 终于真正地火起来 ...

  10. 强大核心功能矩阵,详解腾讯云负载均衡CLB高可靠高性能背后架构

    1 前言 腾讯云负载均衡(Cloud LoadBalancer),简称CLB, 负载均衡通过设置虚拟服务地址(VIP)将来自客户端的请求按照指定方式分发到其关联的多台后端云服务器,服务器将请求的响应返 ...

随机推荐

  1. Teamcenter_NX集成开发:UF_UGMGR函数的使用

    最近工作中经常使用Teamcenter.NX集成开发的情况,因此在这里记录UF_UGMGR函数的使用.使用UF_UGMGR相关函数需要有Teamcenter使用经验,理解Teamcenter中文件夹. ...

  2. TCP三次握手一二三问

    下面整理下TCP握手和挥手的几个问题,参考资料小林图解计算机网络 1.什么是三次握手? Client端向Server端发送SYN为1的报文段,携带一个初始序列号x,client端进入SYN_SENT状 ...

  3. RFM客户分类模型的应用——R语言

    RFM模型是衡量客户价值和客户创造利益能力的重要工具和手段.在众多的客户关系管理(CRM)的分析模式中,RFM模型是被广泛提到的.该机械模型通过一个客户的近期购买行为.购买的总体频率以及花了多少钱3项 ...

  4. Django之admin后台管理

    目录 创建超级用户 向页面中添加表 admin管理页面表名中文显示 创建超级用户 python manage.py createsuperuser 向页面中添加表 登录后,页面中是什么都没有的,还需要 ...

  5. ChatGPT 和 Midjourney 初体验

    ChatGPT 和 Midjourney 这两个系统这些天红的发紫,自己也尝试着注册和使用了一下. 一.ChatGPT 我在上周才刚刚注册 OpenAI,注册过程也比较麻烦. 国内的 IP 是无法访问 ...

  6. 一天吃透MySQL面试八股文

    什么是MySQL MySQL是一个关系型数据库,它采用表的形式来存储数据.你可以理解成是Excel表格,既然是表的形式存储数据,就有表结构(行和列).行代表每一行数据,列代表该行中的每个值.列上的值是 ...

  7. RDIFramework.NET Web版报表管理-助力企业高效智能图表

    功能描述 在RDIFramework.NET Web版本中全新的报表管理功能模块,非常实用的功能,重量级推荐.主要用于对日常常用的报表做定制展示.可以自动发布到模块(就可授权给指定资源访问),在报表定 ...

  8. Java学习笔记08

    1. static关键字 ​ static可以用来修饰的成员变量和成员方法,被static修饰的成员是属于类的是放在静态区中,没有static修饰的成员变量和方法则是属于对象的. 1.1 静态变量 ​ ...

  9. Go语言网络编程: 模拟实现DNS服务器

    环境: 两台虚拟机,不限系统 写在前面 DNS服务器是干什么的?DNS服务器(Domain Name Server,域名服务器)是进行域名和与之相对应的IP地址进行转换的服务器,保存了一张域名和与之相 ...

  10. [SDR] GNU Radio 系列教程(十四) —— GNU Radio 低阶到高阶用法的分水岭 ZMQ 的使用详解

    目录 1.前言 2.ZMQ 块的类型 3.ZMQ 块的使用 4.DEMO 4.1 同一台电脑上的两个流程图 4.2 不同电脑上的两个流程图 4.3 作为 REQ/REP 服务器的 Python 程序 ...