What Are Lvalues and Rvalues in C++?
Lvalues and rvalues represent the two fundamental value categories in C++ that govern how expressions are evaluated, assigned, and passed between functions. At its core, an lvalue refers to an object with an identifiable memory address that persists beyond a single expression, whereas an rvalue represents a temporary value or literal that typically ceases to exist once an expression completes. Understanding this distinction, along with the references designed to bind to them, unlocks modern C++ features such as move semantics, perfect forwarding, and significant runtime performance optimizations.
Identity vs. Lifetime: The Core Concept
To differentiate lvalues from rvalues, consider two questions: Does
the expression have an identifiable memory address that can be taken
using the address-of operator (&)? Does it persist
across multiple statements?
- Lvalues (Locator Values): Expressions that occupy
identifiable locations in memory. Standard named variables, object data
members, references, and dereferenced pointers are all lvalues. Because
they have a distinct storage address, you can assign new values to them
(unless they are declared
const). - Rvalues (Read Values / Temporary Values): Expressions that represent temporary data generated during calculation, literals (excluding string literals), or function return values that do not return a reference. These values exist transiently in registers or temporary stack locations and do not possess persistent identity.
int x = 42; // 'x' is an lvalue; '42' is an rvalue
int y = x + 5; // 'y' is an lvalue; 'x + 5' produces a temporary rvalue
int* ptr = &x; // Valid: 'x' has an identifiable address
// int* bad = &(x + 5); // Error: cannot take the address of a temporary rvalueLvalue References (&)
An lvalue reference forms an alias to an existing object that already
resides in persistent memory. Declared using a single ampersand
(Type&), an lvalue reference cannot bind directly to an
rvalue unless it is qualified with const.
Non-Const Lvalue References
A non-const lvalue reference requires a modifiable lvalue to bind against. It allows direct mutation of the underlying object without incurring copying overhead.
int a = 10;
int& refA = a; // Valid: binds to lvalue 'a'
refA = 20; // Modifies 'a' directly
// int& invalid = 50; // Compilation Error: cannot bind non-const lvalue reference to an rvalueConst Lvalue References
A const lvalue reference (const Type&)
is unique because the C++ standard allows it to bind to both lvalues and
rvalues. When binding to an rvalue, the compiler extends the lifetime of
the temporary object to match the scope of the reference itself.
const int& safeRef = 100; // Valid: binds to rvalue and extends its lifetimeRvalue References
(&&)
Introduced in C++11, rvalue references are designated by a double
ampersand (Type&&). They are engineered
specifically to bind exclusively to temporary rvalues rather than
persistent lvalues.
By exclusively capturing temporary objects that are slated for destruction, an rvalue reference signals to functions and constructors that the resource owned by that object can safely be "stolen" or transferred, bypassing expensive deep copies.
int&& rref = 25 + 75; // Valid: binds to temporary value 100
int count = 10;
// int&& badRref = count; // Compilation Error: cannot bind rvalue reference to lvalue 'count'Practical
Applications: Move Semantics and std::move
The practical utility of distinguishing between reference types becomes visible when implementing move constructors and move assignment operators.
| Category | Primary Syntax | Can Bind To | Key Use Case |
|---|---|---|---|
| Lvalue Reference | Type& |
Non-const lvalues | In-place mutation, aliasing |
| Const Lvalue Reference | const Type& |
Lvalues, rvalues | Read-only access without copying |
| Rvalue Reference | Type&& |
Non-const rvalues | Move semantics, resource pilfering |
Eliminating Deep Copies
Classes managing heap allocations (such as dynamically sized buffers, network sockets, or file handles) historically paid a heavy performance penalty when returning by value or passing arguments. With rvalue references, resources are transferred via pointer swaps:
class DynamicBuffer {
int* data;
size_t size;
public:
// Copy constructor: allocates new memory and duplicates content
DynamicBuffer(const DynamicBuffer& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// Move constructor: pilfers heap pointer from expiring temporary
DynamicBuffer(DynamicBuffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Leave source in a safe, destructible state
other.size = 0;
}
~DynamicBuffer() {
delete[] data;
}
};The Role of std::move
An lvalue does not automatically bind to an rvalue reference. If a
programmer knows an lvalue is no longer needed, std::move
casts the lvalue into an rvalue reference type (an xvalue).
It does not actually move any data on its own; it merely enables
overload resolution to select the move constructor or move assignment
operator.
DynamicBuffer buf1;
DynamicBuffer buf2 = std::move(buf1); // Casts buf1 to an rvalue; invokes move constructorThe Named Rvalue Gotcha
A critical rule in C++ value semantics is that if an entity has a name, the expression evaluating it is an lvalue, even if its declared type is an rvalue reference.
void process(int&& value) {
// 'value' is an rvalue reference by type, but 'value' itself has a name!
// Inside this function, the expression 'value' is an lvalue.
// int&& nextRef = value; // Error: cannot bind rvalue reference to lvalue 'value'
int&& nextRef = std::move(value); // Valid: explicitly cast back to rvalue
}This prevents unexpected multiple moves from occurring when a
variable is referenced more than once across a function body. To forward
the rvalue property deeper into subsequent call stacks, explicit casting
via std::move or std::forward remains
necessary.