Value Categories in C++
Intro#
#include <type_traits>
#define IS_XVALUE(expr) \
(std::is_rvalue_reference<decltype((expr))>{})
#define IS_PRVALUE(expr) \
(!std::is_reference<decltype((expr))>{})
#define IS_LVALUE(expr) \
(std::is_lvalue_reference<decltype((expr))>{})
This is a method I recently discovered to determine the value category of an expression. I’ll explain why it works later.
First, let’s take a look at value categories in C++.
The most important thing to notice before we start is that value categories are not a property of values but a property of expressions. Much depends on what you take value to mean: I looked through the C++ standard, and it does not seem to define the term itself. We all know what an expression is, though (if not, see cppreference).
Every expression belongs to exactly one of three primary value categories: lvalue, prvalue and xvalue. (Two more names group them: a glvalue is an lvalue or an xvalue, and an rvalue is a prvalue or an xvalue.)
We know you can write x = 3 but not 3 = x. Both 3 and x are expressions, so there must be some qualitative difference between them. Since x can appear on the left of the assignment sign, we call it an lvalue; 3 is accordingly called an rvalue.
OK, that is the difference between lvalues and rvalues. But what are prvalues and xvalues?
There are two dimensions to an expression: 1. Does it have an identity? 2. Can it be moved from?
- An expression that has identity and can be moved from is an xvalue (“eXpiring value”).
- One that has identity but cannot be moved from is an lvalue.
- One that has no identity but can be moved from is a prvalue (“pure rvalue”).
For example, these are xvalues:
std::move(x);a.m, a member of an object expression, whereais an rvalue andmis a non-static data member of non-reference type.
Temporary materialization#
The most useful example for understanding xvalues is temporary materialization:
struct S { int m; };
int i = S().m; // member access expects a glvalue as of C++17;
// the prvalue S() is converted to an xvalue
As cppreference puts it: a prvalue of any complete type T can be converted to an xvalue of the same type T. This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object. If T is a class or array of class type, it must have an accessible and non-deleted destructor.
So the key difference between an xvalue and a prvalue is that an xvalue refers to an object that already exists, one whose resources we are allowed to reuse, while a prvalue is just a recipe for a value until it initializes something. There is nothing to reuse in the number 3 or 3.7, but we can reuse the memory allocated by a temporary std::string, and that temporary becomes an object (an xvalue) the moment we need one.
decltype and value categories#
decltype is a good tool for determining the category of an expression, because for an expression of type T it yields an rvalue reference T&& for an xvalue, the plain type T for a prvalue, and an lvalue reference T& for an lvalue.
Someone may object: with int x = 3;, x is an lvalue, yet decltype(x) is int rather than int&. That is the special case for id-expressions in the behaviour of decltype:
If the argument is an unparenthesized id-expression or an unparenthesized class member access expression, then decltype yields the type of the entity named by this expression. If there is no such entity, or if the argument names a set of overloaded functions, the program is ill-formed.
So the right way is int x = 3; decltype((x)) xref = x;. Here the type of xref is int&. That is why the macros above wrap expr in an extra pair of parentheses: it turns every id-expression into an ordinary expression, and decltype then reports its value category.
#include <string>
#include <utility>
struct S { int m; };
int main()
{
int x = 3;
static_assert(IS_LVALUE(x));
static_assert(IS_LVALUE("hello")); // string literals are lvalues
static_assert(IS_PRVALUE(3));
static_assert(IS_PRVALUE(x + 1));
static_assert(IS_PRVALUE(std::string("hi")));
static_assert(IS_XVALUE(std::move(x)));
static_assert(IS_XVALUE(S().m));
}std::move#
The famous function std::move has nothing to do with actually moving something. All it does is turn its argument into an xvalue; usually you call it on an lvalue.
std::move is used to indicate that an object t may be “moved from”, i.e. to allow the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to static_cast<std::remove_reference_t<T>&&>(t) (cppreference).
Revised in October 2026. The macros now spell out std:: and the header they need, and a small test program has been added; it compiles with GCC and Clang in C++17 mode. The prvalue/xvalue comparison was sharpened: since C++17 a prvalue is not an object until it is materialized, so what tells the two apart is whether an object already exists, not whether it can be moved from. Links point to the current cppreference URLs.