Skip to content
C++
C++11Intermediate

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.

CategoryHas identityMovableExamples
lvalueYesNoNamed variable x, *ptr, arr[i], function returning T&
prvalueNoYesLiteral 42, "hello", x+1, function returning T
xvalueYesYesstd::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 reference

prvalues — 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 lifetime

C++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 empty

Key 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.

Sign in to track progress