View the right value reference from four lines of code, and the right value reference from four lines of code
View overview of right value reference from four lines of code
The concept of right value reference may be unfamiliar to some readers. In fact, it is similar to the reference of the Left value in C ++ 98/03. For example, the reference of the Left value in c ++ 98/03 is as follows:
int i = 0;int& j = i;
Here, int & is bound to the left value (but int & cannot be bound to the right value). Correspondingly, the reference to binding the right value is the right value reference, the syntax is as follows: A &. Double quotation marks are used to indicate the right value of the binding type as. Through &, we can easily bind the right value. For example, we can bind a right value as follows:
int&& i = 0;
Here we bind a right value 0. The concept of the right value will be described later. Right-value reference is an important feature added in C ++ 11. It is mainly used to solve two problems encountered in C ++ 98/03, the first problem is that temporary objects do not require expensive copy operations. The second problem is how to forward the temporary objects according to the actual type of parameters in the template function. By introducing the right value reference, the two problems are well solved, and the program performance is improved. We will introduce in detail how the right value reference solves these two problems.
There are many concepts related to right value reference, such as right value, pure right value, dead value, universal references, reference folding, moving semantics, move semantics, and perfect forwarding. Many of them are new concepts. For beginners who have just learned C ++ 11's reference to the right value, they may feel that the reference to the right value is too complicated and it is difficult to clarify the relationship between concepts.
Right-value reference is actually not that complicated. It is actually a story about four lines of code. With four lines of code, we can clearly understand the concept of right-value reference. This article hopes to guide readers through four lines of code to understand the concept of right value reference and clarify the relationship between them, in the end, we can thoroughly master the new feature of C ++ 11-Right Value reference.
Four lines of code: 1st lines of code
int i = getVar();
The above line of code is very simple. Get an integer value from the getVar () function. However, how many types of values will this line of code generate? The answer is that two types of values are generated. One is the left value I, and the other is the temporary value returned by the function getVar (). The temporary value is destroyed after the expression ends, the left value I still exists after the expression ends. This temporary value is the right value. Specifically, it is a pure right value, and the right value is not named. A simple way to distinguish between the left and right values is to see if the address can be obtained for the expression. If yes, it is the left value; otherwise, it is the right value.
All named variables or objects are left values, while anonymous variables are right values. For example, a simple assignment statement:
int i = 0;
In this statement, I is the left value, and 0 is the literal value, that is, the right value. In the above code, I can be referenced, and 0 won't. Specifically, the 0 on the right of the medium expression above is a pure right value (prvalue ), all values in C ++ 11 must belong to the left value, the dead value, and the pure right value. For example, non-referenced temporary variables, temporary variables generated by arithmetic expressions, original literal quantities, and lambda expressions are all pure right values. The dead value is a new expression in C ++ 11 related to the right value reference, such as the object to be moved, the return value of T & function, std :: move return values and return values of conversion functions converted to the T & type. We will introduce the dead value later. Let's take a look at the following code:
int j = 5;auto f = []{return 5;};
In the code above, 5 is an original literal, and [] {return 5;} is a lambda expression, which belongs to the pure right value, their feature is that they are destroyed after the expression ends.
Through the line of code, we have a preliminary understanding of the right value and know what the right value is. Next let's take a look at the second line of code.
2nd lines of code
T&& k = getVar();
The second line of code is very similar to the first line of code, but it is more "&" than the first line of code, and it is the reference of the right value, we know that the Left value reference refers to the reference of the Left value. Accordingly, the reference to the right value is the reference of the right value, and the right value is an anonymous variable, we can only obtain the right value through reference. Although the second line of code does not seem very different from the first line of code, the semantics is actually quite different. Here, the temporary value generated by getVar () is not as the first line of code, after the expression ends, it will be destroyed. Instead, it will be "continued", and its lifecycle will be extended through the right value reference, as long as the Declaration cycle of variable k.
First feature of right value reference
Through the Declaration of the right value reference, the right value is "re-generated", and its lifecycle is as long as the right value references the type variable, as long as the variable is still alive, the temporary amount of the right value will survive. Let's take a simple example to see the lifecycle of the right value. As shown in code list 1-1.
Code List 1-1
#include <iostream>using namespace std;int g_constructCount=0;int g_copyConstructCount=0;int g_destructCount=0;struct A{ A(){ cout<<"construct: "<<++g_constructCount<<endl; } A(const A& a) { cout<<"copy construct: "<<++g_copyConstructCount <<endl; } ~A() { cout<<"destruct: "<<++g_destructCount<<endl; }};A GetA(){ return A();}int main() { A a = GetA(); return 0;}
To clearly observe the temporary values, set the compilation option-fno-elide-constructors during compilation to disable the return value optimization effect.
Output result:
construct: 1copy construct: 1destruct: 1copy construct: 2destruct: 2destruct: 3
As shown in the preceding example, the copy constructor is called twice without the return value optimization () the objects created in the function are returned to construct a temporary object, and the other is generated by constructing object a in the main function. The second destruct is because the temporary object is destroyed after object a is constructed. If the return value optimization is enabled, the output result is:
Construct: 1
Destruct: 1
We can see that the return value optimization will optimize the temporary object, but this is not the c ++ standard, it is the optimization rule of each compiler. We can extend the life cycle of the temporary right value by referencing the right value. If we bind the function return value through the right value reference in the above Code, what are the results? Set the compilation option-fno-elide-constructors during compilation.
Int main () {A & a = GetA (); return 0;} output result: construct: 1 copy construct: 1 destruct: 1 destruct: 2
The right-value reference is less than the previous copy structure and analysis structure because the right-value reference is bound with the right value, which prolongs the life cycle of the temporary right value. We can use this feature to make some performance optimization, that is, to avoid the temporary object copy structure and structure. In fact, in c ++ 98/03, the constant left value reference is often used for performance optimization. The code above is changed:
Const A & a = GetA ();
The output result is the same as the reference to the right value, because the constant left value reference is a "omnipotent" reference type and can accept the left value, right value, constant left value, and constant right value. Note that normal left value references cannot accept the right value. For example, this method is incorrect:
A & a = GetA ();
The above code will report a compilation error, because the left value reference can only accept the left value.
The second feature of right value reference
The right value reference is independent of the left and right values. This means that the variables of the right value reference type may be left or right. For example:
int&& var1 = 1;
Var1 is a reference of the right value, but var1 itself is a left value, because all the named variables are left values.
An interesting question about right value reference is: T & what is it, must it be the right value? Let's take a look at the following example:
Template <typename T> void f (T & t); f (10); // t is the right value of int x = 10; f (x ); // t is the left Value
From the code above, we can see that the value type T & indicates is uncertain. It may be the left value or the right value, which seems a bit strange, this is a feature of right value reference.
Third feature of right value reference
T & t when automatic type inference occurs, it is an undefined reference type (universal references). If it is initialized by a left value, it is a left value; if it is initialized by a right value, it is a right value. Whether it is a left value or a right value depends on its initialization.
Let's look back at the code above. For the function template <typename T> void f (T & t), when the parameter is set to the right value of 10, according to the characteristics of the universal references, t is initialized by a right value, and t is the right value. When the parameter is set to the left value x, t is initialized by a left value reference, and t is a left value. Note that T & is the universal references only when automatic type derivation (such as automatic type derivation of function templates or auto keyword) occurs. Let's take a look at the following example:
template<typename T>void f(T&& param); template<typename T>class Test { Test(Test&& rhs); };
In the preceding example, param is a universal reference, and rhs is a Test & right reference. Because the template function f has a type inference, and Test & has no type derivation, because Test & is a definite type.
It is precisely because the right value reference may be the left value or the right value, depending on initialization, and it is not just determined at once. We can use this to make many articles, for example, the mobile semantics and perfect forwarding described later.
Here, we will mention reference folding. It is precisely because the right value reference is introduced that there may be folding between the left value reference and the right value reference, C ++ 11 determines the rule for referencing and folding. The rule is as follows:
- All the right-value references are superimposed on the right-value reference and still a right-value reference;
- The overlay of all other reference types will change to the left reference.
3rd lines of code
T(T&& a) : m_val(val){ a.m_val=nullptr; }
This line of code actually comes from a class constructor. A parameter of the constructor is a reference to the right value. Why does the right value reference the constructor parameter? Before answering this question, let's take a look at an example. As shown in code 1-2.
Code List 1-2
Class A {public: A (): m_ptr (new int (0) {cout <"construct" <endl;} A (const A & ): m_ptr (new int (*. m_ptr) // copy the constructor {cout <"copy construct" <endl ;}~ A () {delete m_ptr;} private: int * m_ptr;}; int main () {A = GetA (); return 0;} output: constructcopy construct
This example is very simple. A class with heap memory must provide a deep copy constructor, because the default copy constructor is a shortest copy function, which causes a "pointer suspension" problem. If you do not provide a copy constructor for deep copy, the above test code will be incorrect (the compilation option-fno-elide-constructors), and the internal m_ptr will be deleted twice, a temporary right-value destructor is deleted once, and an external constructed a object is released once. The m_ptr of these two objects is the same pointer, this is the so-called pointer suspension problem. Although the copy constructor that provides deep copy can ensure correctness, it may cause extra performance loss in some cases, because sometimes this deep copy is unnecessary. For example, the following code:
In the above Code, the GetA function will return a temporary variable, and then a new object a is constructed through the Temporary Variable copy. The temporary variable is destroyed after the copy construction is complete, if the heap memory is large, the copy structure costs a lot, resulting in extra performance loss. Every time temporary variables are generated and causing extra performance losses, is there a way to avoid performance loss caused by temporary variables? The answer is yes. C ++ 11 has a solution. Let's take a look at the following code. As shown in code 1-3.
Code List 1-3
Class A {public: A (): m_ptr (new int (0) {} A (const A & a): m_ptr (new int (*. m_ptr) // The copy constructor {cout <"copy construct" <endl;} A (A & a): m_ptr (. m_ptr) {. m_ptr = nullptr; cout <"move construct" <endl ;}~ A () {delete m_ptr;} private: int * m_ptr;}; int main () {A = Get (false);} output: constructmove construct
The code listing 1-3 and 1-2 have only one constructor. The output shows that the copy constructor is not called and only the move construct function is called. Let's take a look at this move construct function:
A(A&& a) :m_ptr(a.m_ptr){ a.m_ptr = nullptr; cout << "move construct" << endl;}
This constructor does not make a deep copy. It only transfers the pointer owner to another object, and sets the pointer of parameter object a to null. This is only a small copy. Therefore, this constructor avoids the deep copy of temporary variables.
The above function is actually A mobile constructor. Its parameter is of the right value reference type. Here, A & indicates the right value. Why? As mentioned above, there is no type inference here, which is a definite right value reference type. Why does the constructor match? Because this constructor can only accept the right value parameter, and the return value of the function is the right value, it will match this constructor. Here A & can be seen as the identifier of A temporary value. For A temporary value, we only need to make A shortest copy, without the need to make A deep copy, this solves the performance loss caused by the Temporary Variable copy structure mentioned above. This is the so-called mobile semantics. An important role of right-value reference is to support mobile semantics.
Note that when we provide a mobile constructor, we also provide a copy constructor to prevent the copy constructor from being able to copy the constructor when the movement fails, make our code safer.
We know that the moving semantics matches the temporary value through the right value reference. So, can normal left values use the moving semantics to optimize performance? What should we do? In fact, C ++ 11 provides the std: move method to convert the left value to the right value to facilitate the application of the moving semantics. Moving is to transfer the ownership of object resources from one object to another. It is only a transfer without memory copy. This is the so-called move semantics. 1-1 shows the difference between deep copy and move.
Figure 1-1 differences between deep copy and move
Let's take a look at the following example:
{Std: list <std: string> tokens; // Initialization is omitted... std: list <std: string> t = tokens; // There is a copy here} std: list <std: string> tokens; std: list <std :: string> t = std: move (tokens); // No copy is made here.
If std: move is not required, the copy cost is high and the performance is low. There is almost no price to use move, but the ownership of the resource is converted. He actually changes the left value to the right value reference, and then applies the mobile semantics to call the mobile constructor, which avoids copying and improves program performance. If an object has a large internal memory or dynamic array, it is necessary to write the copy constructor and value assignment function of the move semantics to avoid unnecessary deep copies to improve performance. In fact, all the containers in C ++ 11 have implemented mobile semantics to facilitate our performance optimization.
Here, we also need to pay attention to the misunderstanding of the move semantics. In fact, move cannot move anything. Its only function is to forcibly convert a left value into a right value reference. For some basic types such as int and char [10] fixed-length arrays, the move statement will still be copied (because there is no corresponding moving constructor ). Therefore, move is more meaningful for objects with resources (heap memory or handle.
4th lines of code
template <typename T>void f(T&& val){ foo(std::forward<T>(val)); }
When calling the template function before C ++ 11, there is a headache, and how to pass parameters correctly. For example:
Template <typename T> void forwardValue (T & val) {processValue (val ); // The right value parameter is changed to the left value} template <typename T> void forwardValue (const T & val) {processValue (val); // The parameter is changed to the constant left value reference}
Cannot forward according to the original type of the parameter.
C ++ 11 introduces perfect forwarding: In the function template, it completely follows the parameter type of the template (that is, it maintains the left and right features of the parameter ), pass the parameter to another function called in the function template. Std: forward in C ++ 11 does this. It forwards data according to the actual type of the parameter. See the following example:
Void processValue (int & a) {cout <"lvalue" <endl;} void processValue (int & a) {cout <"rvalue" <endl ;} template <typename T> void forwardValue (T & val) {processValue (std: forward <T> (val); // forward based on the original type of the parameter .} Void Testdelcl () {int I = 0; forwardValue (I); // input the left value forwardValue (0); // input the right value} output: lvaue rvalue
The right value reference T & is a universal references that can accept the Left or Right value. This feature makes it suitable for routing as a parameter, and then uses std :: forward matches the corresponding overload function according to the actual type of the parameter to achieve perfect forwarding.
We can combine perfect forwarding and mobile semantics to implement a generic factory function, which can create all types of objects. The specific implementation is as follows:
template<typename… Args>T* Instance(Args&&… args){ return new T(std::forward<Args >(args)…);}
The parameter of this factory function is of the right value reference type. Internally, std: forward is used for forwarding according to the actual type of the parameter. If the actual type of the parameter is the right value, the structure will be automatically matched during creation. If it is a left value
Summary
With four lines of code, we know what the right value and right value reference are and some features of the right value reference. With these features, we can easily implement Mobile semantics and perfect forwarding. C ++ 11 optimizes the performance by introducing the right-value reference. Specifically, it avoids unnecessary copying through moving semantics, move semantics is used to transfer resources in the temporarily generated left value to another object without any cost. Perfect Forwarding is used to solve the problem that the resource cannot be forwarded according to the actual parameter type (at the same time, one benefit of perfect Forwarding is that it can implement Mobile semantics ).
This article was published in programmer January 2015. Indicate the source for reprinting.
Postscript:The content of this article mainly comes from one of my internal training courses. Many people do not know or understand the reference to the right value of C ++ 11, so I think it is necessary to share it with you, let more people see it, And I sorted it out and sent it to the programmer magazine. I believe that the readers will have a comprehensive and in-depth understanding of the right value reference after reading it.