Historical Context & Motivation
The distinction between lvalues and rvalues is one of the oldest and most consequential ideas in programming language design, predating C++ by decades. The terminology originated in early compiler theory, where the sides of an assignment statement determined how the compiler treated an expression — whether it referred to a persistent memory location or merely computed a temporary result. As C++ evolved from C, this classification was formalized and expanded to enable increasingly sophisticated optimizations, culminating in the move semantics revolution of C++11. Understanding value categories is essential not just for writing efficient code, but for comprehending how the compiler reasons about object lifetimes, resource ownership, and expression evaluation.
& (address-of).T&), which can only bind to lvalues. This enforces the lvalue/rvalue distinction in the type system more rigorously than C ever did, and forces programmers to think about value categories explicitly.T&&) and a refined five-category value taxonomy (lvalue, xvalue, prvalue, glvalue, rvalue). This enables move semantics, eliminating millions of unnecessary deep copies across the C++ ecosystem.The central question that value categories answer is deceptively simple: does this expression identify a persistent object in memory, or does it produce a disposable temporary? The answer determines what the compiler allows you to do with the result — whether you can take its address, bind it to a reference, or steal its resources. Grasping this dichotomy is the prerequisite for understanding move semantics, perfect forwarding, and much of the template machinery that powers modern C++ libraries.
Core Principles & Definitions
At the conceptual level, every expression in C++ has two independent properties: a type (such as int or std::string) and a value category. The type tells you what kind of data the expression represents, while the value category tells the compiler — and the programmer — how the result of that expression relates to storage. These two dimensions are orthogonal: an int expression can be either an lvalue or an rvalue, and so can a std::vector<double> expression. The following cards lay out the foundational ideas.
Lvalue — "Locator Value"
&, and it survives beyond the expression that produced it. Examples: named variables, array elements, dereferenced pointers, and string literals.Rvalue — "Read Value" / Temporary
The Assignment Test (Simplified)
const variable is an lvalue you cannot assign to). The modern definition uses identity and movability instead.Identity vs. Movability
Visual Explanation — Lvalues and Rvalues in Memory
x and y) occupying stable stack addresses — these are lvalues. The right panel shows temporaries: the literal 42, the arithmetic result x + 10, and a function return value — all rvalues with no persistent address (dashed borders). During assignment, the rvalue on the right is consumed to initialize the lvalue on the left.The diagram above captures the fundamental asymmetry: lvalues live in addressable, named storage and persist across statements, while rvalues inhabit a transient, compiler-managed space and are destroyed at the semicolon (or, more precisely, at the end of the full-expression). When you write int y = x + 10;, the sub-expression x is an lvalue (it names a variable), 10 is an rvalue (a literal), and x + 10 is an rvalue (the result of arithmetic). The variable y itself is an lvalue that receives the temporary result.
How the Compiler Decides — Rules and Mechanisms
Rather than a single formula, value category classification follows a set of compiler rules that you can internalize through a few core patterns. The compiler examines each sub-expression and determines its category based on the syntactic form of the expression, not on the type of the result. The same type int can be an lvalue (x) or an rvalue (x + 1). Below we formalize the two key properties the compiler checks, followed by the resulting classification.
The Two Properties
&e is well-defined and yields a stable address, then e has identity. Named variables, array elements, dereferenced pointers, and references all have identity.e after the current expression, the compiler knows it is safe to move rather than copy. Temporaries and expressions cast via std::move satisfy this property.Quick Classification Rules
| Expression Form | Category | Why? |
|---|---|---|
x (named variable) | lvalue | Has identity (stable address), not implicitly movable |
*ptr (dereference) | lvalue | Refers to the object at the pointer's address |
arr[i] (subscript) | lvalue | Designates a specific element in memory |
42 (integer literal) | rvalue (prvalue) | No identity — just a value baked into the instruction |
x + y (arithmetic) | rvalue (prvalue) | Produces a fresh temporary, no lasting address |
std::move(x) | rvalue (xvalue) | Has identity (it's still x) but explicitly marked movable |
"hello" (string literal) | lvalue | String literals are stored in static memory with a fixed address |
"hello" to be an rvalue because it looks like a literal. However, string literals are arrays stored in static memory, so the expression has a well-defined address and is therefore an lvalue. In contrast, numeric literals like 42 are encoded directly into machine instructions and have no addressable location — they are rvalues.The C++11 Value Category Taxonomy
While the simple lvalue/rvalue split suffices for everyday reasoning, C++11 refined the system into five categories arranged in a hierarchy. Understanding this taxonomy is important because the standard and compiler diagnostics reference these terms, and they underpin the rules for reference binding, overload resolution, and template argument deduction. The two primary categories — glvalue ("generalized lvalue") and rvalue — each split into sub-categories, with the xvalue ("expiring value") sitting at their intersection.
std::move).For this introductory lesson, the most important takeaway from the taxonomy is the xvalue — it is the category that bridges lvalues and rvalues. An xvalue is an expression that still identifies an object (has identity), but the programmer has explicitly signaled that the object's resources may be cannibalized. This is precisely what std::move(x) does: it does not move anything by itself; it merely casts x from an lvalue to an xvalue, enabling the compiler to select a move constructor or move assignment operator instead of a copy.
& operator to an expression and it compiles, the expression is an lvalue. If it fails, the expression is an rvalue (either a prvalue or an xvalue). Note that user-defined operator& can muddy this test, but for built-in types and standard library types, it is reliable.Worked Example — Classifying Expressions
Consider the following C++ snippet. We will classify every sub-expression's value category step by step.
std::string name = "Alice"; std::string greeting = "Hello, " + name; std::string stolen = std::move(greeting);
"Alice" is a string literal of type const char[6]. Despite looking like a "plain value," string literals are stored in static memory and have a stable address. Therefore, "Alice" is an lvalue. It is used here to construct a std::string via an implicit conversion, producing a temporary — but the literal itself is an lvalue."Alice" → lvaluename is a named variable of type std::string. Named variables always designate persistent objects on the stack and are therefore lvalues. You can apply &name to confirm.name → lvalue"Hello, " + name invokes operator+ for std::string, which returns a new std::string by value. A function call that returns a non-reference type produces a prvalue (rvalue). This temporary has no name and no persistent address; it exists only to initialize greeting."Hello, " + name → prvalue (rvalue)std::move(greeting) is a cast that converts the lvalue greeting into an xvalue. The object still has identity (it is the same object greeting at the same address), but it is now considered movable. The std::string move constructor will transfer the internal buffer to stolen, leaving greeting in a valid but unspecified state.std::move(greeting) → xvalue (rvalue with identity)name holds "Alice" (unchanged — only read, never moved from). stolen holds "Hello, Alice" (received via move). greeting is in a valid but unspecified state — likely empty. All three variables are still lvalues as expressions; value category describes the expression, not the object's contents.Reference Binding Rules — Where Categories Matter
The most immediate practical consequence of value categories is how they interact with C++ references. The compiler enforces strict rules about which reference types can bind to which categories, and these rules drive function overload resolution — the mechanism by which the compiler chooses between copy and move operations. Understanding this table is essential for writing correct modern C++ code and interpreting compiler error messages.
| Reference Type | Binds to Lvalues? | Binds to Rvalues? | Typical Use |
|---|---|---|---|
T& (lvalue ref) | ✅ Yes | ❌ No | Modify a persistent object |
const T& (const lvalue ref) | ✅ Yes | ✅ Yes | Read any expression; extends rvalue lifetime |
T&& (rvalue ref) | ❌ No | ✅ Yes | Move constructors / move assignment |
const T&& (const rvalue ref) | ❌ No | ✅ Yes | Rarely used — prevents both copy and move |
The critical row is the second one: a const T& can bind to both lvalues and rvalues, which is why pre-C++11 functions accepting const std::string& could accept temporaries. When C++11 added rvalue references, the compiler gained the ability to overload on value category: if both f(const T&) and f(T&&) exist, an rvalue argument selects the T&& overload, enabling the function to move from the argument. This is the foundation of the copy-vs-move optimization pattern in modern C++ containers and algorithms.
T& is a master key that only works on occupied rooms (lvalues) — you need a real, persistent guest. A T&& is a checkout key — it only works when the guest (rvalue) is leaving and the hotel can reclaim the room's resources. A const T& is a read-only visitor pass — it works for any room regardless of the guest's status, but you can't take anything from it.Connection to Move Semantics and Advanced Topics
Understanding lvalues and rvalues is the conceptual prerequisite for the most impactful C++11 feature: move semantics. Without a clear mental model of value categories, the motivations behind move constructors, std::move, std::forward, and perfect forwarding remain opaque. The table below maps concepts from this introductory lesson to their advanced counterparts that you will encounter in subsequent study.
| This Lesson (Conceptual) | Advanced Topic | Why It Matters |
|---|---|---|
| Rvalues are temporaries | Move constructors & move assignment | Temporaries can transfer heap resources (e.g., dynamic arrays) in O(1) instead of O(n) copying |
std::move casts lvalue → xvalue | Resource ownership transfer | Enables explicit opt-in to move semantics for named objects you no longer need |
| Reference binding rules | Perfect forwarding (std::forward) | Template functions can preserve the caller's value category when passing arguments, avoiding unnecessary copies in generic code |
| Prvalues have no identity | Guaranteed copy elision (C++17) | Prvalues initialize objects directly without materializing intermediaries, so even non-movable types can be returned by value |
| Value category ≠ type | Reference collapsing & forwarding references | Template deduction rules collapse reference types based on the argument's value category, enabling universal references |
The key forward-looking insight is that value categories are not merely a theoretical classification — they directly control which overloads the compiler selects, which constructors get called, and whether resources are transferred efficiently or duplicated wastefully. Every major standard library container (e.g., std::vector, std::unique_ptr, std::string) leverages move semantics internally, so a solid understanding of lvalues and rvalues directly impacts your ability to write and reason about performant C++ code.
Practice Problems
x (a named variable) is an lvalue, but x + 1 is an rvalue, even though both expressions have the same type int.int a = 5, b = 10; int* p = &a;
(a) a (b) a + b (c) *p (d) 42 (e) ++avoid increment(int& ref) { ref++; }
int main() { increment(5); }Buffer class that manages a dynamically allocated array. You want to provide both a copy constructor and a move constructor. Explain how value categories determine which constructor the compiler calls in each of the following scenarios:
(a) Buffer b2 = b1; where b1 is a named Buffer
(b) Buffer b3 = createBuffer(); where createBuffer() returns a Buffer by value
(c) Buffer b4 = std::move(b1);template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
When wrapper is called with an lvalue int x = 10; wrapper(x);, the parameter arg has type int& due to reference collapsing. But when called with an rvalue wrapper(42);, arg has type int&&. Explain why arg itself is always an lvalue inside wrapper, regardless of its declared type, and why std::forward is necessary to restore the original value category.Summary
Every expression in C++ has a value category that is independent of its type. An lvalue denotes a persistent object with a stable memory address — named variables, dereferenced pointers, array elements, and string literals are all lvalues. An rvalue produces a temporary that has no lasting identity — literals, arithmetic results, and function return values (by value) fall into this category. C++11 refined the taxonomy into five categories (lvalue, xvalue, prvalue, glvalue, rvalue) based on two properties: identity and movability.
The practical impact of value categories is most visible in reference binding: non-const lvalue references (T&) bind only to lvalues, rvalue references (T&&) bind only to rvalues, and const T& binds to both. This asymmetry enables the compiler to distinguish copy operations from move operations via overload resolution — the foundation of move semantics and perfect forwarding in modern C++.