漫谈C++11利器之右值引用(move语义&Perfect Forwarding)

简介: 该文章来自阿里巴巴技术协会(ATA) 作者:空溟  C++11(2011)标准推出已经很长时间了,最接地气的特性就要属"右值引用"了(Rvalue Reference),它实现了move语义和完美转发(Perfect Forwarding),一开始觉得不好理解,所以一直想对其做一个总结。网上也有

该文章来自阿里巴巴技术协会(ATA

作者:空溟

 C++11(2011)标准推出已经很长时间了,最接地气的特性就要属"右值引用"了(Rvalue Reference),它实现了move语义和完美转发(Perfect Forwarding),一开始觉得不好理解,所以一直想对其做一个总结。网上也有很多牛人已经做了细致的分析,但基本都是讲原理的多,本文就从Rvalue Reference引入动机入手,举例说明右值引用的使用场景,从而引出move语义和完美转发。

1. 右值引用动机:

从一个例子说起

 

不难发现,1. retvretvreadFile()结束之后便烟消云散);2. 返回的临时对象,在完成拷贝后也消失了或许有人会问,编译器不是有各种优化技术:Copy Elision、RVO(包括NRVO)等来避免临时对象的产生和拷贝吗?是的,对于上面的简单代码来说,大部分编译器都已经能够做到优化掉这两个对象,直接把那个retv创建到接受返回值的对象,即v中去。但是如果发生下面的情况呢? 

 

拷贝构造,变成了拷贝赋值,编译器就只能缴械投降,因为标准只允许在拷贝构造的情况下进行(N)RVO。

​那我把参数改成vector<int>&引用不就行了吗?的确可以,但有时这并不是我们需要的形式。

再看一个例子:

 

同样,编译器对于tmp临时对象也无能为力,特别是当T类型对象拥有指向(或引用)从堆内存分配的数据,那么这种深copy带来的开销可能是很大的。总之,编译器优化只能在特定的场景下生效。为此,C++11标准引入了右值引用,从语法层面支持了move语义,使用它可以使临时对象的拷贝具有move窃取功能,解决临时对象copy上的效率问题。理解右值引用,首先得区分左值(lvalue)和右值(rvalue)。

2. 何为左值和右值?

通俗来讲,左值可以运用&操作符取得地址,注意临时对象是无法取得地址的,因为很容易导致问题,而且左值必然有名字,其它的都是右值

举例:

 

 

需要注意的是,一个命名的左值引用是一个右值,就如上面最后一种情况。

3. 右值引用 & move语义:

 右值引用(rvalue reference,&&)跟传统意义上的引用(reference,&)很相似,为了更好地区分它们俩,传统意义上的引用又被称为左值引用(lvalue reference)。下面简单地总结了左值引用和右值引用的绑定规则(函数类型对象会有所例外):

(1)非const左值引用只能绑定到非const左值;
(2)const左值引用可绑定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能绑定到非const右值,但不适用于函数模板的形参;
(4)const右值引用可绑定到const右值和非const右值,它没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着修改);

可以看出,使用左值引用时,我们无法区分出绑定的是否是非常量右值的情况。那么,为什么要对非常量右值进行区分呢,区分出来了又有什么好处呢?如果我们确定某个值是非const右值(或者是后面不再使用的左值),那么我们在进行临时对象的copy的时候,完全没有必要copy实际的数据,而只需转移实际数据的指针。bang!记得auto_ptr吗?auto_ptr在"拷贝"的时候其实并非严格意义上的拷贝。"拷贝"是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的"拷贝"却会将源对象"掏空",只留一个空壳,实际上是一次资源所有权的转移,但auto_ptr 的危险之处在于看上去应该是复制,实际上确实转移。auto_ptr调用被转移过的的成员函数将会导致不可预知的后果, c++11中被unique_ptr替换,而unique_ptr就是用move语义实现的。

基于上面的理解,再来看下面一个例子:

 

由于f是右值引用,是否可以通过直接调用string的move构造而不做任何处理。这是错误的!结果这里只有string的copy构造被调用。原因就是上面提到的对于一个named rvalue reference,它是一个lvalue,由于rvalue不能绑定lvalue。而我们的愿意是要move,而不是copy,所以这里就需要使用标准库的std::move函数:m_s(std::move(f.m_s));而对于一个unamed rvalue reference,它是一个右值,这样就可以直接调用string的move构造。

 可是std::move()函数是如何把上述的左值f变成右值的呢?这就要从std::move()函数的实现说起,其实std::move()函数的实现非常地简单,下面以libcxx库中的实现(在<type_trait>头文件中)为例:

 

其中remove_reference的实现如下:

 

从move()函数的实现可以看到,move()函数的形参(Parameter)类型为右值引用,它怎么能绑定到作为实参(Argument)的左值呢?这不是仍然不符合右值引用的绑定规则三吗?简单地说,如果move只是个普通的函数(而不是模板函数),那么根据右值应用的绑定规则三和规则四可知,它的确不能使用左值作为其实参。但它是个模板函数,牵涉到模板参数推导,就有所不同了。

接下去说明下右值引用形参的函数模板的实参推演规则,即引用折叠(reference collapsing):设 T 为模板的类型参数,A 为实参的基本类型,则有:

T 形参 折叠后的T 折叠后实参型
A& T& A A&
A& T&& A& A&
A&& T& A& A&
A&& T&& A A&&

​函数模板参数推导规则(右值引用参数部分):当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。若实参为左值 A& ,则模板参数 T 应推导为引用类型 A& 。(根据引用折叠规则, A& + && => A&, 而T&& <=> A&,故T <=> A& )若实参为右值 A&& ,则模板参数 T 应推导为非引用类型 A 。(根据引用折叠规则, A或A&& + && => A&&, 而T&& <=> A&&,故T <=> A或A&&,这里强制规定T <=> A )

  了解了模板函数参数的推导过程,已经不难理解std::move()函数的实现了,当使用左值(假设其类型为T)作为参数调用std::move()函数时,实际实例化并调用的是std::move<T&>(T&),而其返回类型T&&,这就是move()函数左值变右值的过程(其实左值本身仍是左值,只是被当做右值对待而已)。

​理解了move语义后,我们不难想到可以对func(const X& data)的函数进行重载优化,定义一个func(X&& data);当实参是X&、const X&、const X&&时,调用老的函数;而当实参是X&&时,可以调用新的函数以获得性能上的提升,也就说当我们确定了是右值或者不在使用的左值情况下,可以做move等的优化操作;

完美转发 (Perfect Forwarding):

有了上面的理解,再来看接下的完美转发就容易了。C++11 之前,一直存在着参数转发的问题(可以参考:http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html),即不能方便地实现完美转发。转发的目的在于传递引用参数的附加属性,比如const/volatile和左右值属性。为了刻画这个问题,我们以左右值属性的传递为例(const/volatile 属性也存在相似的问题),参考下面的类定义:
 
 

 

为了支持移动语义,就需要重载构造函数,由于构造函数有两个参数,还需要考虑到右值引用和左值引用的组合形式:
 
  如果构造函数有 n 个参数,就需要 2^n 个重载!
  根据前面讲到的模板函数参数推到规则,这个问题就得到了完美解决:当函数的形参声明为 T&& 时,当且仅当实参为右值或者右值引用,折叠后的的实参类型才是右值引用,否则为左值引用。通过这个折叠规则,就可以实现左右值引用属性的转发。std::forward 就可以简单地实现为:

foward的完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数;而且一定要和模板参数自动推导关联起来使用,单独forward自己是没法完美转发的。简单说,就是做到了参数的属性不变,这样也就完美的实现了参数的完整传递。

总结:右值引用,表面上看只是增加了一个引用符号,但它对 C++ 软件设计和类库的设计有非常大的影响。它既能简化代码,又能提高程序运行效率。每一个 C++ 软件设计师和程序员都应该理解并能够应用它。我们在设计类的时候如果有动态申请的资源,也应该设计转移构造函数和转移拷贝函数。在设计类库时,还应该考虑 std::move 的使用场景并积极使用它。

参考资料:
C++11 FAQ,了解各个特性。

目录
相关文章
|
3月前
|
C++
c++左值和右值,左值引用和右值引用
c++左值和右值,左值引用和右值引用
26 0
|
1月前
|
存储 安全 编译器
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
79 0
|
1月前
|
存储 编译器 C++
【C++】—— C++11新特性之 “右值引用和移动语义”
【C++】—— C++11新特性之 “右值引用和移动语义”
|
2月前
|
算法 编译器 C++
C++新特性 右值引用&&
C++新特性 右值引用&&
|
3月前
|
存储 编译器
C++11(左值(引用),右值(引用),移动语义,完美转发)
C++11(左值(引用),右值(引用),移动语义,完美转发)
32 0
|
3月前
|
C++
C++中的左值、右值、左值引用、右值引用
C++中的左值、右值、左值引用、右值引用
|
3月前
|
编译器 C++
c++左值、右值引用和移动语义
c++左值、右值引用和移动语义
21 0
|
3月前
|
安全 编译器 C++
|
3月前
|
消息中间件 Kubernetes NoSQL
c++11左值引用与右值引用
c++11左值引用与右值引用
c++11新特性——右值引用和move语义
c++11新特性——右值引用和move语义