服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - 浅析C++11中的右值引用、转移语义和完美转发

浅析C++11中的右值引用、转移语义和完美转发

2021-04-15 13:31daisy C/C++

对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂。最近翻翻资料感觉突然开窍,因此顺便记录下C++11中的右值引用、转移语义和完美转发,方便大家查阅参考。

1. 左值与右值:

    C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值.

    可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值.

    从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如:

?
1
2
3
4
5
6
7
8
9
10
11
int& foo(){int tmp; return tmp;}
 
int fooo(){int tmp; return tmp;}
 
int a=10;
 
const int b;
 
int& temp=foo();//虽然合法,但temp引用了一个已经不存在的对象
 
int tempp=fooo();

以上代码中,a,temp和foo()都是非常量左值,b是常量左值,fooo()是非常量右值,10是常量右值,有一点要特别注意:返回的引用是左值(可以取地址)!

一般来说,编译器是不允许对右值进行更改的(因为右值的生存期不由程序员掌握,即使更改了右值也未必可以用),对于内置类型对象尤其如此,但C++允许使用右值对象调用成员函数,虽然允许这样做,但出于同样原因,最好不要这么做.

2. 右值引用:

    右值引用的表示方法为

?
1
Datatype&& variable

    右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用.右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期,右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的建构来减少对象建构和析构操作以达到提高效率的目的,例如对于以下函数:

?
1
2
3
4
5
(Demo是一个类)
Demo foo(){
  Demo tmp;
  return tmp;
}

在编译器不进行RVO(return value optimization)优化的前提下以下操作:

?
1
Demo x=foo();

将会调用三次构造函数(tmp的,x的,临时对象的),相应的在对象被销毁时也会调用三次析构函数,而如果采用右值引用的方式:

?
1
Demo&& x=foo();

那么就不需要进行x的建构,本来本来要被销毁的临时对象也会由于x的绑定而将生存期延长至和x一样(可以理解为x赋予了那个临时对象一个合法地位:一个名字),就需要提高了效率(代价就是tmp需要占据4字节空间,但这是微不足道的).

    右值引用与左值引用绑定规则:

         常量左值引用可以绑定到常量和非常量左值,常量和非常量右值;

         非常量左值引用只能绑定到非常量左值;

         非常量右值引用只能绑定到非常量右值(vs2013也可以绑定到常量右值);

         常量右值引用只能绑定到常量和非常量右值(非常量右值引用只是为了语义的完整而存在,常量左值引用就可以实现它的作用).

         虽然从绑定规则中可以看出常量左值引用也可以绑定到右值,但显然不可以改变右值的值,右值引用就可以,从而实现转移语义,因为右值引用通常要改变所绑定的右值,所以被绑定的右值不能为const.

    注意:右值引用是左值!

3. 转移语义(move semantics):

    右值引用被引入的目的之一就是实现转移语义,转移语义可以将资源 ( 堆,系统对象等 ) 的所有权从一个对象(通常是匿名的临时对象)转移到另一个对象,从而减少对象构建及销毁操作,提高程序效率(这在2的例子中已经作了解释).转移语义与拷贝语义是相对的.从转移语义可以看出,实际上,转移语义并不是新的概念,它实际上已经在C++98/03的语言和库中被使用了,比如在某些情况下拷贝构造函数的省略(copy constructor elision in some contexts),智能指针的拷贝(auto_ptr “copy”),链表拼接(list::splice)和容器内的置换(swap on containers)等,只是还没有统一的语法和语义支持

    虽然普通的函数和操作符也可以利用右值引用实现转移语义(如2中的例子),但转移语义通常是通过转移构造函数和转移赋值操作符实现的.转移构造函数的原型为Classname(Typename&&) ,而拷贝构造函数的原型为Classname(const Typename&) ,转移构造函数不会被编译器自动生成,需要自己定义,只定义转移构造函数也不影响编译器生成拷贝构造函数,如果传递的参数是左值,就调用拷贝构造函数,反之,就调用转移构造函数.

例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Demo{
 
public:
 
  Demo():p(new int[10000]{};
 
  Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//转移构造函数
 
  Demo(const Demo& lre):arr(new int[10000]),size(arr.size){
 
    for(int cou=0;cou<10000;++cou)
 
      arr[cou]=lew.arr[cou];
 
  }
 
private:
 
  int size;
 
  int* arr;
 
}

    从以上代码可以看出,拷贝构造函数在堆中重新开辟了一个大小为10000的int型数组,然后每个元素分别拷贝,而转移构造函数则是直接接管参数的指针所指向的资源,效率搞下立判!需要注意的是转移构造函数实参必须是右值,一般是临时对象,如函数的返回值等,对于此类临时对象一般在当行代码之后就被销毁,而采用转移构造函数可以延长其生命期,可谓是物尽其用,同时有避免了重新开辟数组.对于上述代码中的转移构造函数,有必要详细分析一下:

?
1
Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}

lre是一个右值引用,通过它间接访问实参(临时对象)的资源来完成资源转移,lre绑定的对象(必须)是右值,但lre本身是左值;

因为lre是函数的局部对象,”lre.arr=NULL"必不可少,否则函数结尾调用析构函数销毁lre时仍然会将资源释放,转移的资源还是被系统收回.

4. move()函数

    3中的例子并非万能,Demo(Demo&& lre)的实参必须是右值,有时候一个左值即将到达生存期,但是仍然想要使用转移语义接管它的资源,这时就需要move函数.

    std::move函数定义在标准库<utility>中,它的作用是将左值强行转化为右值使用,从实现上讲,std:move等同于static_cast<T&&>(lvalue) ,由此看出,被转化的左值本身的生存期和左值属性并没有被改变,这类似于const_cast函数.因此被move的实参应该是即将到达生存期的左值,否则的话可能起到反面效果.

5. 完美转发(perfect forwarding)

    完美转发指的是将一组实参"完美"地传递给形参,完美指的是参数的const属性与左右值属性不变,例如在进行函数包装的时候,func函数存在下列重载:

?
1
2
3
void func(const int);
void func(int);
void func(int&&);

如果要将它们包装到一个函数cover内,以实现:

?
1
2
3
void cover(typename para){
  func(para);
}

使得针对不同实参能在cover内调用相应类型的函数,似乎只能通过对cover进行函数重载,这使代码变得冗繁,另一种方法就是使用函数模板,但在C++ 11之前,实现该功能的函数模板只能采用值传递,如下:

?
1
2
3
4
5
6
template<typename T>
void cover(T para){
  ...
  func(para);
  ...
}

但如果传递的是一个相当大的对象,又会造成效率问题,要通过引用传递实现形参与实参的完美匹配(包裹const属性与左右值属性的完美匹配),就要使用C++ 11 新引入的引用折叠规则:

函数形参       T的类型         推导后的函数形参

T&               A&                A&
T&               A&&              A&
T&&             A&                A&
T&&             A&&              A&&

 因此,对于前例的函数包装要求,采用以下模板就可以解决:

?
1
2
3
4
5
6
template<typename T>
void cover(T&& para){
  ...
  func(static_cast<T &&>(para));
  ...
}

 

如果传入的是左值引用,转发函数将被实例化为:

?
1
2
3
4
5
void func(T& && para){
 
  func(static_cast<T& &&>(para));
 
}

应用引用折叠,就为:

?
1
2
3
4
5
void func(T& para){
 
  func(static_cast<T&>(para));
 
}

如果传入的是右值引用,转发函数将被实例化为:

?
1
2
3
4
void func(T&& &&para){
 
   func(static_cast<T&& &&>(para));
}

应用引用折叠,就是:

?
1
2
3
4
5
void func(T&& para){
 
  func(static_cast<T&&>(para));
 
}

对于以上的static_cast<T&&> ,实际上只在para被推导为右值引用的时候才发挥作用,由于para是左值(右值引用是左值),因此需要将它转为右值后再传入func内,C++ 11在<untility>定义了一个std::forward<T>函数来实现以上行为,

所以最终版本为

?
1
2
3
4
5
6
7
template<typename T>
 
void cover(T&& para){
 
  func(forward(forward<T>(para)));
 
}

std::forward的实现与static_cast<T&&>(para)稍有不同

std::forward函数的用法为forward<T>(para) , 若T为左值引用,para将被转换为T类型的左值,否则para将被转换为T类型右值

总结

以上就是关于C++11中右值引用、转移语义和完美转发的全部内容,这篇文章介绍的很详细,希望对大家的学习工作能有所帮助。

延伸 · 阅读

精彩推荐