Lvalues & Rvalues
Before discussing std::move
, let’s first understand lvalues and rvalues; otherwise, we won’t know what std::move
is doing. In C++, lvalues (left values) and rvalues (right values) are two fundamental value type concepts that significantly influence the syntax and semantics of variables, expressions, and operations.
On a nutshell: lvalue: On the left side of the =
. rvalue: On the right side of the =
.
Lvalues
Lvalues (locator values) represent a memory location. Lvalues can appear on the left side of an assignment operator, meaning they can be assigned new values. Lvalues are typically named variables or expressions that can take addresses.
Characteristics:
- Can take addresses (using the
&
operator). - Can be assigned new values.
- Have a persistent storage location in memory.
Example
1
2
3
4
5
int x = 10; // x is an lvalue. It can take an address &x and can be assigned a new value x = 20.
int* ptr = &x; // ptr is an lvalue. It can take an address &ptr and can be assigned ptr = &y;
x = 5; // x is an lvalue and can be assigned a new value.
Rvalues
Rvalues (right values) are values that do not represent a specific memory location. Rvalues are usually temporary objects, constants, or the results of expressions. These values cannot take addresses and cannot be assigned new values.
Characteristics:
- Cannot take addresses.
- Usually temporary objects, destroyed after the expression is evaluated.
- Cannot be assigned new values.
Example:
1
2
3
4
5
int y = 10; // 10 is an rvalue, it is a constant.
int z = x + 5; // (x + 5) is an rvalue, it is the result of an expression.
int* p = &x; // p is an lvalue, but &x is an rvalue (address constant).
Lvalue Reference
Nothing special here, if you’ve written C++ before, you’ve likely used it. An lvalue reference binds to an lvalue and can be declared using the & symbol.
1
2
3
int x = 10;
int& ref = x; // ref is an lvalue reference, bound to lvalue x.
Rvalue Reference
This is more interesting, introduced in C++11. An rvalue reference binds to an rvalue and can be declared using the &&
symbol. Rvalue references introduce move semantics and perfect forwarding with std::move
.
1
2
3
4
5
int&& rref = 20; // rref is an rvalue reference, bound to rvalue 20.
int x = 10;
int&& rref2 = x + 5; // rref2 is an rvalue reference, bound to the result of the expression (x + 5).
Direct Comparison Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <utility> // std::move
void printValue(const int& lref) {
std::cout << "Lvalue reference: " << lref << std::endl;
}
void printValue(const int&& rref) {
std::cout << "Rvalue reference: " << rref << std::endl;
}
int main() {
int x = 10; // x is an lvalue
printValue(x); // Pass lvalue reference
printValue(20); // Pass rvalue reference
printValue(std::move(x)); // Convert lvalue x to rvalue reference
auto temp = std::move(x);
printValue(temp); // temp is still an lvalue
return 0;
}
Result:
In this example, the printValue function has two overloads, one accepting an lvalue reference and the other an rvalue reference. Depending on whether the argument is an lvalue or rvalue, the corresponding function is called. std::move
temporarily converts an lvalue to an rvalue, so the above temp is still an lvalue.
You might not see the use case for std::move
yet. Don’t worry, the next article will explain it in detail.