C++ • C++-SPECIFIC TOPICS

Lvalues vs. Rvalues — Explain lvalues vs rvalues conceptually (intro)

Understanding value categories is the gateway to mastering move semantics and modern C++ performance.

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.

1960s
CPL and the "Left/Right" Insight
Christopher Strachey, working on CPL (Combined Programming Language), introduces the terms L-value and R-value to distinguish expressions that denote memory locations (left of assignment) from those that produce values (right of assignment).
1972
C Inherits the Concept
Dennis Ritchie's C language adopts the lvalue/rvalue distinction in its type system. An lvalue in C is "an expression referring to an object," while everything else is effectively an rvalue. The C standard uses these categories to define valid operands for operators like & (address-of).
1985
C++ Adds References
Bjarne Stroustrup's C++ introduces lvalue references (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.
2011
C++11 and Move Semantics
C++11 introduces rvalue references (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.
2017
Guaranteed Copy Elision
C++17 mandates copy elision for prvalues, meaning that temporary objects of class type need not even have move constructors in certain contexts. The value category of an expression now directly controls whether an object is materialized at all.

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.

1

Lvalue — "Locator Value"

An expression is an lvalue if it designates a persistent object with an identifiable memory location. You can take its address with &, and it survives beyond the expression that produced it. Examples: named variables, array elements, dereferenced pointers, and string literals.
2

Rvalue — "Read Value" / Temporary

An expression is an rvalue if it produces a temporary or disposable value without a stable identity. You generally cannot take its address. Its resources can be safely "moved" because no other code holds a reference to it. Examples: integer literals, arithmetic results, return values from functions returning by value.
3

The Assignment Test (Simplified)

Historically, if an expression can appear on the left side of an assignment, it is an lvalue. If it can only appear on the right, it is an rvalue. This heuristic works for built-in types but breaks down for user-defined types (a const variable is an lvalue you cannot assign to). The modern definition uses identity and movability instead.
4

Identity vs. Movability

The C++11 standard classifies expressions along two axes: has identity (can you distinguish it from other objects?) and can be moved from (is it safe to steal its resources?). Lvalues have identity but are not movable by default; prvalues are movable but have no identity; xvalues have both.
✦ KEY TAKEAWAY
Think of an lvalue as a labeled mailbox bolted to a wall — it has a fixed address, anyone can send mail to it, and it persists between deliveries. An rvalue is like a package in a courier's hand — it exists only long enough to be delivered. You can read its contents or transfer them somewhere permanent, but the package itself has no lasting address. In C++, the compiler uses this distinction to decide whether it's safe to copy or move resources, which has profound implications for performance.

Visual Explanation — Lvalues and Rvalues in Memory

The left panel shows named variables (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

IDENTITY PROPERTY
has_identity(e) ≡ "Can we distinguish e from other objects by comparing addresses?"
If &e is well-defined and yields a stable address, then e has identity. Named variables, array elements, dereferenced pointers, and references all have identity.
MOVABILITY PROPERTY
can_be_moved_from(e) ≡ "Is it safe to transfer e's resources to a new owner?"
If no other code will use 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

Common expression forms and their value categories
Expression FormCategoryWhy?
x (named variable)lvalueHas identity (stable address), not implicitly movable
*ptr (dereference)lvalueRefers to the object at the pointer's address
arr[i] (subscript)lvalueDesignates 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)lvalueString literals are stored in static memory with a fixed address
⚠️ Surprising Case: String Literals
Many students expect "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.

The C++11 taxonomy refines the original binary split. A glvalue has identity (lvalue + xvalue). An rvalue is movable (xvalue + prvalue). The xvalue uniquely belongs to both groups — it has identity but has been explicitly marked as movable (e.g., via 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.

💡 The Address-Of Test
A practical test: if you can apply the built-in & 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);

Classify Each Expression
1
Step 1 — Identify the string literalThe expression "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" → lvalue
2
Step 2 — Identify the named variable after initializationAfter the declaration, name 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
3
Step 3 — Classify the concatenation expressionThe expression "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)
4
Step 4 — Classify std::move(greeting)The expression 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)
5
Step 5 — Final state of all variablesAfter execution, 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.
name → "Alice", stolen → "Hello, Alice", greeting → moved-from state

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 binding rules in C++
Reference TypeBinds to Lvalues?Binds to Rvalues?Typical Use
T& (lvalue ref)✅ Yes❌ NoModify a persistent object
const T& (const lvalue ref)✅ Yes✅ YesRead any expression; extends rvalue lifetime
T&& (rvalue ref)❌ No✅ YesMove constructors / move assignment
const T&& (const rvalue ref)❌ No✅ YesRarely 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.

✦ KEY TAKEAWAY
Think of reference binding like hotel room keys. A 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.

Mapping introductory concepts to advanced C++ features
This Lesson (Conceptual)Advanced TopicWhy It Matters
Rvalues are temporariesMove constructors & move assignmentTemporaries can transfer heap resources (e.g., dynamic arrays) in O(1) instead of O(n) copying
std::move casts lvalue → xvalueResource ownership transferEnables explicit opt-in to move semantics for named objects you no longer need
Reference binding rulesPerfect forwarding (std::forward)Template functions can preserve the caller's value category when passing arguments, avoiding unnecessary copies in generic code
Prvalues have no identityGuaranteed copy elision (C++17)Prvalues initialize objects directly without materializing intermediaries, so even non-movable types can be returned by value
Value category ≠ typeReference collapsing & forwarding referencesTemplate 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

PROBLEM 1 — CONCEPTUAL
Explain in your own words why the expression x (a named variable) is an lvalue, but x + 1 is an rvalue, even though both expressions have the same type int.
PROBLEM 2 — BASIC CALCULATION
For each of the following expressions, classify them as lvalue or rvalue. Assume int a = 5, b = 10; int* p = &a; (a) a (b) a + b (c) *p (d) 42 (e) ++a
PROBLEM 3 — INTERMEDIATE
The following code fails to compile. Identify the value category error and explain why the compiler rejects it. void increment(int& ref) { ref++; } int main() { increment(5); }
PROBLEM 4 — APPLIED
You are implementing a 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);
PROBLEM 5 — CRITICAL THINKING
Consider the following function template: 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++.

Varsity Tutors • C++ • Lvalues vs. Rvalues — Explain lvalues vs rvalues conceptually (intro)