lvalues, rvalues, xvalues — Value Categories
Every expression in C++ has a type and a value category. The value category answers two questions: does this expression have a stable identity (an address you can take), and is it safe to move resources away from it? These two properties combine into three primary categories — lvalue, prvalue, and xvalue — plus two umbrella terms (glvalue and rvalue). Understanding this taxonomy is the foundation for understanding move semantics, overload resolution between T& and T&&, and why std::move is just a cast.
The two fundamental properties
The C++ standard defines value categories using two binary properties:
Has identity (is a "glvalue")
The expression refers to an object with a stable memory location — you can take its address with &, and it will still be there later. Named variables, array elements, and dereferenced pointers have identity.
Can be moved from (is an "rvalue")
The expression's resources can be stolen — the object is either a temporary that is about to be destroyed, or it has been explicitly marked as safe to move (via std::move). Taking from it won't break anyone's expectations.
| Category | Has identity | Movable | Examples |
|---|---|---|---|
| lvalue | Yes | No | Named variable x, *ptr, arr[i], function returning T& |
| prvalue | No | Yes | Literal 42, "hello", x+1, function returning T |
| xvalue | Yes | Yes | std::move(x), function returning T&& |
glvalue = generalized lvalue = lvalue ∪ xvalue (has identity). rvalue = prvalue ∪ xvalue (movable). xvalue sits at the intersection: it has identity but has been marked safe to move from.
lvalues — named, persistent, addressable
An lvalue(historically "left-hand-side value") is an expression that refers to an object with a persistent identity. You can take its address, bind a non-const reference to it, and assign to it (if it isn't const). Most named things are lvalues.
int x = 42; // x is an lvalue
int* p = &x; // OK — lvalues are addressable
int& r = x; // OK — non-const ref binds to lvalue
std::string s = "hello"; // s is an lvalue
s[0] = 'H'; // s[0] is an lvalue (subscript of lvalue is lvalue)
// Function calls that return a reference produce an lvalue:
int& front(std::vector<int>& v) { return v[0]; }
front(vec) = 99; // fine — front() returns an lvalue referenceprvalues — temporaries, literals, computations
A prvalue(pure rvalue) has no persistent identity — it is a temporary produced by a computation. Literals, arithmetic expressions, and functions that return by value all produce prvalues. You cannot take the address of a prvalue, and you cannot bind a non-const lvalue reference to one. A const reference or an rvalue reference can extend a prvalue's lifetime.
42 // prvalue — integer literal
3.14 // prvalue — double literal
x + 1 // prvalue — result of addition (a temporary)
std::string("hello") // prvalue — unnamed temporary
// Cannot take address of a prvalue:
int* p = &42; // ERROR: cannot take address of prvalue
// Non-const ref cannot bind to prvalue:
int& r = 42; // ERROR
const int& cr = 42; // OK — const ref extends lifetime
int&& rr = 42; // OK — rvalue ref extends lifetimeC++17 introduced guaranteed copy elision (also called "mandatory RVO"): when a prvalue is used to initialize an object, the compiler is required to construct it directly in place — no copy or move occurs, not even conceptually. This is why return std::vector<int>(1000) is always efficient.
xvalues — expiring, movable, but still named
An xvalue (expiring value) has an identity (an address) but has been marked as safe to steal from. The most common way to produce an xvalue is std::move(x) — which is literally just a cast to T&&. After being moved from, the object is in a valid but unspecified state — you must not use it except to reassign or destroy it.
std::string s = "hello"; // std::move(s) is an xvalue — same object as s, but marked movable std::string t = std::move(s); // t steals s's buffer // s is now in a valid but unspecified state: s = "world"; // fine — reassigning a moved-from string is always safe std::cout << s; // "world" // Functions returning T&& also produce xvalues: std::string&& ref = std::move(s); // ref is an xvalue expression
The key insight: std::move does not move anything. It just changes the value category of an expression from lvalue to xvalue, enabling the compiler to select a move constructor or move assignment operator instead of a copy.
How value categories drive overload resolution
The compiler uses value categories to select which overload to call. An lvalue binds to T&; an rvalue (prvalue or xvalue) binds to T&&. This is exactly how standard library types implement efficient move semantics without sacrificing copy semantics for lvalues.
void process(std::string& s) {
std::cout << "copy version\n";
}
void process(std::string&& s) {
std::cout << "move version\n";
}
std::string name = "Alice";
process(name); // lvalue → calls copy version
process(std::string("Bob")); // prvalue → calls move version
process(std::move(name)); // xvalue → calls move version
// Inside std::vector::push_back:
// push_back(const T&) — lvalue path: copies the element
// push_back(T&&) — rvalue path: moves the element
std::vector<std::string> v;
v.push_back(name); // copy — name still usable
v.push_back(std::move(name)); // move — name now emptyKey rules to remember
lvalue = has identity (address); rvalue = movable (safe to steal from)
These are the two fundamental properties. lvalue has identity but is not movable (by default). prvalue is movable but has no identity. xvalue has both.
std::move is just a cast — it does not move anything itself
std::move(x) converts x from an lvalue to an xvalue. The actual stealing happens in the move constructor or move assignment operator that gets called.
After std::move, treat the moved-from object as valid but unspecified
You can safely reassign or destroy it. Do not read its value — the standard only guarantees it's in some valid state.
Named rvalue references are lvalues
void f(T&& x) — inside f, x is an lvalue because it has a name. To forward its rvalue-ness, use std::forward<T>(x), not x.