l-values and r-values

7 minutes read

In C++, it's crucial to understand l-values and r-values to grasp the language's basic operations, particularly in memory management. These ideas are key to knowing how expressions are evaluated and how objects are given or moved in a program.

L-values

Every C++ expression is characterized by two main properties:

  • A non-reference type;

  • The value category (main: prvalue, xvalue, lvalue; or mixed: rvalue, glvalue.)

These properties decide if the result of the expression can be assigned to another object.

There is no need to learn all the differences between these categories now. For now, just know the difference between l and r. You can find a detailed description of the other categories on the Microsoft website, but this is rarely needed in daily use.

Let's look at a few examples:

int a = 42; // '42' is of type 'int' and is a prvalue

Here 42 is a literal value of type int. It is a prvalue because it is a temporary value not tied to any memory location.

int b = a; // 'a' is of type 'int' and is an lvalue

In this case, a is an int variable with a definite memory address; therefore, it is an lvalue.

int d = a + b; // The expression 'a + b' is of type 'int' and is an rvalue

The expression a + b produces a temporary value, which is an rvalue. It does not have a specific memory address and is used on the right side of assignment expressions.

An l-value refers to a memory cell that you can find. It points to a specific memory area, letting you take its address.

An l-value might appear on the left side of an assignment (that's why it's called "l-value"). It shows an object in a specific memory spot (like variables). An l-value points to a memory spot you can pinpoint.

In any assignment, the 'lvalue' must be able to store data.

L-values are in two groups:

  • Non-changeable l-values, which are const.

  • Changeable l-values, which you can alter (anything that's not const).

Here are l-value examples:

int x = 5;  // x is an l-value
x = 10;     // OK: x is on the left side of an assignment
int* y = &x; // OK: you can find x's address

In these examples, x is an l-value because it points to a known memory spot.

R-values

The opposite of an l-value is an r-value.

An r-value is the value of data stored at some address in memory. It's typically a temporary object or a value not tied to a memory cell. Think of them as everything but l-values.

Examples include individual literals (such as 13) or expressions (such as 3 + 10).

An r-value is a temporary value that lasts only for the duration of the expression; they are destroyed when the expression ends. An r-value appears on the right side of the assignment operator; it represents a value that you can give to an l-value.

It cannot have anything assigned to it.

⚠️ For example, the statement 1 = 12; will cause a compiler error because 1 is not an l-value. The number 1 does not have its own address in memory, so you cannot assign anything to it.

These are some examples of l-values:

int a = 5 + 3; // 5 + 3 is an r-value
int b = a;     // a is an l-value, but in this context, it's used as an r-value

Here, 5 + 3 is an r-value because it is a temporary result not tied to a memory location.

In the second example, you're not interested in the variable a itself or its address, but in the value it holds. That's why in the expression int b = a;, a acts as an r-value.

R-value References

R-value references, introduced in C++11, are a key feature that enables move semantics, a major enhancement in C++. They allow the efficient transfer of resources from temporary and unnamed objects (r-values) to new objects.

This feature is particularly important for optimizing the performance of C++ applications, especially when dealing with large data structures or resources such as file handles, network connections, or memory allocations.

An r-value reference is declared using &&. It's used to bind to r-values (temporary objects or values that don't have a persistent memory address).

This distinction is critical for move semantics and perfect forwarding. Which we'll talk about in other topics.

Example of R-value References:

void processValue(int& value) { /* ... */ }     // l-value reference
void processValue(int&& value) { /* ... */ }    // r-value reference

processValue(x);  // Calls the l-value version
processValue(5);  // Calls the r-value version

In this example, processValue has two overloads: one that takes an l-value reference and one that takes an r-value reference.

In the case of processValue(x), x is passed directly, and the function can modify the original variable because it has access to its address in memory.

In the case of processValue(5), a temporary value is passed. The function can use this value, but it has no permanent location in memory, and it cannot be changed in the same way as l-value.

Conclusion

L-values and r-values are fundamental concepts in C++, describing the properties of expressions in terms of their memory locations and values. With the introduction of r-value references in C++11, these concepts have become even more important, particularly in the context of optimizing memory usage and improving performance.

These are the basic concepts that we will rely on when further studying modern methods of working with memory, such as move semantics, correct work with links and perfect forwarding.

2 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo