C++ 移动语义(通用引用,完美转发的终极理解)

C++11中引入了一组新特征,包括:左值和右值、左值引用、右值引用、通用引用、完美转发、引用折叠等。这些特征相互关联又可单独使用,理解起来并不太容易。

从宏观(抽象)的角度来看,编程语言(自然语言其实也一样)分为语法和语义两个部分,学习某个概念的时候,一定要先搞懂这个东西是语法范畴还是语义范畴。上面说的这些东西,都是语法范畴,它们特点是都对应一种特定的字符。比如右值与&&关联,通用引用与T&&关联,完美转发则与std::forward<>关联。而之所以把这些放到一起来讲,就是因为这些语法对应的语义是同一个,那就是移动语义。

一切语法都是为了语义(这句话要深刻理解),所谓语义,就是编译器翻译代码的方式。程序员用语法来实现语义,本质上是控制编译器的行为方式。具体到这里,要控制的是编译器是选择拷贝构造函数还是移动构造函数

下面我们就以一种三方(c++用户,c++标准,c++编译器)对话的形式,把这些东西的来龙去脉理一理。

用户:标准啊标准,你不是说c++快吗,为何我的代码还这么慢呢?

标准:编译器小弟,你快去看看,为啥代码这么慢,赶紧找点儿能优化的东西出来!

编译器:大哥,我看了半天,发现这里可以优化,那就是临时变量!这些东西只创建一次,然后被复制到别的地方,紧接着就析构了。这不是妥妥的浪费吗?

标准:那你咋不给他优化了啊?别复制了,直接把内容给他移动过去不就行了?

编译器:不是我不想啊,但我不知道该怎么移动啊,用户的东西,咱可不敢乱动啊!

标准:有道理,用户你看,这个地方是能优化的,但是呢,你得告诉编译器怎么移动你的对象,这样吧,我给你弄个移动构造函数出来,你把它写好,然后让编译器去调用它,你看怎么样?

用户:那。。。。行吧!(又给自己找点儿事儿干)不过我怎么用参数区别移动构造函数和拷贝构造函数呢?

标准:这个好办,弄个新的语法就行了啊,这个我擅长。就用&&这个吧,这个表示右值引用,正好跟前面的左值引用对应,怎么样?

标准:基于上面的讨论,下面我规定:

1. 用户要想优化自己定义的对象,必须实现移动构造函数和移动赋值操作符。(为了和之前的拷贝构造区分,需要增加右值引用语法。)

2. 编译器你负责识别临时对象,临时对象赋值的时候就调用用户提供的移动构造...

用户:等会儿!我费劲巴拉写了移动构造函数和移动赋值操作符,结果就只能针对临时变量用啊?这玩意儿看不见摸不着的,这不是浪费感情吗?不行!我要有自己选择调用哪个函数的权利!

标准:这。。。行吧,那我给你提供个方式,让你能把一个普通的变量转成临时变量,额,不对,这不能叫临时变量了,算了,重新起个名字吧,我看看叫啥呢。。。。就叫右值吧。

用户:行吧,你爱叫啥叫啥,快把接口给我!

标准:既然都是类型转换,那还按照类型转换的语法吧。(int&&)a这个怎么样,这样a就转成右值了,当然static_cast<int&&>(a)咱也支持...

用户:靠,又是强制类型转换,这玩意儿这么丑陋,你咋就这么爱用呢。。。

标准:好好好,别急,我回头让STL给你封装一下,给你个优雅的接口,叫std::move()怎么样,见名知意,怎么样可以吧?

用户:行吧,就这样吧。。。

标准:好了,下面我追加规定:

3. 用户可以通过std::move()把一个普通的值,转成右值,编译器也要去调用移动构造函数。

。。。。。过了三天。。。。

用户:标准啊标准,虽然这个移动语义平时挺好用,但是在写模板的时候,非常不方便啊,每次用模板实现一个接口,都需要提供两个版本:(假设T&&被当作右值引用)

template <typename T>
void foo(T&& a) {
    other_foo(std::move(a));
}

template <typename T>
void foo(T& a) {
    other_foo(a);

}

一个是左值引用,一个是右值引用,难道就不能用一个模板去匹配两种类型吗,你这模板不就是用来根据类型实例化的吗?

标准:这个好办啊,我给你弄个新语法,如果你想用一个模板既能生成左值引用,又能生成右值引用,你就写成这样T&&&。。。

用户:你搁这儿套娃呢啊,我看那个只针对右值引用的版本根本没用,毕竟谁会写个函数,只接收右值呢?你非得移动我的对象不可?

标准:奥,既然这样,那就不用T&&&了,直接用T&&吧,这样它就不能叫右值引用了,就叫。。。通用引用吧,它既能匹配左值,又能匹配右值,这样不就丝滑了吗?

用户:这还差不多。

标准:我追加规定:

4. template <typename T> void foo(T&& a);为通用引用形式,编译器要根据用户传入的参数类型来实例化,如果用户传入的是左值,那a被实例化为左值引用,如果用户传入的是右值,那a被实例化成右值引用。

用户:还有一个问题,既然我调用模板函数时,都把参数标记成右值了,那在模板函数内部传递给其他函数的时候,让编译器自动转成右值不就好了,为啥每次都得加std::move()?

编译器:虽然你传过来的是个右值,但右值引用本身是左值啊。行,就算我给你记着a是右值引用,但后面再调用的函数可能只接收左值啊,这时候如果自动加上std::move()那不就编译不过了吗?行,就算我根据参数形式,选择性给你加std::move()。还有一种情况,比如foo()里会调用多个函数,每个函数都把a当参数,那只能在最后一个函数加std::move(),前面的是不能加的!这个我太难区分了啊!所以我看这个还是用户自己选择性加比较合适,这样用户还能保留把右值引用退化成左值引用的能力。

用户:听着好像有点儿道理,但我的模板只有一份代码,我怎么只在a是右值引用的时候加std::move()呢?

标准:这好办,无非是引入一个新的函数,就叫std::forward(a)吧。这个函数在a是左值引用的时候返回a对应的左值,在a是右值引用的时候返回a对应的右值,不就好了吗?回头我让STL给你封装。。。

STL:大哥你等会儿吧,你这个我干不了,编译器不记着a是左值引用还是右值引用,那我std::forward(a)接收过来的就是个普通的类型啊,我怎么知道它引用的是左值还是右值?

标准:这个。。。。还真有点儿麻烦,a在类型推断完以后,就没有左值引用还是右值引用的区分了,得找个东西记着它是左值引用还是右值引用才行,编译器自己不愿意记,那就只能用现有的东西了,我看看代码。。。哎,这儿不还有个T吗,把它记录到T里就OK了呀!这样,如果a被实例化成左值引用(比如说int&),那T就不是基本类型了,而是对应的左值引用类型(int&),如果a被实例话成右值,那T还是基本类型(int)。等调用std::forward的时候,把这个类型T有传给你,这不就能区分了?

STL:(你是真能对付啊)老大英明!

编译器:等会儿老大!T如果是int&,那a的类型就变成int& &&了啊,这是。。。引用的引用!我不会翻译啊!

标准:刚不说了嘛,只有a是左值引用的时候,T才是int&,所以a肯定也是int&,也就是说int& &&=int&,引用的引用还是引用,这听着多么丝滑啊~

编译器:(你是真能对付啊)老大英明!但如果只处理& &&这种类型,那会让这个东西看起来像是咱们打的补丁!您既然说了引用的引用还是引用,而每种引用都有左值引用和右值引用的区分,这排列组合。。。。一共有4种,干脆咱都给它支持了,这样它不就像一个新的feature了吗?名字我都给您想好了,叫引用折叠,怎么样,一听就高大上啊~

标准:你小子脑子倒是挺快。。。那行吧~其实无论引用怎么叠加,最后区别都是调用拷贝构造还是调用移动构造函数,所以结果只有两种,左值引用和右值引用两种。刚才已经说了& &&=&,再来看看& &,这个跟右值引用无关,结果只能是左值引用&,&& &和第一种类似,也按照&处理吧。这样前三个都是左值引用了,最后的&& &&,就按照右值引用处理吧!下面我追加规定:

5. 用户在模板中,如果要让编译器选择性给变量加std::move()(左值引用不加,右值引用加),就要调用std::forward<T>(),这个叫作完美转发

6. 如果T被推断成引用类型,那再由T定义的引用类型,会触发引用折叠:

& & -> &
& && -> &
&& & -> &
&& && -> &&

怎么样,完美!!

用户:好嘛,给打补丁的语法起个高大上的名字,让别人觉得这是新feature,可真有你的啊!

标准:没办法,这年头做点儿实事真的难啊!好了,散会!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值