【CppCon2019】Back to Basics: Move Semantics (part 1 of 2)-学习笔记

视频原链接:
CppCon 2019: Klaus Iglberger “Back to Basics: Move Semantics (part 1 of 2)

图片原链接:
github/back_to_basics_move_semantics_part_1__klaus_iglberger__cppcon_2019.pdf

假设现在有两个vector变量,他们的startfinishend_of_storage指向地址和储存值如下图所示。

在这里插入图片描述

现在我们将V1赋值给V2,并且期望赋值后的V2结构如下图所示,需要怎么做呢,就引出了今天要讲解的移动语义(move semantics)。

在这里插入图片描述

先来看另外一个例子,假设现在有一个createVector函数和v2变量如下图所示。

在这里插入图片描述

现在我们将调用createVector函数的返回值赋值给v2,如下图所示。

因为在createVector内部只返回了一个临时创建的没有命名的变量,所以我们暂时叫他__tmp__

在这里插入图片描述

如果将__tmp__赋值给v2的操作是一个个值的深拷贝将非常损耗性能。

所以我们先将指针进行copy,如下图所示。

接着将__tmp__中所持有的指针“删除”,如下图所示。

当然现在省略了大量的细节。

还需要注意的是上述指的是临时对象没有被其他引用的情况。

在这里插入图片描述

现在我们将这种思想带回到第一个例子中,使用到的就是std::move,细节的部分后面再看,先可以大致认为v1被转移到了v2,如下面几张图所示
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在深入细节之前,先简单谈一下C++中的左值(lvalue)和右值(rvalue)。

在这里插入图片描述

在过去,l和r就是代表left和right,能出现在等号左边的就是lvalue,只能出现在右边的就是rvalue。

但这仅仅是过去的概念,在现在的C++中有更加明确的确定方式,

如下图所示,s+s=s,在右边的s实际上是lvalue,在左边的s+s是rvalue,因为右边的s是有命名、有标识符的,而s+s计算出来的临时对象是无命名的、无标识标识符的。(注:还有个比较常用的判断左右值方法是能否取得对象的内存地址)

在这里插入图片描述
在这里插入图片描述

现在我们来稍微深入一点细节,在下图的左边是一个标准vector的结构,右边是之前举的代码例子。

首先看v2=v1,因为v1是一个lvalue,所以会调用到拷贝赋值(copy assignment)函数,这没什么问题。

在这里插入图片描述

接着看第二个例子v2=createVector(),这里的createVector()是个rvalue,但在C++11之前其实没有办法很好的区分,因此还是会调用拷贝赋值函数多拷贝一次。

在这里插入图片描述

但在C++11以及之后,我们通常使用&&来表示右值引用,因此该句代码可以正常调用到移动赋值(move assignment)函数。

在这里插入图片描述

接着看第三个例子v2=std::move(v1),这里的std::move(v1)会被当做右值调用移动赋值函数,将v1里的内容搬移至v2。对于被搬移的对象我们通常都不会再使用它了,让他随着生命周期结束自然销毁,因此v1也被称为xvalue(expiring value,将亡值)。

在这里插入图片描述

其实std::move并不会直接搬移任何东西,他只是将参数值强制转换为右值引用,如下图所示。

在这里插入图片描述

对上面的内容做个小总结:

  • C++中的容器使用值语义。
  • 在C++11之前会导致不必要的复制操作。
  • C++11引入了右值引用来辨别lvalue和rvalue。
  • 右值引用是可以修改的临时对象.

在这里插入图片描述

下面再来深入一点实现的细节。

首先看一下这个简单的Widget类,如下图所示,有三个成员变量和展示出来的两个函数,展示出来的这两个函数现在也被添加进默认提供的特殊成员函数中,目前一共有6个默认提供的特殊成员函数,分别是构造函数、析构函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数。

在这里插入图片描述

目前这个Widget类的结构比较“简单”,因此只需要将移动构造函数和移动赋值函数=default交给编译器就可以了,甚至不需要声明出来。

在C++核心指南(C++ Core Guidelines)中也有提及:C.20: If you can avoid defining default operations, do(如果你可以避免去定义默认的操作,那就不要定义了)。这也被称为“rule of zero”(有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权。其他类都不应该拥有自定义的析构函数、复制/移动构造函数或复制/移动赋值运算符)。

在这里插入图片描述

但是现在为了方便探讨,我们把unique_ptr智能指针改为普通指针,如下图所示,这样我们需要去手动完善移动相关函数

在这里插入图片描述

现在我们来完善移动构造函数,我们的目标是:

  • 将w的内容转移至自身对象
  • 让w处于有效但未定义的状态

在这里插入图片描述

第一步,设置i,这里直接复制是没有问题的

在这里插入图片描述

第二步,设置s,如果这里直接传w.s就有点问题了,因为这里w是一个lvalue(进入函数时为右值,但是被w引用后w为左值)所以会调用copy,导致效率较低

在这里插入图片描述

所以我们调用std::move将w的字符串转移为右值,能让字符串正确的进行move。

在这里插入图片描述

当然,也可以在设置i的时候调用std::move,尽管这不会带来性能提升,还是复制,但是在移动构造中统一这样写也算比较好的习惯。

在这里插入图片描述

接着移动指针。

在这里插入图片描述

一定要记住,在移动构造/移动拷贝的操作中,指针相关都是比较特殊的操作。

例如这个地方有两个指针指向了相同的值,那么意味着可能临时对象析构的时候有可能会把指向的堆上值也同时析构,因此要把临时对象的指针给悬空,如下图所示。

现在,我们就完成了第一个目标,把w的内容转移给自身对象。

在这里插入图片描述

第二个目标稍后再看,先来看看两个优化的细节。

第一个优化是可以把搬移指针和指针悬空用std::exchange优化写法,如下图所示,这是一个很小的优化可有可无。

在这里插入图片描述

第二个优化比较重要,移动构造函数需要声明为noexcept(不会抛出异常)。

在这里插入图片描述
在这里插入图片描述

因为STL为了保证容器类型的内存安全,在大多数情况下只会调用被标记为不会抛出异常的移动构造函数,否则会调用其拷贝构造函数来作为替代,下图为测试性能对比。

在这里插入图片描述

现在回到刚刚的第二点目标,要让传进来的w处于有效但是未定义的状态。

这其实来自于另一条核心准则,简单点说就是要保证在x=std::move(y)执行之后y=z可以按照通常的语义执行并且符合预期(只是举例,虽然此处讲的是移动构造)。

在这里插入图片描述

所以这个地方更加标准的做法是将w.i也归为默认值(只是举例,意思是尽量都归为默认值),如下图所示(例如这个w.i可能是通过某种方式去累加的,一开始有个默认值0,但是搬移完后的赋值函数中并没有置为0,就会出问题)。

在这里插入图片描述

当然如果你能确保不会有上述情况的发生,也可以不做这个操作,而且不做这个操作对于性能来说更好。

在这里插入图片描述

现在这个移动构造函数就算是做完了,总结一下其实就是分为两个步骤,第一步将成员移动,第二步,做某些必要的重置操作(例如指针)。

在这里插入图片描述

我们再回到最开始的Widget类,那个时候我们使用的是unique_ptr,那其实只用做搬移操作(详见智能指针)。

在这里插入图片描述

如果只用做搬移操作,那完全可以交给编译器进行操作,这样就是比较遵循RAII和Rule of zero的设计了。

在这里插入图片描述

移动构造基础部分就算是结束了。

程序员们应该要有自己的思考,而不是盲目的遵循着某模式。

在这里插入图片描述

只要能够理解上面的内容,那写出移动赋值函数来问题就也不大了,在移动赋值中比较特殊的就是要记得,既然是被赋值,那么自身资源大概率已经被初始化了,所以要记得清理原本的资源。

在这里插入图片描述

有些人会喜欢用下图这样的swap操作,但是讲者不是很推荐,的确只要设计没问题那么把值交换给临时变量理论上来说一定会被析构,但是在不同的调用语义下我们是比较难推测出资源到底还会存活多久才被销毁,确定性很差,因此讲者还是比较推荐更加明确的写法。

在这里插入图片描述

还有不要忘了很影响效率的noexcept声明,总结下来移动赋值其实就多了第一步清理资源的步骤。

在这里插入图片描述

在这里插入图片描述

当然,如果我们是使用unique_ptr也是会交给编译器来处理,生活如此美好。

(CppCon2019的RAII视频也讲的挺好,而且有英语字幕,比移动语义这个自动生成的字幕易懂很多。)

在这里插入图片描述

在这里插入图片描述

这里还有个小技巧,拷贝赋值和移动赋值可以统一只实现一个如下图的形式,利弊可以自行考虑一下。

在这里插入图片描述

还要记得,如果定义或者=delete了任何一个特殊的成员函数,要记得把其他的特殊成员函数也全部显示声明出来。

这也被成为“rule of five”或者“rule of six”。

(cppon2019有另一场关于RAII、rule of zero、rule of five稍微深入一点的讲座,而且油管上的视频还有英文字幕,比移动语义讲座视频的自动生成字幕易懂很多)。

在这里插入图片描述

最后再引用C++核心准则的一条:优先使用简单、常规的方式传递参数。

大概情况可以参考下图附带的表格对比,具体情况再具体分析。

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值