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!