Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Value Categories: lvalue, xvalue, prvalue

โญโญโญ advancedโณ 10 min read๐Ÿ“ Lesson 131 of 204

Every expression is an lvalue, an xvalue, or a prvalue. Mastering the taxonomy explains moves, overloading, and half of the compiler errors you will meet.

Every expression in C++ has two independent properties: a type, and a value category. The category answers one question: what does this expression refer to, and can I reuse it?

The three categories

  • lvalue โ€” has identity, can be reused: a named variable, a dereferenced pointer, a function returning T&.
  • xvalue ("expiring") โ€” has identity, but marks it as reusable: std::move(x), a function returning T&&, a member of an xvalue.
  • prvalue ("pure") โ€” no identity yet, just a value being born: literals, T{...}, a function returning T by value.

lvalues + xvalues are glvalues (they designate an object). xvalues + prvalues are rvalues (they can be moved from).

std::string a = "hi";        // 'a' is an lvalue
std::move(a);                // std::move(a) is an xvalue
std::string(3, '?');         // prvalue
a + "!";                     // prvalue (operator+ returns by value)

Why the taxonomy exists

Overload resolution cares: f(T&) beats f(T&&) for lvalues, and f(T&&) is viable only for rvalues. That is the entire mechanism behind move-on-return, emplace_back, and forwarding. std::move does not move anything โ€” it is a cast to xvalue: "here is identity, please cannibalize it."

Reading categories out of types with decltype

The standard gives you a probe: for an expression e, decltype((e)) is:

  • T& if e is an lvalue,
  • T&& if e is an xvalue,
  • T if e is a prvalue.
static_assert(std::is_same_v<decltype((a)), std::string&>);             // lvalue
static_assert(std::is_same_v<decltype((std::move(a))), std::string&&>); // xvalue
static_assert(std::is_same_v<decltype((a + "!")), std::string>);        // prvalue

(Plain decltype(a) names the declared type; the double parentheses make decltype treat it as an expression.)

The two classic traps

Trap 1 โ€” named rvalue references are lvalues. Inside void f(T&& x), the expression x is an lvalue (it has a name!). Passing it on, assigning it, or storing it copies unless you std::forward/std::move it. One place the compiler compensates: the bare statement return x; performs an implicit move for rvalue-reference parameters (C++20 P1825) โ€” a convenience, not a rule to lean on. Everywhere else, forward explicitly.

Trap 2 โ€” rvalue-ness does not mean modifiable. A const T&& parameter binds rvalues but cannot be moved from; some APIs use it deliberately to reject moves.

Practice discipline for this module: before each exercise, write down the category you expect. The graded checks use decltype probes โ€” the same tool you will use in code review.

โšก Now practice

Ready to Code
Predict the CategoryThree challenges where you define functions whose call expressions land in exactly the right value category โ€” verified with decltype probes, the tool professionals use.
3 challenges ยท ยท ~22 min