Modern C++ study notes – lvalue and rvalue

Original link: https://xiaozhou.net/study-notes-of-modern-cpp-lvalues-and-rvalues-2023-09-06.html

The concept of lvalues ​​and rvalues

Lvalues ​​and rvalues ​​are new concepts introduced in Modern C++. in short:

  • The lvalue is on the left side of the equal sign, and we can take the address of the lvalue.
  • The rvalue is located on the right side of the equal sign. It is essentially a value, that is, a literal constant, and we cannot perform an address operation on it.

 1
 int x = 999 ; // x is an lvalue, 999 is an rvalue

We can roughly think of the lvalue as a container, and the rvalue is stored in the container. If there is no such container, the rvalue in it will be invalid. For the following program, we will get a similar error when compiling:

 1
 error: lvalue required as left operand of assignment
 1
2
 int x;
123 = x;

Obviously, what is needed on the left side of the equal sign is an lvalue, and 123, as a literal constant type, cannot act as an lvalue. Similarly, we cannot take the address of an rvalue:

 1
2
 int *x;
x = & 123 ; // cannot take address of rvalue

The compiler reports an error:

 1
 error: lvalue required as unary '&' operand 

Implicit conversion from lvalue to rvalue

An lvalue may be converted to an rvalue in many cases, such as the - operator in C++, which takes two rvalues ​​as parameters and returns the result of the calculation as an rvalue.

 1
2
3
 int x = 10 ;
int y = 5 ;
int z = x - y;

In the above program fragment, we clearly see that x, y are lvalues ​​themselves, but they participate in the subtraction operation as rvalues. How is this done? The answer is that the compiler implicitly converts lvalues , converting x and y to rvalues. The same goes for other multiplication, division, and addition operations in C++.

If lvalues ​​can be converted to rvalues, can rvalues ​​themselves be converted to lvalues? The answer is no .

lvalue reference and rvalue reference

The concept of reference in C++ is to modify the value of the original variable by reference in the program conveniently, and, in the process of calling the method to pass parameters, passing the reference can avoid copying. In general, lvalue references can only point to lvalues, and rvalue references can only point to rvalues. It sounds nonsense, but there are special cases.

lvalue reference

 1
2
3
 int x = 10 ;
int & ref_x = x;
ref_x++;

In the above sample program, we defined an lvalue x and then assigned 10. A reference is then defined, pointing to x . Thus, ref_x becomes a reference to x , which is called an lvalue reference. By manipulating ref_x , we can change the value of x .

If we simplify the above program to:

 1
 int & ref_x = 10 ;

When compiling, we get errors like:

 1
 cannot bind non- const lvalue reference of type 'int&' to an rvalue of type 'int'

Obviously, an lvalue reference can only point to an lvalue, not an rvalue. Yes, from the error message, our method can draw another way of writing:

 1
 const int & ref_x = 10 ;

According to the rules of the compiler, we are allowed to point to rvalues ​​by defining an lvalue of type const. But since this lvalue is defined as const , there is no way to modify the pointed value.

rvalue reference

Rvalue references in C++ are represented by && . With an rvalue reference, the rvalue it points to can be modified.

 1
2
 int && ref_x = 10 ; //Define an rvalue reference
ref_x--; //Modify the rvalue it points to through an rvalue reference

If we try to point an rvalue reference to an lvalue:

 1
2
 int x = 10 ;
int && ref_x = x;

The compiler will also throw a similar error, telling us that an rvalue reference cannot point to an lvalue.

 1
 error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 

The nature of left and right value references

Through a simple example program, we can know the essence of lvalue reference and value reference.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
 void increase ( int && input) {
input++;
}

int main () {
int x = 10 ;

int & ref_a = &x;
int && ref_b = std ::move(x);

increase(x); // compile error, cannot pass lvalue
increase(ref_a); // compile error, cannot pass lvalue reference
increase(ref_b); // compile error, rvalue reference itself is an lvalue

increase( std ::move(a)); // compile passed
increase( std ::move(ref_a)); // compile passed
increase( std ::move(ref_b)); // compile passed

increase( 7 ); //compilation passed, 7 is an rvalue

return 0 ;
}

From the above code example, we can see that the rvalue reference ref_b itself is also an lvalue. When calling increase , it needs to be converted to an rvalue through std::move , so that the compiler will not report an error.

Through the above examples, we can conclude the following rules:

  • The introduction of left and right value references is to avoid copying.
  • Lvalue references usually point to lvalues, and can also point to rvalues ​​by adding const keyword constraints, but rvalues ​​cannot be modified.
  • An rvalue reference is essentially an lvalue, and an rvalue reference usually points to an rvalue, but it can also point to an lvalue through forms such as std::move .

Rvalue references and move semantics

In the previous examples, we have covered operations such as std::move . Rvalue references combined with std::move can usually achieve move semantics, thereby avoiding copying and improving program performance.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 # include <iostream>
# include <vector>

int main () {
std :: vector < std :: string > list ;

std :: string str_a = "Hello" ;
std :: string str_b = "World" ;

list.push_back (str_a);
list.push_back ( std ::move(str_b));

std :: cout << "str_a: " << str_a << std :: endl ;
std :: cout << "str_b: " << str_b << std :: endl ;
std :: cout << "list[0]: " << list [ 0 ] << std :: endl ;
std :: cout << "list[1]: " << list [ 1 ] << std :: endl ;
return 0 ;
}

If we run the above sample program, we will get this program output:

 1
2
3
4
 str_a : Hello
str_b :
list[0] : Hello
list[1] : World

Obviously, when str_a is added to the vector, no move semantics are involved, so the value of str_a is copied into the vector. In the process of adding str_b to the vector, because std::move is used, the value of str_b is移动to the vector. When outputting the value of vector later, you can see that it already contains the values ​​of str_a and str_b . But the value of str_b itself has been偷走.

It should be noted that the name of std::move itself is quite confusing. In fact, its work here is only to convert str_b from an lvalue to an rvalue, and will not actually move resources. If we replace the code that adds str_b with:

 1
 list.push_back ( static_cast < std :: string &&>(str_b));

will achieve the same effect. And the real secret lies in the two different overloaded methods provided by std::vector :

 1
2
 void push_back ( const T& value ) ;
void push_back ( T&& value ) ;

The first overloaded method accepts an lvalue reference. When str_a is passed in, due to the restriction of const keyword, its value will be copied and put into the vector, while the value of str_a itself is not Affected. The second overloaded method accepts an rvalue reference, and the push_back method puts its value into the vector and transfers str_b ownership of the string value World . In this way, when we output its value again, we find that it is already empty.

Perfect forwarding (std::forward)

Perfect Forwarding, the meaning of forwarding is that when a method passes its parameters to another method, not only the parameters themselves are forwarded, but also their attributes (lvalue references keep lvalue references, rvalue references keep rvalue references ).

std::forward actually does type conversion, the difference is that std::move only converts lvalues ​​to rvalues, and std::forward can be converted to lvalues ​​or rvalues.

In std::forward<T>(arg) , if the type T is an lvalue, the parameter arg will be converted to an lvalue, otherwise arg will be converted to an rvalue.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 # include <iostream>
# include <utility>

void target_func ( int & arg) { std :: cout << "lvalue reference" << std :: endl ; }

void target_func ( int && arg) { std :: cout << "rvalue reference" << std :: endl ; }

template < typename T> void forward (T&& arg) {
target_func( std ::forward<T>(arg));
}

int main () {
forward( 5 );
int x = 5 ;
forward(x);
return 0 ;
}

In the above sample program, forward uses a Universal Reference type to accept a parameter, and forwards the parameter to target_func through std::forward . Since this method has two overloads, which accept lvalue references and rvalue references respectively. When we passed in the rvalue 5 and the lvalue x respectively, we found that the forward method can accurately forward the parameters to the corresponding overloaded method. Therefore, the output of the program is respectively:

 1
2
 rvalue reference
lvalue reference

reference article

  1. Understanding the meaning of lvalues ​​and rvalues ​​in C++
  2. Cpp Reference
  3. Perfect Forwarding

This article is transferred from: https://xiaozhou.net/study-notes-of-modern-cpp-lvalues-and-rvalues-2023-09-06.html
This site is only for collection, and the copyright belongs to the original author.