C++ / MOVE SEMANTICS AND SPECIAL MEMBER FUNCTIONS
Lvalues, rvalues, and value categories
Classify any C++ expression as an lvalue, xvalue, or prvalue from its shape, and predict which overloads and references it can bind to.
What you will learn
- Classify an expression by two questions: does it have identity, may it be moved from?
- Read a call's category from its return type: T is prvalue, T& lvalue, T&& xvalue.
- Know that naming a variable always yields an lvalue, even a std::string&& variable.
- Probe a category with decltype((e)): T& is lvalue, T&& is xvalue, plain T is prvalue.
Understanding Lvalues, rvalues, and value categories
A value category is a property of an expression, not of an object, a variable, or a type. Each expression answers two independent questions: does it have identity, meaning you can say which object it denotes, and may the compiler move from it? Identity without movability is an lvalue, identity with movability is an xvalue, and movability without identity is a prvalue, which is why there are exactly three leaf categories. The words glvalue and rvalue are umbrellas over those leaves: glvalue means has identity, so lvalue or xvalue, and rvalue means may be moved from, so xvalue or prvalue.
You can usually read the category off the shape of the expression. Any name that refers to a variable, a member, or a function is an lvalue whatever its declared type, so a variable of type std::string&& still gives an lvalue when you mention it, and a string literal is an lvalue because it denotes an array object that lives in the program. A function call takes its category from the declared return type, with T giving a prvalue, T& an lvalue and T&& an xvalue, and the built-in operators follow the same logic: prefix ++, unary * and array subscripting yield lvalues, while arithmetic, comparison and postfix ++ yield prvalues.
The idea that makes prvalues click is that a prvalue is not an object yet; it is a recipe for initialising one. Since C++17 an object appears only when the context demands one, for instance when the prvalue initialises a variable or a reference, when you access a member through it, or when a statement discards it; that step is called temporary materialisation, and it turns the prvalue into an xvalue. Because binding rules and overload resolution inspect the category of the expression rather than the object behind it, getting the category right is what lets you predict which function actually runs.
<iostream>
<string>
<type_traits>
<utility>
template <typename T>
const char* category_of() {
if constexpr (std::is_lvalue_reference_v<T>) return "lvalue";
else if constexpr (std::is_rvalue_reference_v<T>) return "xvalue";
else return "prvalue";
}
// decltype((e)) reports the category: T& for an lvalue, T&& for an xvalue,
// plain T for a prvalue. The operand is never evaluated.
SHOW(expr)
std::string make() { return std::string("temp"); }
int main() {
int n = 5;
int arr[3]{};
std::string s = "hello";
std::string&& r = make(); // r is a variable, not a temporary
SHOW(n);
SHOW(n + 1);
SHOW(arr[0]);
SHOW(make());
SHOW(r);
SHOW(std::move(s));
SHOW(s + "!");
SHOW(++n);
SHOW(n++);
std::cout << "n is still " << n << '\n';
}
A value category describes an expression, not an object, and it follows from two properties only: whether the expression has identity and whether it may be moved from.
Worked examples
What lvalue-ness lets you write to
Shows that only an lvalue can be the target of built-in assignment, while a class prvalue accepts assignment that goes nowhere.
<iostream>
<string>
struct Grid {
int cells[4] = {0, 0, 0, 0};
int& at(int i) { return cells[i]; } // call is an lvalue
int get(int i) { return cells[i]; } // call is a prvalue
};
int main() {
Grid g;
g.at(0) = 9; // fine: writes through an lvalue
// g.get(0) = 9; // error: built-in assignment needs an lvalue
std::cout << "cells[0] = " << g.cells[0] << '\n';
std::string s = "abc";
(s + "d") = "zz"; // compiles: operator= runs on a temporary
std::cout << "s = " << s << '\n';
}
Example explained
Line 1g.at(0) = 9 works because a call to a function returning int& is an lvalue, so it denotes the object g.cells[0].
Line 2g.get(0) = 9 is rejected: the return type is int by value, making the call a prvalue, and built-in assignment demands a modifiable lvalue.
Line 3(s + "d") = "zz" compiles because the left side is a class prvalue and operator= is a member function, which can be called on the materialised temporary.
Line 4s still prints abc, because the write landed in that temporary and it was destroyed at the end of the statement.
Identity is what glvalues have and prvalues do not
Compares addresses to show that two mentions of an lvalue denote one object while two equal prvalues materialise two distinct objects.
<iostream>
<string>
std::string make() { return std::string(3, 'x'); }
int main() {
std::string s = "shared";
const std::string& a = s; // lvalue: binds to the object called s
const std::string& b = s;
const std::string& t1 = make(); // prvalue: materialises a fresh object
const std::string& t2 = make();
std::cout << std::boolalpha
<< "&a == &b : " << (&a == &b) << '\n'
<< "&t1 == &t2 : " << (&t1 == &t2) << '\n'
<< "t1 == t2 : " << (t1 == t2) << '\n';
}
Example explained
Line 1&a == &b is true because the lvalue s has identity, so both references bind to the same object.
Line 2Each make() call is a separate prvalue, and binding it to a reference materialises its own object, so the two addresses differ.
Line 3t1 == t2 is true, which separates the two ideas: equal value does not mean same identity.
Line 4Both temporaries survive to the end of main because binding to a const reference extends their lifetime, so comparing their addresses is meaningful.
Important notes
The categories form a tree, not a flat list: an xvalue is both a glvalue and an rvalue, so asking whether something is an rvalue is not the same as asking whether it is a prvalue.
Older texts describe a prvalue as a temporary object; since C++17 no object exists until materialisation, so think of a prvalue as an initialiser instead.
Common mistakes
Assuming a parameter declared T&& is an rvalue inside the function; mentioning it by name is an lvalue, so an inner call quietly picks the copying overload.
Reading decltype(x) as the category of x: for int x it yields int, not int&, and only decltype((x)) asks about the expression, so category checks fail for the wrong reason.
Treating string literals as prvalues, then being surprised that a template deduces const char (&)[6] from "hello" and that &"hello" is legal.
Try it yourself
Change, predict, then run
Add SHOW lines for arr, "abc", n > 0 ? n : n, and static_cast<int&&>(n) to the main program, writing your prediction as a comment beside each one first. Run it and work out why any prediction you got wrong was wrong.
Open the C++ workspaceCheck your understanding
Inside void sink(std::string&& s), what is the value category of the expression s?
- xvalue, because s is declared as an rvalue reference
- lvalue, because naming a variable always produces an lvalue
- prvalue, because the caller passed a temporary
- It depends on whether the caller passed a temporary or a named object
Show answer
An id-expression that names a variable is an lvalue regardless of the variable's declared type, so s is an lvalue and, for example, will select a std::string& overload. Option 0 confuses the declared type std::string&& with the category of the expression that names it; an xvalue comes from expressions such as a cast to std::string&& or a call whose return type is std::string&&, not from the mere mention of the variable.