现代 C++ 性能飞跃之:移动语义
*以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「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++ 性能飞跃之:移动语义的更多相关文章
- PHP从PHP5.0到PHP7.1的性能全评测
本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...
- PHP的性能演进(从PHP5.0到PHP7.1的性能全评测)
本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...
- 如何提高Java并行程序性能??
在Java程序中,多线程几乎已经无处不在.与单线程相比,多线程程序的设计和实现略微困难,但通过多线程,我们却可以获得多核CPU带来的性能飞跃,从这个角度说,多线程是一种值得尝试的技术.那么如何写出高效 ...
- 从PHP5.0到PHP7.1的性能全评测
本文是最初是来自国外的这篇:PHP Performance Evolution 2016, 感谢高可用架构公众号翻译成了中文版, 此处是转载的高可用架构翻译后的文章从PHP 5到PHP 7性能全评测( ...
- C++11中右值引用和移动语义
目录 左值.右值.左值引用.右值引用 右值引用和统一引用 使用右值引用,避免深拷贝,优化程序性能 std::move()移动语义 std::forward()完美转发 容器中的emplace_back ...
- java之高并发与多线程
进程和线程的区别和联系 从资源占用,切换效率,通信方式等方面解答 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元:而把传统的进程称为重型进程(H ...
- 【风雪之隅】写在PHP7发布之际一些话 2015-12-02
做开源也有4,5年的时间了,从最初的 Yaf,到今天的 PHP7,我参与的项目越来越多,使用我代码的用户也越来越多,明天就要发布的PHP7,绝对是我从事开源以来的一个最重要里程碑,我应该纪念一下今天, ...
- C#下内存管理--垃圾收集
章节安排 内存管理简介 垃圾回收机制 性能问题 C#下非托管资源的处理 要强调的几点 References 内存管理简介 对于任何一种编程语言,内存管理都是不得不提很重要的一块内容,但可惜的是目前为止 ...
- 2016值得关注的语言平台、JS框架
语言和平台 Python 3.5 在今年发布了,带来了很多新特性 比如 Asyncio,,为你带来了类似 node.js 的事件机制,还有type hints. 鉴于Python 3 终于真正地火起来 ...
- 强大核心功能矩阵,详解腾讯云负载均衡CLB高可靠高性能背后架构
1 前言 腾讯云负载均衡(Cloud LoadBalancer),简称CLB, 负载均衡通过设置虚拟服务地址(VIP)将来自客户端的请求按照指定方式分发到其关联的多台后端云服务器,服务器将请求的响应返 ...
随机推荐
- Defi开发简介
Defi开发简介 介绍 Defi是去中心化金融的缩写, 是一项旨在利用区块链技术和智能合约创建更加开放,可访问和透明的金融体系的运动. 这与传统金融形成鲜明对比,传统金融通常由少数大型银行和金融机构控 ...
- 逍遥自在学C语言 | 关系运算符
前言 一.人物简介 第一位闪亮登场,有请今后会一直教我们C语言的老师 -- 自在. 第二位上场的是和我们一起学习的小白程序猿 -- 逍遥. 二.构成和表示方式 关系运算符的作用是判断符号两边大小的关系 ...
- mysql的concat与concat_ws拼接字符串的使用
concat的使用 可以拼接多个字符 mysql> select concat(name,dept,job) from t1; +-----------------------+ | conca ...
- 如何将 CentOS 8 转换为 CentOS Stream
CentOS 未来是不会更新数字版本了.CentOS 项目组,未来会变更为Stream版本,也就是俗称的滚动版本,那么如何将数字版本升级为滚动版本呢? 若需要将其转换为滚动版本,那么即可参考本文进行升 ...
- mariabackup -prepare step on increment backup failed
问题描述:使用mariabackup对maridb10.6.4进行物理备份,进行增量恢复的时候报错.截止到目前,还是mariadb的一个bug,还没有修复.在增备的过程中如果出现新库的建立,数据库就会 ...
- Qt5.9 UI设计(七)——统一样式设计
前言 前面已经将UI设计部分实现,各页面也做了最简单的设计,本章介绍一下qss样式的使用.样式设计最终的显示效果如下图: 操作步骤 将stylesheet.qss 样式文件添加进工程 styleshe ...
- Qt 加载 libjpeg 库出现“长跳转已经运行”错误
继上篇 Qt5.15.0 升级至 Qt5.15.9 遇到的一些错误 篇幅有点长,先说解决方法,在编译静态库时加上 -qt-libjpeg,编译出 libjpeg 库后,在项目中使用 #pragma c ...
- 已知n个数的入栈序列,求一共有多少种出栈序列 (卡特兰数)
已知\(n\)个数的入栈序列,求一共有多少种出栈序列 这个经典问题有两种解法. 解法一: 设\(f(x)\)为\(x\)个数入栈后,再全部出栈的序列数量 假设我们有\(4\)个数\(a,b,c,d\) ...
- 3.2 构造器、this、包机制、访问修饰符、封装
构造器 构造器:在实例化的一个对象的时候会给对象赋予初始值,因此我们可以通过修改构造器,来改变对象的初始值,构造器是完成对象的初始化,并不是创建对象 我们也可以创建多个构造器实现不同的初始化,即构造器 ...
- Python_11 类的方法
一.查缺补漏 1. 实例方法必须用类实例化对象()来调用,用类来调用时会执行,但是self中不是实例化类地址而是传的字符串 二.类中的方法 1. 实例方法 1. 定义在类里面的普通方法(函数) 2. ...