Mobile semantics, perfect forwarding

C++11 adds a new right value reference. When it comes to right value reference, it can be extended to some related concepts:

  • Lvalue
  • Right value
  • Pure right value
  • Will die
  • lvalue reference
  • rvalue reference
  • Mobile semantics
  • Perfect forwarding
  • Return value optimization

1, Left value and right value

Concept 1

Lvalue: things that can be put to the left of parentheses are called lvalues

Right value: things that cannot be put to the left of parentheses are called right values

Concept 2

Lvalue: things that can take an address and have a name are lvalues

R-value: something without a name that cannot take an address is an R-value

give an example:

1 int a = b + c;

A is an lvalue with variable name. You can take the address or put it to the left of the equal sign. The return value of expression b+c is an R-value. It has no name and cannot take an address. & (a + b) cannot be compiled and cannot be placed to the left of the equal sign.

1 int a= 4; //a Is an lvalue, and 4 as a normal literal is an lvalue

Lvalues generally include:

  • Function name and variable name
  • Function call that returns an lvalue reference
  • Pre self increasing and decreasing expressions + + i, -- i
  • Expressions connected by assignment expressions or assignment operators (a=b, a+=b, etc.)
  • Dereference expression * p
  • String literal "abcd"

2, Pure right value, dead value

Both pure right value and dead value belong to right value.

Pure right value

The temporary variables generated by the operation expression, the original literal quantity not associated with the object, the temporary variables returned by non reference, and the lambda expression are all pure right values.

give an example:

  • Literals other than string literals
  • Returns a function call of a non reference type
  • Post self increasing and decreasing expressions i + +, i--
  • Arithmetic expression (a+b, a*b, a & & B, a==b, etc.)
  • Get address expression, etc. & A

Will die

Dead value refers to the new expression related to right value reference in C++11. It usually refers to the object to be moved, the return value of T & & function, the return value of std::move function, and the return value of conversion to T & & type conversion function. The dead value can be understood as the value to be destroyed. The value obtained by "stealing" the memory space of other variables can avoid the release and allocation of memory space and prolong the life cycle of variable value when ensuring that other variables are no longer used or about to be destroyed. It is often used to complete the special task of moving construction or moving assignment.

give an example:

1 class A {
2     xxx;
3 };
4 A a;
5 auto c = std::move(a); // c Is going to die
6 auto d = static_cast<A&&>(a); // d Is going to die

III. left value reference and right value reference

According to the name, you can guess the meaning. Lvalue reference is the type of reference to lvalue, and lvalue reference is the type of reference to lvalue. They are all references, an alias of the object, and do not have the heap memory of the bound object, so they must be initialized immediately.

1 type &name = exp; // lvalue reference 
2 type &&name = exp; // rvalue reference 

lvalue reference

1 int a = 5;
2 int &b = a; // b Is an lvalue reference
3 b = 4;
4 int &c = 10; // error,10 Unable to get address and reference
5 const int &d = 10; // ok,Because it is a constant reference, it refers to a constant number. This constant number will be stored in memory and the address can be taken

It can be concluded that for an lvalue reference, the value to the right of the equal sign must be addressable. If the address cannot be taken, the compilation will fail, or the const reference form can be used, but in this way, the output can only be read through reference, and the value cannot be modified, because it is a constant reference.

rvalue reference

If you use an R-value reference, the value to the right of the equal sign of the expression needs to be an R-value. You can use the std::move function to forcibly convert an l-value to an R-value.

1 int a = 4;
2 int &&b = a; // error, a Is an lvalue
3 int &&c = std::move(a); // ok

4, Mobile semantics

Deep copy, shallow copy

 1 class A {
 2 public:
 3     A(int size) : size_(size) {
 4         data_ = new int[size];
 5     }
 6     A(){}
 7     A(const A& a) {
 8         size_ = a.size_;
 9         data_ = a.data_;
10         cout << "copy " << endl;
11     }
12     ~A() {
13         delete[] data_;
14     }
15     int *data_;
16     int size_;
17 };
18 int main() {
19     A a(10);
20     A b = a;
21     cout << "b " << b.data_ << endl;
22     cout << "a " << a.data_ << endl;
23     return 0;
24 }

Output:

1 copy
2 b 0x100750b60
3 a 0x100750b60
4 CPP(893,0x1000ffd40) malloc: *** error for object 0x100750b60: pointer being freed was not allocated
5 CPP(893,0x1000ffd40) malloc: *** set a breakpoint in malloc_error_break to debug

In the above code, the two outputs the same address, data of a and b_ The pointer points to the same piece of memory, which is a shallow copy. It is just a simple assignment of data. When data is reconstructed_ Memory will be released twice, causing problems with the program. How to eliminate this hidden danger? You can use the following deep copies:

 1 class A{
 2 public:
 3     A(int size):size_(size){
 4         data_ = new int[size];
 5     }
 6     A(){}
 7     A(const A& a){
 8         size_ = a.size_;
 9         data_ = new int[size_];
10         memcpy(data_, a.data_,size_);
11         std::cout<<"copy"<<std::endl;
12     }
13     ~A(){
14         delete [] data_;
15     }
16     int *data_;
17     int size_;
18 };
19 int main(int argc, const char * argv[]) {
20     // insert code here...
21     A a(10);
22     A b = a;
23     std::cout<<"b "<<b.data_<<std::endl;
24     std::cout<<"a "<<a.data_<<std::endl;
25     return 0;
26 }

Output:

1 copy
2 b 0x1051c65d0
3 a 0x1051c65a0

Deep copy means that when copying an object, if there are pointers pointing to other resources inside the copied object, you need to re open up a new memory resource instead of a simple assignment.

Mobile semantics

It can be understood as the transfer of ownership. The previous copy is to reallocate a piece of memory for other people's resources to copy each other's resources. For mobile semantics, it is similar to the meaning of transfer or resource theft. If you turn that resource into your own, others will no longer own it and will no longer use it. The new mobile semantics in C++11 can save a lot of copy burden. How to use mobile semantics? The answer is by moving the constructor.

 1 class A{
 2 public:
 3     A(int size):size_(size){
 4         data_ = new int[size];
 5     }
 6     A(){}
 7     A(const A& a){
 8         size_ = a.size_;
 9         data_ = new int[size_];
10         memcpy(data_, a.data_,size_);
11         std::cout<<"copy"<<std::endl;
12     }
13     A(A&& a){
14         this->data_ = a.data_;
15         a.data_=nullptr;
16         std::cout<<"move "<<endl;
17     }
18     ~A(){
19         if(data_ != nullptr)
20         {
21             delete [] data_;
22         }
23     }
24     int *data_;
25     int size_;
26 };
27 int main(int argc, const char * argv[]) {
28     // insert code here...
29     A a(10);
30     A b = a;
31     std::cout<<"a "<<a.data_<<std::endl;
32     A c=std::move(a);//Call the move constructor
33     std::cout<<"b "<<b.data_<<std::endl;
34     std::cout<<"c "<<c.data_<<std::endl;
35     std::cout<<"a "<<a.data_<<std::endl;
36     return 0;
37 }

Output:

1 copy
2 a 0x1007c2630
3 move 
4 b 0x1007c2660
5 c 0x1007c2630
6 a 0x0

If you do not use std::move(), there will be a great copy cost. Using mobile semantics can avoid many useless copies and provide program performance. All STL S in C + + implement mobile semantics, which we use. For example:

1 std::vector<string> vecs;
2 ...
3 std::vector<string> vecm = std::move(vecs); // Save a lot of copies

be careful:

Mobile semantics is only for class objects that implement mobile constructors. Does it have any preferential effect on the basic types such as int and float? It will still be copied because they do not implement mobile constructors such as applications.

Perfect forwarding

Perfect forwarding means that you can write a function template that accepts any argument and forward it to other functions. The objective function will receive exactly the same argument as the forwarding function. The forwarding function argument is an lvalue, the objective function argument is also an lvalue, the forwarding function argument is an lvalue, the objective function argument is a lvalue, and the objective function argument is also a lvalue. How to achieve perfect forwarding? The answer is to use std::forward().

 1 void PrintV(int &t){
 2     std::cout<<"lvalue"<<std::endl;
 3 }
 4 void PrintV(int &&t){
 5     std::cout<<"rvalue"<<std::endl;
 6 }
 7 template<typename T>
 8 void Test(T &&t){
 9     PrintV(t);
10     PrintV(std::forward<T>(t));
11     PrintV(std::move(t));
12 }
13 int main(int argc, const char * argv[]) {
14     // insert code here...
15     Test(1); // lvalue rvalue rvalue
16     int a = 1;
17     Test(a);  // lvalue lvalue rvalue
18     Test(std::forward<int>(a)); // lvalue rvalue rvalue
19     Test(std::forward<int&>(a)); // lvalue lvalue rvalue
20     Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
21     return 0;
22 }

Output:

 1 lvalue
 2 rvalue
 3 rvalue
 4 lvalue
 5 lvalue
 6 rvalue
 7 lvalue
 8 rvalue
 9 rvalue
10 lvalue
11 lvalue
12 rvalue
13 lvalue
14 rvalue
15 rvalue

analysis

  • Test(1): 1 is an R-value. T & & T in the template is a universal reference. The R-value 1 is passed to the test function and becomes an R-value reference. However, when calling PrintV, t becomes an l-value. Because it becomes a variable with a name, lvalue will be printed. When PrintV (STD:: forward < T > (T)), it will be forwarded perfectly according to the original type, so rvalue will be printed. PrintV(std::move(t)) will undoubtedly print rvalue.
  • Test(a): A is an lvalue. T & & T in the template is a universal reference. The lvalue A is passed to the test function and becomes an lvalue reference, so lvalue, lvalue and rvalue are printed
  • Test (STD:: forward < T > (a)): whether the forwarding is a left value or a right value depends on T. If t is a left value, it will be forwarded as a left value, and if t is a right value, it will be forwarded as a right value.

5. Return value optimization

Return value optimization (RVO) is a C + + compilation optimization technology. When a function needs to return an object instance, it will create a temporary object and copy the target object to the temporary object through the copy constructor. Here, the copy constructor and destructor will be called unnecessarily at a cost. Through return value optimization, the C + + standard allows the omission of calling these copy constructors.

When will the compiler optimize the return value?

  • The value type of return is the same as that of the function
  • return is a report object

Example 1

1 std::vector<int> return_vector(void) {
2     std::vector<int> tmp {1,2,3,4,5};
3     return tmp;
4 }
5 std::vector<int> &&rval_ref = return_vector();

RVO will not be triggered. The copy constructs a temporary object, the life cycle of the temporary object and rval_ref binding is equivalent to the following code:

1 const std::vector<int>& rval_ref = return_vector();

Example 2

1 std::vector<int>&& return_vector(void) {
2     std::vector<int> tmp {1,2,3,4,5};
3     return std::move(tmp);
4 }
5 
6 std::vector<int> &&rval_ref = return_vector();

This code will cause a runtime error because rval_ref refers to the tmp being destructed.

Example 3

1 std::vector<int> return_vector(void) {
2     std::vector<int> tmp {1,2,3,4,5};
3     return std::move(tmp);
4 }
5 
6 std::vector<int> &&rval_ref = return_vector();

Like the example, std::move is a temporary object. It is unnecessary and the return value will be ignored.

Best code

1 std::vector<int> return_vector(void) {
2     std::vector<int> tmp {1,2,3,4,5};
3     return tmp;
4 }
5 
6 std::vector<int> rval_ref = return_vector();

This code will trigger RVO, will not copy or move, and will not generate temporary objects.

Tags: C++

Posted by colbyg on Mon, 02 May 2022 11:23:58 +0300