lvalues and rvalues in C++
If you've done any amount of programming in C++, it's inevitable that you've heard the term lvalue
or rvalue
get thrown around. Whether it's from long, wordy compiler errors or just inside the cryptic language documentation, it's difficult to avoid these terms. However, understanding what they mean can be a bit confusing, so let's see if we can dissect this strange beast.
Definitions
First we'll start with the formal definition of these types. According to the ISOCPP standard, an lvalue
is a value which "designates a function or an object." An rvalue
is "a temporary object [...] or subobject thereof, or a value that is not associated with an object." Confused yet? In simpler (and not completely accurate) terms, we can say this:
- An
lvalue
is something which contains a value. - An
rvalue
is something which is a value.
Of course, even that can be kind of cryptic, so let's break each down with some examples and ways to keep them straight in your head.
LVALUE
Take an assignment operation such as this:
int my_number = 27;
A handy rule of thumb is to remember that lvalues
are the "left values" in assignment statements. I remember them as "lookup values" - they are not a value directly, but rather help you locate the value they represent. In the above example, my_number
is not a value. It is a variable which you can use to then look up the value 27
.
RVALUE
An rvalue
is anything that is not an lvalue
. Another way to think of it is that it is the "right value" in assignment expressions. I like to recall them as "real values." Unlike an lvalue
, an rvalue
does not need to be looked up first, since they are the value. They can also be the result of an expression which produces a value (such as 5 * 3
) but more on that later.
Examples and Clarifications
Let's take a look at some specific code examples to gain better clarity on what is and isn't each type of value.
Simple Statements
std::string simple_string = "Hello!"
In this case, the simple_string
variable is an object that contains the value Hello!
. The variable simple_string
is an lvalue
and the value on the right hand side, "Hello!"
is an rvalue
. This is the most basic of statements and simplest way to show these value types in action.
Calculated Values
int first_num = 10;
int second_num = 5;
int product = first_num * second_num;
This example is a bit more interesting. The first two expressions are pretty standard. The final expression, where we create the product, has something notable. Although first_num
and second_num
are both lvalues
, the operation itself is still an rvalue
. Logically, this makes sense because otherwise you could do weird things like this:
int first_num = 10;
int second_num = 5;
// Compiler Error!
int product = &(first_num * second_num);
This will fail, with my compiler complaining "lvalue required as unary '&' operand." Hopefully you now better understand this error message. An rvalue
is a temporary object. The value 50
will be calculated, temporarily stored and then gone forever. Even if you did manage to capture the memory address where 50
was stored, it'd be transient and useless.
int get_product(int first_num, int second_num) {
return first_num * second_num;
}
// Compiler Error!
int product = &get_product(5, 10);
This last example produces the same compiler error, for the same reason. Function calls, and their return values, are transient rvalues
and therefore the memory address of their result cannot be accessed.
Right and Left Exceptions
const string& name = "aarondevelops";
string name_addr = name;
Finally, here's a curious example because it breaks the "left value" and "right value" rules of thumb we mentioned earlier. Notice the first expression, we have a const string address lvalue
initialized called name
. But then later, the name
variable is used on the right side of the expression. The name
variable is still an lvalue
, even thought it is on the right side of the expression!
You may have also noticed the const
declaration. Without this, the compiler will complain that you "cannot bind non-const lvalue reference of type 'std::string&' to an rvalue." What this is saying in layman's terms is that you can't (and shouldn't) store an address reference to an rvalue
. However, if the value is const
than the compiler can convert the rvalue
to an lvalue
during compile-time. In other words, it permanently stores the value "aarondevelops"
(as it is a constant value) and then allows you to capture the address of that value. Neat!