Understanding lvalues and rvalues is fundamental in modern C++. These concepts, which originated from the idea of what can appear on the “left” vs “right” side of an assignment, have evolved into a system that enables powerful techniques like move semantics and forwarding.
The evolution of value categories
In early C, the distinction was simple: lvalues could appear on the left side of assignment (x = 5), while rvalues could only appear on the right side. However, as C++ evolved with features like operator overloading, references, and eventually move semantics, this simple binary classification became insufficient.
Modern C++ (C++11 and later) introduced a more nuanced system that distinguishes between different types of expressions based on two key properties:
- Identity: Does the expression have a name or memory address?
- Movability: Can resources be safely “moved” from this expression?
This evolution was driven by the need for better performance, especially when dealing with expensive-to-copy objects like containers holding large amounts of data.
Understanding lvalues
Lvalues (historically “left-hand values”) are expressions that designate objects with a persistent identity.
Key characteristics of lvalues:
- Have a specific memory location (address) that persists
- Can appear on the left side of an assignment
- Persist beyond the expression that creates them
- Can be referenced multiple times with consistent meaning
- Examples: variables, array elements, member variables
Lvalues represent objects that have a stable identity in your program. When you declare a variable like int x = 5;
, you’re creating an lvalue that will exist at a specific memory location until it goes out of scope. This stability allows you to take its address, modify it, and reference it multiple times.
Rvalues
Rvalues (historically “right-hand values”) are expressions that represent temporary values without a persistent identity. They’re typically the result of computations or operations.
Rvalues:
- Are temporary values without a persistent memory address
- Cannot appear on the left side of an assignment (usually)
- Exist only during the evaluation of the expression
- Are candidates for optimization (like move operations)
- Examples: literal values (
42
,"hello"
), function return values, arithmetic expressions (x + y
)
Rvalues represent temporary data that doesn’t need to persist. Understanding this allows the compiler to optimize operations involving these temporaries, potentially avoiding expensive copy operations when the temporary is about to be destroyed anyway.
Let’s examine this code that demonstrates the fundamental differences:
cpp
#include <iostream>
double add(double a, double b) {
return a + b; // Returns an rvalue (temporary result)
}
int main() {
// All of these are lvalues - they have memory addresses
int x{5}; // x has a memory location we can access
int y{10}; // y has a memory location we can access
int z{20}; // z has a memory location we can access
// Working with lvalue addresses
int* ptr = &x; // ✓ Valid - x is an lvalue, has an address
z = (x + y); // ✓ Valid - assigning rvalue result to lvalue z
std::cout << "z : " << z << std::endl;
std::cout << "Address of x: " << &x << std::endl;
// These operations would cause COMPILER ERRORS:
// std::cout << "&(x+y) : " << (&(x+y)) << std::endl;
// Error: (x+y) evaluates to 15, which is a temporary rvalue
// Temporaries don't have addressable memory locations
// int* ptr1 = &(x + y);
// Error: Can't take address of rvalue expression
// int* ptr2 = &add(10.1, 20.2);
// Error: Function return value is a temporary rvalue
// int* ptr3 = &45;
// Error: Literal 45 is an rvalue constant
// Understanding function return values
double result = add(10.1, 20.2);
// What happens here:
// 1. add() is called with arguments 10.1 and 20.2
// 2. Function returns 30.3 as a temporary rvalue
// 3. This temporary value is COPIED into 'result'
// 4. The temporary is then destroyed
// 5. 'result' now contains a copy of the temporary value
std::cout << "result is : " << result << std::endl;
return 0;
}
- Lvalue persistence: Variables like
x
,y
, andz
exist at specific memory locations that persist throughout their scope. You can take their addresses and reference them multiple times. - Rvalue temporaries: Expressions like
(x + y)
oradd(10.1, 20.2)
create temporary values that exist only during the expression evaluation. They have no addressable memory location. - Assignment semantics: When you assign an rvalue to an lvalue (
z = (x + y)
), the temporary result is copied into the lvalue’s memory location.