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 |
int 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 |
int *x; |
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 |
int x = 10 ; |
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 |
int x = 10 ; |
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 |
int && ref_x = 10 ; //Define an rvalue reference |
If we try to point an rvalue reference to an lvalue:
1 |
int x = 10 ; |
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 |
void increase ( int && input) { |
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 |
# include <iostream> |
If we run the above sample program, we will get this program output:
1 |
str_a : Hello |
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 |
void push_back ( const 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 |
# include <iostream> |
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 |
rvalue reference |
reference article
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.