A brief analysis of rvalue references, transfer semantics and perfect forwarding in C++11

高洛峰
Release: 2017-01-23 14:11:24
Original
1484 people have browsed it

1. Lvalues ​​and rvalues:

C++ has no standard definitions for lvalues ​​and rvalues, but there is a widely recognized saying: those with addresses, names, and non-temporary ones are lvalue; one that cannot take an address, has no name, and is temporary is an rvalue.

It can be seen that immediate data, the value returned by the function, etc. are all rvalues; instead of anonymous objects (including variables), the values ​​returned by the function References, const objects, etc. are all lvalues.

Essentially understood, creation and destruction are controlled by the compiler behind the scenes. Programmers can only ensure that what is valid in this line of code is rvalues ​​(including immediate numbers). ); and those created by users, whose lifetime can be known through scoping rules, are lvalues ​​(including references to local variables returned by functions and const objects), for example:

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();
Copy after login

In the above code, a, temp and foo() are all non-constant lvalues, b is a constant lvalue, fooo() is a non-constant rvalue, and 10 is a constant rvalue. There is one thing to pay special attention to: The reference returned is an lvalue (can take an address)!

Generally speaking, the compiler does not allow changes to rvalues ​​(because the lifetime of the rvalue is not controlled by the programmer, even if the rvalue is changed, it may not be usable), especially for built-in type objects. , but C++ allows the use of rvalue objects to call member functions. Although this is allowed, it is best not to do so for the same reason.

2. Rvalue reference:

Rvalue reference The representation method is

Datatype&& variable
Copy after login

Rvalue reference is a new feature of C++11, so the reference of C++98 is an lvalue reference. Rvalue reference is used to bind When bound to an rvalue, the lifetime of the rvalue that would have been destroyed after being bound to the rvalue will be extended to the lifetime of the rvalue reference bound to it. The existence of the rvalue reference is not to replace the lvalue reference. Instead, make full use of the construction of rvalues ​​(especially temporary objects) to reduce object construction and destruction operations to improve efficiency. For example, for the following functions:

(Demo是一个类)
Demo foo(){
  Demo tmp;
  return tmp;
}
Copy after login

Under the premise that the compiler does not perform RVO (return value optimization) optimization, the following operations are performed:

Demo x=foo();
Copy after login

The constructor (tmp's, x, temporary object), correspondingly the destructor will be called three times when the object is destroyed, and if rvalue reference is used:

Demo&& x=foo();
Copy after login

Then there is no need to construct x. The temporary object that was originally going to be destroyed will also have its lifetime extended to the same as x due to the binding of x (it can be understood that x gives the temporary object a legal status: a name ), the efficiency needs to be improved (the price is that tmp needs to occupy 4 bytes of space, but this is trivial).

Binding rules for rvalue references and lvalue references:

Constant left Value references can be bound to constant and non-constant lvalues, constants and non-constant rvalues;

Non-constant lvalue references can only be bound to non-constant lvalues;

Non-constant right values Value references can only be bound to non-const rvalues ​​(vs2013 can also be bound to constant rvalues); It only exists for the completeness of semantics, and constant lvalue references can achieve its role).

Although it can be seen from the binding rules that constant lvalue references can also be bound to rvalues, it is obviously not You can change the value of an rvalue by using an rvalue reference to achieve transfer semantics. Because an rvalue reference usually changes the bound rvalue, the bound rvalue cannot be const.

Note :Rvalue references are lvalues!

3. Move semantics:

One of the purposes of rvalue references being introduced is to implement transfer semantics. Transfer semantics can move resources (heap , system objects, etc.) is transferred from one object (usually an anonymous temporary object) to another object, thereby reducing object construction and destruction operations and improving program efficiency (this has been explained in the example of 2). Transfer semantics It is opposite to copy semantics. As can be seen from transfer semantics, in fact, transfer semantics is not a new concept. It has actually been used in the language and library of C++98/03, such as in some cases. The omission of the copy constructor (copy constructor elision in some contexts), the copy of the smart pointer (auto_ptr "copy"), the splicing of the linked list (list::splice) and the replacement of the container (swap on containers), etc., are just not unified yet. Syntax and semantic support

Although ordinary functions and operators can also use rvalue references to implement transfer semantics (such as the example in 2), transfer semantics are usually implemented through transfer constructors and transfer assignment operators. The prototype of the transfer constructor is Classname(Typename&&), while the prototype of the copy constructor is Classname(const Typename&). The transfer constructor will not be automatically generated by the compiler and needs to be defined by yourself. Only defining the transfer constructor will not affect compilation. The constructor generates a copy constructor. If the passed parameter is an lvalue, the copy constructor is called. Otherwise, the transfer constructor is called.

For example:

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;
 
}
Copy after login

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

Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}
Copy after login

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

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

4. move()函数

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

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

5. 完美转发(perfect forwarding)

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

void func(const int);
void func(int);
void func(int&&);
Copy after login

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

void cover(typename para){
  func(para);
}
Copy after login

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

template<typename T>
void cover(T para){
  ...
  func(para);
  ...
}
Copy after login

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

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

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

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

template<typename T>
void cover(T&& para){
  ...
  func(static_cast<T &&>(para));
  ...
}
Copy after login

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

void func(T& && para){
 
  func(static_cast<T& &&>(para));
 
}
Copy after login

应用引用折叠,就为:

void func(T& para){
 
  func(static_cast<T&>(para));
 
}
Copy after login

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

void func(T&& &¶){
 
   func(static_cast<T&& &&>(para));
}
Copy after login

应用引用折叠,就是:

void func(T&& para){
 
  func(static_cast<T&&>(para));
 
}
Copy after login

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

所以最终版本

template<typename T>
 
void cover(T&& para){
 
  func(forward(forward<T>(para)));
 
}
Copy after login

std::forward的实现与static_cast(para)稍有不同

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

总结

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

更多浅析C++11中的右值引用、转移语义和完美转发相关文章请关注PHP中文网!

source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template