Introduction of of ravalue reference rvalue reference in C++ standard

  • 2020-04-01 21:24:39
  • OfStack

1. Rvalue references are introduced in the background
The loss of efficiency caused by the generation and copy of temporary objects has always been a problem for people in C++. However, the C++ standard allows the compiler to have full freedom in the generation of temporary objects, thus developing compiler optimization techniques such as CopyElision, RVO (including NRVO), which can prevent the generation and copy of temporary objects in some cases. The following is a brief introduction to CopyElision, RVO, not interested in this can be directly skipped:
(1) CopyElision
CopyElision technology is to prevent some unnecessary temporary object generation and copy, such as:
 
structA{ 
A(int){} 
A(constA&){} 
}; 
Aa=42; 

Theoretically, the above Aa=42; The statement takes three steps: the first step constructs A temporary object of type A by 42, the second step constructs A by copying the temporary object as an argument, and the third step destructs the temporary object. If A is A large class, its temporary object construction and destruction will cause A large memory overhead. We only need one object a, so why not just construct a with 42 as an argument? CopyElision technology does just that.
【 description 】 : You can add A print statement to the copy constructor of A to see if it is called. If it is not called, congratulations, your compiler supports CopyElision. However, it is important to note that although the copy constructor of A is not called, its implementation cannot be without access rights. If you try to put it in the private permission, the compiler will surely report an error.
(2) ReturnValueOptimization (RVO, ReturnValueOptimization)
Return value optimization is also used to prevent some unnecessary temporary objects from being generated and copied, such as:
 
structA{ 
A(int){} 
A(constA&){} 
}; 
Aget(){returnA(1);} 
Aa=get(); 

Theoretically, the above Aa=get(); The statements are executed separately: first create a temporary object in the get() function (assumed to be tmp1), then copy the return value with tmp1 as an argument (assumed to be tmp2), and finally copy a with tmp2 as an argument, with destruction of tmp1 and tmp2. If A is A large class, its temporary object construction and destruction will cause A large memory overhead. The return value optimization technique is used to solve this problem. It can avoid the generation and copy of tmp1 and tmp2 temporary objects.
A) you can add a print statement to the copy constructor of a to see if it is called. If it is not called, congratulations, your compiler supports return value optimization. However, it is important to note that although the copy constructor of A is not called, its implementation cannot be without access rights. If you try to put it in the private permission, the compiler will surely report an error.
B) in addition to the return value optimization, you may have heard of one called named return value optimization (NamedReturnValueOptimization NRVO) optimization techniques, from the programmer's point of view, it is the same logic with RVO. However, its temporary object is identified by a variable name. For example, if the above get() function is modified as:
 
Aget(){ 
Atmp(1);//#1 
//dosomething 
returntmp; 
} 
Aa=get();//#2 

How many object constructs are there for type A after the above modification? Although #1 looks like it has a display construct and #2 looks like it has a display construct, if your compiler supports NRVO and CopyElision, you will find that the whole Aa=get(); The statement is executed with only one construction of the A object. If you print the address of the TMP variable before the get() return statement, Aa=get(); After the statement, print the address of a, you will find the two addresses are the same, this is the result of NRVO technology.
(3) CopyElision, RVO inevitable temporary object generation and copy
While technologies such as CopyElision and NVO (including NRVO) can prevent the creation and copying of temporary objects, they cannot work in certain situations, such as:
 
template<typenameT> 
voidswap(T&a,T&b){ 
Ttmp(a); 
a=b; 
b=tmp; 
} 

We just want to exchange the data held by the two objects a and b, but we have to back up one of them with a temporary object TMP. If the t-type object has data that points (or references) to be allocated from the heap memory, then the memory overhead of the deep copy is imaginable. For this reason, the C++11 standard introduces the right value reference, which can make the copy of temporary objects have the move semantics, so that the copy of temporary objects can have the efficiency of shallow copy, so that to a certain extent to solve the efficiency loss caused by the deep copy of temporary objects.

2. Left and right values in C++03 standard
To understand an rvalue reference, you first need to distinguish between an lvalue and an rvalue.
The C++03 standard divides expressions into left and right values, and "either left or right" :
Everyexpressioniseitheranlvalueoranrvalue.
The easiest way to tell if an expression is left or right is to see if you can address it: if you can, it is left; Otherwise, it's an rvalue.
[note] : due to the introduction of the rvalue reference, the classification of expressions in the C++11 standard is no longer as simple as "either left or right". However, for the sake of simple understanding, we only need to distinguish the rvalue temporarily. The classification in the C++11 standard will be described later.

3. Binding rules for rvalue references
Rvaluereference (rvaluereference, &&) is similar to the traditional reference (reference, &), which is also called lvaluereference (lvaluereference) in order to better distinguish the two. Here is a brief summary of the binding rules for lvalued and rvalued references (with the exception of function type objects) :
(1) non-const lvalue references can only be bound to non-const lvalues;
(2) const lvalue reference can be bound to const lvalue, non-const lvalue, const right value, non-const right value;
(3) non-const rvalue references can only be bound to non-const rvalue;
(4) const right value reference can be bound to const right value and non-const right value.
The test examples are as follows:
 
structA{A(){}}; 
Alvalue;//Non-const lvalue object
constAconst_lvalue;//Const lvalue object
Arvalue(){returnA();}//Returns a non-const rvalue object
constAconst_rvalue(){returnA();}//Returns a const rvalue object
//Rule 1: non-const lvalue references can only be bound to non-const lvalues
A&lvalue_reference1=lvalue;//ok 
A&lvalue_reference2=const_lvalue;//error 
A&lvalue_reference3=rvalue();//error 
A&lvalue_reference4=const_rvalue();//error 
//Rule 2: const left value references can be bound to const left value, non-const left value, const right value, and non-const right value
constA&const_lvalue_reference1=lvalue;//ok 
constA&const_lvalue_reference2=const_lvalue;//ok 
constA&const_lvalue_reference3=rvalue();//ok 
constA&const_lvalue_reference4=const_rvalue();//ok 
//Rule 3: a non-const rvalue reference can only be bound to a non-const rvalue
A&&rvalue_reference1=lvalue;//error 
A&&rvalue_reference2=const_lvalue;//error 
A&&rvalue_reference3=rvalue();//ok 
A&&rvalue_reference4=const_rvalue();//error 
//Rule 4: const rvalue references can be bound to const rvalues and non-const rvalues, not to lvalues
constA&&const_rvalue_reference1=lvalue;//error 
constA&&const_rvalue_reference2=const_lvalue;//error 
constA&&const_rvalue_reference3=rvalue();//ok 
constA&&const_rvalue_reference4=const_rvalue();//ok 
//Rule 5: exceptions to function types
voidfun(){} 
typedefdecltype(fun)FUN;//typedefvoidFUN(); 
FUN&lvalue_reference_to_fun=fun;//ok 
constFUN&const_lvalue_reference_to_fun=fun;//ok 
FUN&&rvalue_reference_to_fun=fun;//ok 
constFUN&&const_rvalue_reference_to_fun=fun;//ok 

【 description 】 : (1) some compilers that support rvalue references but have a lower version may allow rvalue references to be bound to lvalues, such as g++4.4.4 is allowed, but g++4.6.3 is not allowed, clang++3.2 is not allowed, it is said that VS2010beta is allowed, the official version is not allowed, I do not have VS2010 environment, not tested.
(2) the rvalue reference is bound to a literal constant, as in int&&rr=123; Although the literal 123 here is called a constant, it is of type int, not constint. Section 4.4.1 of the C++03 standard document and its footnotes are as follows:
IfTisanon - classtype thetypeofthervalueisthecv - unqualifiedversionofT.
InC++ classrvaluescanhavecv - qualifiedtypes (becausetheyareobjects). ThisdiffersfromISOC, inwhichnon lvaluesneverhavecv -- qualifiedtypes.
So 123 is non-const right, int&&rr=123; The statement conforms to rule 3 above.

4. Expression classification in C++11 standard
The introduction of right value reference makes the classification of expressions in C++11 standard not so simple as non-left value or right value. The following figure is the classification of expressions in C++11 standard:
The < img border = 0 style = "DISPLAY: block; MARGIN - LEFT: auto; MARGIN - RIGHT: auto "Alt =" "SRC =" / / files.jb51.net/file_images/article/201211/20121119151007131.jpg ">  
The brief explanation is as follows:
(1) lvalue is still an lvalue in the traditional sense;
(2) the xvalue (eXpiringvalue) literal meaning can be understood as the value of the life cycle is coming to an end, it is the value of the expression of some involves rvalue references (Anxvalueistheresultofcertainkindsofexpressionsinvolvingrvaluereferences), for example: a call to a return type as the function of rvalue references of the return value is the xvalue.
(3) the literal meaning of prvalue (purervalue) can be understood as the pure right value, or it can be considered as the traditional right value, such as temporary object and literal value.
(4) the generalized left value of glvalue (generalizedvalue), including the traditional left value and xvalue.
(5) in addition to the traditional right value, rvalue also includes xvalue.
The above lvalue and prvalue are consistent with the concepts of left value and right value respectively in the traditional sense, which is relatively clear. While xvalue is described as "the value of some expressions involving the right value reference", what are some of them? The C++11 standard provides four situations that are explicitly defined as xvalue:
 
[Note:Anexpressionisanxvalueifitis: 
--theresultofcallingafunction,whetherimplicitlyorexplicitly,whosereturntypeisanrvaluereferencetoobjecttype, 
--acasttoanrvaluereferencetoobjecttype, 
--aclassmemberaccessexpressiondesignatinganon-staticdatamemberofnon-referencetypeinwhichtheobjectexpressionisanxvalue,or 
--a.*pointer-to-memberexpressioninwhichthefirstoperandisanxvalueandthesecondoperandisapointertodatamember. 
Ingeneral,theeffectofthisruleisthatnamedrvaluereferencesaretreatedaslvaluesandunnamedrvaluereferencestoobjectsaretreatedasxvalues;rvaluereferencestofunctionsaretreatedaslvalueswhethernamedornot.--endnote] 
[Example: 
structA{ 
intm; 
}; 
A&&operator+(A,A); 
A&&f(); 
Aa; 
A&&ar=static_cast<A&&>(a); 
Theexpressionsf(),f().m,static_cast<A&&>(a),anda+aarexvalues.Theexpressionarisanlvalue. 
--endexample] 

Simply put, a namedrvaluereference (namedrvaluereference) is an lvalue, an unnamed rvaluereference (unamedrvaluereference) is an xvalue, and an rvaluereference of a referenced function type is treated as an lvalue whether it is named or not. Here's an example that's easier to understand:
[/ code]
Arvalue () {returnA (); }
A && rvalue_reference () {returnA (); }
Fun (); // returns an unnamed reference to the right value of xvalue
A && ra1 = rvalue (); //ra1 is a named rvalue application, belonging to the left value
A && in ra2 = ra1; //error, ra1 is treated as an lvalue, so ra2 cannot be bound to ra1 (not conforming to rule 3)
A&la = ra1; //ok, non-const lvalue references can be bound to non-const lvalues (conforming to rule 1)
 
5 , move semantic  
 Now, we're back 1-(3) , which mentioned move Semantics, so how do you make a copy of the temporary object have move Meaning? Let's take the implementation of a class as an example:  
[code] 
classA{ 
public: 
A(constchar*pstr=0){m_data=(pstr!=0?strcpy(newchar[strlen(pstr)+1],pstr):0);} 
//copyconstructor 
A(constA&a){m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0);} 
//copyassigment 
A&operator=(constA&a){ 
if(this!=&a){ 
delete[]m_data; 
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0); 
} 
return*this; 
} 
//moveconstructor 
A(A&&a):m_data(a.m_data){a.m_data=0;} 
//moveassigment 
A&operator=(A&&a){ 
if(this!=&a){ 
m_data=a.m_data; 
a.m_data=0; 
} 
return*this; 
} 
~A(){delete[]m_data;} 
private: 
char*m_data; 
}; 

As you can see from the above example, in addition to traditional copyconstructors and copyassigment, we also added moveconstructor and moveassigment to the a-class implementation. In this way, when we copy A temporary object of class A (right value), we use the move copy constructor with move semantics to avoid strcpy() function calls in the deep copy. When we assign A temporary object of class A (right value) to another object, we use A move semantic move assignment to avoid copying the strcpy() function call in the assignment. This is what we call move semantics.

6, STD ::move() function implementation
Now that we know the move semantics, let's look at the efficiency problem in 1-(3) :
 
template<typenameT>//If T is classA
voidswap(T&a,T&b){ 
Ttmp(a);//According to binding rule 3 of the right value reference, moveconstructor is not called, but copyconstructor is
a=b;//According to binding rule 3 of the rvalue reference, the moveassigment is not called, but copyassigment is
b=tmp;//According to binding rule 3 of the rvalue reference, the moveassigment is not called, but copyassigment is
} 

As you can see from the above example, although we implemented moveconstructor and moveassigment, the swap() function example still USES the traditional copyconstructor and copyassigment. For them to actually use the copy and copy of move semantics, it's time for the STD ::move() function to come in. Here's an example:
 
voidswap(A&a,A&b){ 
Atmp(std::move(a));//STD ::move(a) is the right value, and moveconstructor is called
a=std::move(b);//STD ::move(b) is the right value, where moveassigment is called
b=std::move(tmp);//STD ::move(TMP) is the right value, where moveassigment is called
} 

We can't help but ask: we know that an rvalue reference cannot be bound to an lvalue by binding rules three and four to the rvalue application, but how does the STD ::move() function change the above lvalue a, b, and TMP to an rvalue? This starts with the implementation of the STD ::move() function. In fact, the implementation of the STD ::move() function is very simple. Type_trait > For example:
 
template<class_Tp> 
inlinetypenameremove_reference<_Tp>::type&&move(_Tp&&__t){ 
typedeftypenameremove_reference<_Tp>::type_Up; 
returnstatic_cast<_Up&&>(__t); 
} 

Where the implementation of remove_reference is as follows:
 
template<class_Tp>structremove_reference{typedef_Tptype;}; 
template<class_Tp>structremove_reference<_Tp&>{typedef_Tptype;}; 
template<class_Tp>structremove_reference<_Tp&&>{typedef_Tptype;}; 

As you can see from the implementation of the move() function, the Parameter type of the move() function is an rvalue reference. How can it bind to the lvalues a, b, and TMP as arguments? This still does not conform to the right value of the application of binding rule three! Simply put, if move is just a normal function (not a template function), then it really can't use an lvalue as an argument, based on the binding rules 3 and 4 that apply to the rvalue. But it's a template function, which is different when it comes to template parameter derivation. In section 14.8.2.1 of the C++11 standard document, the derivation of template function parameters is described as follows:
Templateargumentdeductionisdonebycomparingeachfunctiontemplateparametertype (callitP) withthetypeofthecorrespondingargumentofthecall (callitA) asdescribedbelow. (14.8.2.1.1)
IfPisareferencetype, thetypereferredtobyPisusedfortypededuction IfPisanrvaluereferencetoacvunqualifiedtemplateparameterandtheargumentisanlvalue, thetype "lvaluereferencetoA isusedinplaceofAfortypededucti" On. (14.8.2.1.3)
The derivation of template parameters is actually the comparison and matching of parameter and argument. If parameter is a reference type (such as P&), then P is used for type derivation. If the parameter is A cv-unqualified (without const and volatile modification) right-value reference type (such as P&&), and the argument is an lvalue (such as an object of type A), type derivation is done with A& (using A& instead of A).
 
template<class_Tp>voidf(_Tp&&){} 
template<class_Tp>voidg(const_Tp&&){} 
intx=123; 
f(x);//Ok, f() template function parameter is non-const non-volatile right value reference type, argument x is the left value of int, use int& to do parameter derivation, so call f<Int &> (int) &
f(456);//Ok, argument right, f<Int> (int) &&
g(x);//Error, g() function template parameter is the const rvalue reference type, will call g<Int> (constint&&), from rvalue reference rule 4, you know that the const rvalue reference cannot be bound to the lvalue and therefore causes a compilation error

After understanding the derivation process of template function parameters, it is not difficult to understand the implementation of STD ::move() function. When using the lvalue (assuming its type is T) as the parameter to call STD ::move() function, what is actually instantiated and called is STD ::move< T & > (T&), whose return type is T&&, is the process by which the left value of the move() function changes to the right value.
【 description 】 : the father of c + + BjarneStroustrup said: if a move () function was renamed rval () may be better, but the move () this name is already in use for many years (Maybeitwouldhavebeenbetterifmove hadbeencalledrval () (), butbynowmove () hasbeenusedforyears.).

7. Complete example
So far, we have learned a lot about rvalue references, and the following is an example of a complete use of rvalue references to implement move semantics:
 
#include<iostream> 
#include<cstring> 
#definePRINT(msg)do{std::cout<<msg<<std::endl;}while(0) 
template<class_Tp>structremove_reference{typedef_Tptype;}; 
template<class_Tp>structremove_reference<_Tp&>{typedef_Tptype;}; 
template<class_Tp>structremove_reference<_Tp&&>{typedef_Tptype;}; 
template<class_Tp> 
inlinetypenameremove_reference<_Tp>::type&&move(_Tp&&__t){ 
typedeftypenameremove_reference<_Tp>::type_Up; 
returnstatic_cast<_Up&&>(__t); 
} 
classA{ 
public: 
A(constchar*pstr){ 
PRINT("constructor"); 
m_data=(pstr!=0?strcpy(newchar[strlen(pstr)+1],pstr):0); 
} 
A(constA&a){ 
PRINT("copyconstructor"); 
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0); 
} 
A&operator=(constA&a){ 
PRINT("copyassigment"); 
if(this!=&a){ 
delete[]m_data; 
m_data=(a.m_data!=0?strcpy(newchar[strlen(a.m_data)+1],a.m_data):0); 
} 
return*this; 
} 
A(A&&a):m_data(a.m_data){ 
PRINT("moveconstructor"); 
a.m_data=0; 
} 
A&operator=(A&&a){ 
PRINT("moveassigment"); 
if(this!=&a){ 
m_data=a.m_data; 
a.m_data=0; 
} 
return*this; 
} 
~A(){PRINT("destructor");delete[]m_data;} 
private: 
char*m_data; 
}; 
voidswap(A&a,A&b){ 
Atmp(move(a)); 
a=move(b); 
b=move(tmp); 
} 
intmain(intargc,char**argv,char**env){ 
Aa("123"),b("456"); 
swap(a,b); 
return0; 
} 

The output result is:
 
constructor 
constructor 
moveconstructor 
moveassigment 
moveassigment 
destructor 
destructor 
destructor 

8. Behind-the-scenes tidbits
The proposal to introduce rvalue references in the C++ 11 standard was put forward by HowardHinnant. Its original proposal N1377 was put forward in 2002, with numerous modifications to N1385, N1690, N1770, N1855, N1952, and N2118. , including its final version N2118 HowardHinnant proposal use rvalue references directly tied to lvalue examples, and by HowardHinnant, BjarneStroustrup and BronekKozicki 08 October jointly signed the ABriefIntroductiontoRvalueReferences article also have rvalue references directly tied to lvalue example, It is not clear why, but it is not difficult to understand why earlier versions of compilers (such as g++4.4.4) did not report a compilation error when binding an rvalue reference to an lvalue, while newer compilers did.
In addition, HowardHinnant is chairman of the C++ standards committee LibraryWorkingGroup, maintainer of libcxx and libcxxabi, and a senior software engineer at apple.

Related articles: