(Translation) Move semantics and right value reference in C ++ 11, move the right value

Source: Internet
Author: User

(Translation) Move semantics and right value reference in C ++ 11, move the right value

Solemnly declare: This article is the author's online translation of the original text, some of which are added to explain, ownership belongs to the original author!

Address: http://www.cprogramming.com/c++11/rvalue-references-And-move-semantics-in-c0000000011.html

C ++ has been committed to generating fast programs. Unfortunately, until C ++ 11, there was always a stubborn issue that reduced the speed of the C ++ program: the creation of temporary variables. Sometimes these temporary variables can be optimized by the compiler (for example, return value optimization), but this is not always feasible, which usually leads to high object replication costs. What did I say?

Let's take a look at the following code:

 1 #include <iostream> 2 #include <vector> 3 using namespace std; 4  5 vector<int> doubleValues (const vector<int>& v) 6 { 7     vector<int> new_values( v.size() ); 8     for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr ) 9     {10         new_values.push_back( 2 * *itr );11     }12     return new_values;13 }14 15 int main()16 {17     vector<int> v;18     for ( int i = 0; i < 2; i++ )19     {20         v.push_back( i );21     }22     v = doubleValues( v );23 }

(Note: The vector <int> doubleValues (const vector <int> & v) function in the code is to multiply the value of vector v by 2, store it in another vector, and return it. If we add the values in the output v of the following code in row 22nd, we will find that the values in v have not changed, and they are all 0.

for (auto x : v)    cout << x << endl;

This should be changed:

 1 #include <iostream> 2 #include <vector> 3 using namespace std; 4  5 vector<int> doubleValues (const vector<int>& v) 6 { 7     vector<int> new_values; 8     for (auto x : v) 9         new_values.push_back(2 * x);    10     return new_values;11 }12 13 int main()14 {15     vector<int> v;16     for ( int i = 0; i < 2; i++ )17     {18         v.push_back( i );19     }20     v = doubleValues( v );21 }

In addition, I do not recommend using vector like the original author, because push_back will change the memory distribution and size of the original vector, and some unexpected errors will occur, and the code is not robust .)

If you have done a lot of high-performance optimization work, I am sorry for the pain this stubborn disease has brought to you. If you haven't done this kind of optimization, well, let's take a look at why such code is a nightmare in C ++ 03 (the next part is to explain why C ++ 11 is better in this regard ). This problem is related to the replication variable. When the doubleValues () function is called, it constructs a temporary vector (new_values) and fills in the data. This is not efficient, but if we want to keep the purity of the original vector, we need another copy. Think about what happened to the return of the doubleValues () function?

All data in new_values must be copied again! Theoretically, there may be a maximum of two replication operations:

1. The returned Temporary Variable occurs;

2. It occurs in v = doubleValues (v); here.

The first copy operation may be automatically optimized by the compiler (that is, the return value is optimized), but the second copy of temporary variables to v is unavoidable, because the memory space needs to be re-allocated and the whole vector needs to be iterated.

The example here may have some minor issues. Of course, you can avoid this problem by using other methods, such as passing a pointer or a filled vector. In fact, both programming methods are reasonable. In addition, the method of returning a pointer requires at least one memory allocation. It is also one of the objective of C ++ to avoid memory allocation.

Worst of all, the value returned by the doubleValues () function is a temporary variable that is no longer needed. When the copy operation is completed in v = doubleValues (v), the results of v = doubleValues (v) are discarded. Theoretically, the entire replication process can be avoided, and only the temporary vector pointer is saved to v. In fact, why don't we move objects? In C ++ 03, no matter whether the object is temporary or not, we have to run the same code in the copy operator = or the copy constructor, no matter where the value comes from, therefore, "pilfering" is impossible. In C ++ 11, this behavior is acceptable!

This is the meaning of the right value and move! When you are using temporary variables that will be discarded, the move semantics can avoid unnecessary copies, and these resources from temporary variables can be used elsewhere. The move semantics is a new feature of C ++ 11 and is calledRight referenceYou also want to understand what benefits this can bring to programmers. First, let's talk about what is the right value, then what is the right value reference, and finally we will go back to the move semantics and see how the right value reference is implemented.

Right and left-strong competitor or friend?

In C ++, there are left and right values. The left value is an expression that can get the address, that is, a memory address locator address-essentially, a left value can provide a semi-permanent memory. We can assign a value to the left value, for example:

1 int a;2 a = 1; // here, a is an lvalue

You can also make the left value not a variable, such:

1 int x;2 int& getRef ()3 {4    return x;5 }6  7 getRef() = 4;

Here, getRef () returns a reference to a global variable, so its return value is a permanent location stored in the memory. You can use getRef () Just like using common variables ().

  If an expression returns a temporary variable, the expression is the right value.. For example:

1 int x;2 int getVal ()3 {4     return x;5 }6 getVal();

Here, getVal () is the right value, because the returned value x is not a reference to the global variable x, but a temporary variable. If we use an object instead of a number, this is a bit interesting, for example:

1 string getName ()2 {3     return "Alex";4 }5 getName();

GetName () returns a string object constructed within the function. You can assign it to the variable:

string name = getName();

Now you are using a temporary variable. getName () is the right value.

Check that the right value of the temporary object referenced by the right value involves the temporary object-like the return value of doubleValues. If we clearly know that the value returned from an expression is temporary and that we know how to compile the method to reload the temporary object, isn't that good? Why is it true. What is a right-value reference is a reference bound to a temporary object!Before C ++ 11, if a temporary object exists, you need to bind it with "regular" or "lvalue reference, but what if the value is const? For example:
1 const string& name = getName(); // ok2 string& name = getName(); // NOT ok

Obviously, you cannot use a "mutable" reference here, because if you do so, you can modify the object to be destroyed, which is quite dangerous. By the way, saving the temporary object in the const reference ensures that the temporary object will not be destroyed immediately. This is a good C ++ programming habit, but it is still a temporary object and cannot be modified.

However, in C ++ 11, a new reference is introduced, that is, "Right Value reference", which allows binding a variable reference to a right value instead of a left value. In other words,The right value reference focuses on checking whether a value is a temporary object.The right value uses the & syntax instead of &. It can be const and non-const, just like the left value reference, although you rarely see the left value reference of const.

1 const string&& name = getName(); // ok2 string&& name = getName(); // also ok - praise be!

So far everything has been running well, but how is this implemented? The most important difference between left and right references is that the left and right values of function parameters are used. Take a look at the following two functions:

1 printReference (const String& str)2 {3     cout << str;4 }5  6 printReference (String&& str)7 {8     cout << str;9 }

The printReference () function is interesting here: printReference (const String & str) accepts any parameter. The left and right values are acceptable, regardless of whether the Left or Right values are variable. PrintReference (String & str) accepts any parameter that can be changed to the right value. In other words, write as follows:

1 string me( "alex" );2 printReference(  me ); // calls the first printReference function, taking an lvalue reference 3 printReference( getName() ); // calls the second printReference function, taking a mutable rvalue reference

Now we should have a way to determine whether to use references for temporary or non-temporary objects. The method of referencing the version with the right value is like a secret backdoor that enters the Club (boring club, I guess). If it is a temporary object, it can only be entered. Since we have a way to determine whether an object is a temporary object, how should we use it?

Move constructor and move value assignment operator

When you use the right value reference, the most common mode is to create the move constructor and the move assignment operator (following the same principle ). The move constructor, like the copy constructor, uses an instance object as a parameter to create a new instance based on the original Instance Object. Then the move constructor can avoid memory allocation, because we know that it has provided a temporary object, instead of copying the entire object, it is just "moving. Suppose we have a simple ArrayWrapper class, as shown below:

 1 class ArrayWrapper 2 { 3     public: 4         ArrayWrapper (int n) 5             : _p_vals( new int[ n ] ) 6             , _size( n ) 7         {} 8         // copy constructor 9         ArrayWrapper (const ArrayWrapper& other)10             : _p_vals( new int[ other._size  ] )11             , _size( other._size )12         {13             for ( int i = 0; i < _size; ++i )14             {15                 _p_vals[ i ] = other._p_vals[ i ];16             }17         }18         ~ArrayWrapper ()19         {20             delete [] _p_vals;21         }22     private:23     int *_p_vals;24     int _size;25 };

Note that the copy and copy constructor allocates memory and copies each element in the array each time. This is a huge workload for replication operations. Let's add the move copy constructor to achieve efficient performance.

 1 class ArrayWrapper 2 { 3 public: 4     // default constructor produces a moderately sized array 5     ArrayWrapper () 6         : _p_vals( new int[ 64 ] ) 7         , _size( 64 ) 8     {} 9  10     ArrayWrapper (int n)11         : _p_vals( new int[ n ] )12         , _size( n )13     {}14  15     // move constructor16     ArrayWrapper (ArrayWrapper&& other)17         : _p_vals( other._p_vals  )18         , _size( other._size )19     {20         other._p_vals = NULL;21     }22  23     // copy constructor24     ArrayWrapper (const ArrayWrapper& other)25         : _p_vals( new int[ other._size  ] )26         , _size( other._size )27     {28         for ( int i = 0; i < _size; ++i )29         {30             _p_vals[ i ] = other._p_vals[ i ];31         }32     }33     ~ArrayWrapper ()34     {35         delete [] _p_vals;36     }37  38 private:39     int *_p_vals;40     int _size;41 };

In fact, the move constructor is simpler than the copy constructor, which is quite good. Pay attention to the following two points:

1. the parameter is a non-const right value reference

2. other. _ p_vals should be set to NULL.

The preceding 2nd points are an explanation of 1st points. If we use the right const value for reference, we cannot set other. _ p_vals to NULL. But why should we set other. _ p_vals to NULL? The reason is that when a temporary object leaves its scope, its destructor will be called just like all other C ++ objects. When the Destructor is called, _ p_vals is released. Here we just copied _ p_vals. If we do not set _ p_vals to NULL, move is not a real "move", but a copy, once we use the released memory, the running will crash.The significance of the move constructor is to avoid copying by changing the original temporary object.

Repeat it again. The purpose of the heavy-duty move constructor is to call the move constructor only when it is a temporary object, and only the temporary object can be modified. This means that if the return value of the function is a const object, the copy constructor will be called instead of the move constructor, so do not write it like this:

1 const ArrayWrapper getArrayWrapper (); // makes the move constructor useless, the temporary is const!

In some cases, we haven't discussed how to move the constructor. For example, a field in the class is also an object. Observe the following class:

 1 class MetaData 2 { 3 public: 4     MetaData (int size, const std::string& name) 5         : _name( name ) 6         , _size( size ) 7     {} 8   9     // copy constructor10     MetaData (const MetaData& other)11         : _name( other._name )12         , _size( other._size )13     {}14  15     // move constructor16     MetaData (MetaData&& other)17         : _name( other._name )18         , _size( other._size )19     {}20  21     std::string getName () const { return _name; }22     int getSize () const { return _size; }23     private:24     std::string _name;25     int _size;26 };

Our array includes the field name and size, so we should change the definition of ArrayWrapper, as follows:

 1 class ArrayWrapper 2 { 3 public: 4     // default constructor produces a moderately sized array 5     ArrayWrapper () 6         : _p_vals( new int[ 64 ] ) 7         , _metadata( 64, "ArrayWrapper" ) 8     {} 9  10     ArrayWrapper (int n)11         : _p_vals( new int[ n ] )12         , _metadata( n, "ArrayWrapper" )13     {}14  15     // move constructor16     ArrayWrapper (ArrayWrapper&& other)17         : _p_vals( other._p_vals  )18         , _metadata( other._metadata )19     {20         other._p_vals = NULL;21     }22  23     // copy constructor24     ArrayWrapper (const ArrayWrapper& other)25         : _p_vals( new int[ other._metadata.getSize() ] )26         , _metadata( other._metadata )27     {28         for ( int i = 0; i < _metadata.getSize(); ++i )29         {30             _p_vals[ i ] = other._p_vals[ i ];31         }32     }33     ~ArrayWrapper ()34     {35         delete [] _p_vals;36     }37 private:38     int *_p_vals;39     MetaData _metadata;40 };

In this way, you can? You only need to call the move constructor of MetaData in ArrayWrapper. Everything is natural, isn't it? The problem is that this cannot be done! The reason is simple: the other in the move constructor references the right value. Here it should be the right value, rather than the right value reference! If the value is left, call the copy constructor instead of the move constructor. It's a little strange, it's a bit difficult, right?-I know. There is a way to differentiate: the right value is an expression that will be destroyed later. When a temporary object is about to be destroyed, we pass it into the move constructor, which is equivalent to giving it a second life and still valid in the new scope. This is the case where the right value appears. In our constructor, the object has a name field, which is always valid within the function. In other words, we can use it multiple times in a function, and the temporary variables defined in the function remain valid within the function. The left value can be located. We can access a left value somewhere in the memory. In fact, we may want to use the function later. If the move structure is called, we have a right value to reference the object, and we can use the "move" object.

1 // move constructor2 ArrayWrapper (ArrayWrapper&& other)3     : _p_vals( other._p_vals  )4     , _metadata( other._metadata )5 {6     // if _metadata( other._metadata ) calls the move constructor, using 7     // other._metadata here would be extremely dangerous!8     other._p_vals = NULL;9 }

In the last case, both the left and right references are left-value expressions. It is not necessary that the Left value reference must be a const bound to the right value, but the right value reference can always be bound to a reference to the right value. Similar to the content pointed to by pointers and pointers. The value used is the right value, but when we use the right value itself, it becomes the left value.

Std: move

So what skills can be used to deal with such a situation? We can use std: move to include it in <utility>. If you want to convert the left value to the right value, you can use std: move. Here, std: move does not move anything, But converts the left value to the right value, you can also call the move constructor. See the following code:

1 #include <utility> // for std::move2 // move constructor3 ArrayWrapper (ArrayWrapper&& other)4      : _p_vals( other._p_vals  )5       , _metadata( std::move( other._metadata ) )6 {7         other._p_vals = NULL;8 }

Similarly, you should modify MetaData:

1 MetaData (MetaData&& other)2     : _name( std::move( other._name ) ) // oh, blissful efficiency3     : _size( other._size )4 {}
Value assignment operator

Like the move constructor, we should also have a move assignment operator, which is written in the same way as the move constructor.

Move constructor and implicit Constructor

As you know, in C ++, as long as you manually declare the constructor, the compiler will no longer generate default constructor for you. This is also true: adding a move constructor to a class requires you to define and declare a default constructor. In addition, declaring the move constructor does not prevent the compiler from generating an implicit copy constructor for you. Declaring the move assignment operator does not prevent the compiler from creating standard assignment operators.

Std: How the move operation works

You may wonder: how to compile a function like std: move? How does the right value reference be converted to the left value reference? Maybe you have already guessed the answer, that is, typecasting. Std: The actual declaration of move is complicated, but its core idea is to reference static_cast to the right. This means that you don't actually need to use move -- but you should do so to better express what you mean. In fact, conversion is necessary and a good thing. This will prevent you from accidentally converting the left value to the right value, because it will lead to unexpected move, which is quite dangerous.You must use std: move (or a conversion) explicitly to convert the left value to the right value reference. the right value reference is not bound to its own left value.

The function returns an explicit right value reference.

When should you write a function that returns a reference to the right value? What does the function mean by returning the right value reference? Does the function that returns an object through a value already have a right value?

First, let's answer the second question: returning an explicit right value reference is different from returning an object through a value (by value. Let's take a look at the following example:

 1 int x; 2   3 int getInt () 4 { 5     return x; 6 } 7   8 int && getRvalueInt () 9 {10     // notice that it's fine to move a primitive type--remember, std::move is just a cast11     return std::move( x );12 }

Obviously, in the first case, although getInt () is actually the right value, the copy operation is still performed on x. We can write an auxiliary function to see:

1 void printAddress (const int& v) // const ref to allow binding to rvalues2 {3     cout << reinterpret_cast<const void*>( & v ) << endl;4 }5  6 printAddress( getInt() ); 7 printAddress( x );

The operation found that the x Addresses printed by the two are significantly different. On the other hand:

1 printAddress( getRvalueInt() ); 2 printAddress( x );

The printed x address is the same, because getRvalueInt () explicitly returns a right value.

Therefore, the reference to the returned right value is obviously different from the reference to the non-returned right value. If you return an existing object instead of a temporary object created in the function (the compiler may optimize the return value for you to avoid the copy operation), this difference is the most obvious.

The question is, do you need to do this. The answer is: Probably not. In most cases, you are most likely to get a left (dangling) Right Value (one case is that the reference exists, but the temporary object it references has been destroyed ). This situation is similar to the left value of the referenced object that does not exist. The right value reference does not always ensure that the object is valid. Returning the right value reference makes this special case meaningful: You have a member function that uses std: move to return fields in the class.

Move semantics and standard library

Back to the first example, we are using a vector, but we have no control over the class vector, and we do not know whether the vector moves the constructor or the move value assignment operator. Fortunately, the Standards Committee has added the move semantics to the standard library, which means you can efficiently return vectors, maps, strings, and any standard library objects you want to return, make full use of the move semantics.

Moving Objects in STL containers

In fact, the standard library is doing better. If you use the move semantics by creating the move constructor and the move assignment operator in your object, when you store these objects in the STL container, STL will automatically use std :: move, make full use of the move semantics to avoid the efficiency of the copy operation.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.