Article by Ayman Alheraki on February 4 2026 09:32 AM
Many C++ developers can correctly define lvalue, rvalue, glvalue, xvalue, and prvalue.
Yet when it’s time to apply the Rule of Three or Rule of Five in real code, something still feels off.
That feeling is valid.
The standard’s value categories are precise, but they are also descriptive, not instructional. They tell you what an expression is, but they don’t automatically tell you what you should do about ownership, copying, or moving.
This article connects the theory to the part that matters in production C++: identity and ownership transitions.
Instead of memorizing tables, ask one question:
Does this expression represent a reusable object (identity), or a disposable payload?
In modern C++ (RAII + Rule of Five), this is the real axis behind the categories.
A glvalue refers to something that has identity: it exists somewhere in memory and you can refer to it again.
Typical glvalues:
named variables
class members
dereferenced pointers
references
std::string s;s // glvalueIf it has identity, it can be used again later. That fact alone explains why copying exists as a meaningful operation.
An lvalue is a glvalue that represents a reusable object. It’s stable. You’re not expected to take it apart.
std::string s;foo(s); // s is an lvaluePractical meaning:
the caller likely still needs s
copying is the safe default
moving requires explicit permission
That’s why copy constructors and copy assignment exist: they support the “reusable object” path.
This is where confusion tends to accumulate.
An xvalue still has identity, but you have explicitly marked it as disposable—most commonly with std::move.
std::string s;foo(std::move(s)); // s becomes an xvalueWhat this really means:
“This object still exists, but I promise I won’t use its old value again.”
That promise is the moral and technical basis for move semantics. A move constructor is not “magic performance.” It is an ownership transfer that is only correct because the source is allowed to become “valid but unspecified.”
A prvalue is a pure temporary result. It has no identity of its own in the way a named variable does.
std::string("hello") // prvaluemake_string() // prvaluePractical meaning:
there is no “old object” to preserve
it’s a payload meant to build a new object
Since C++17, prvalues are even more important: in many cases they do not materialize a separate temporary object at all; instead, the final object is constructed directly when needed. This is why return-by-value is typically efficient and not something you should fear.
The Rule of Five is not really about value categories. It’s about resource ownership and transitions.
If your class owns a resource (memory, file handle, socket, mutex, OS object), you must answer four questions:
What happens when I copy from a reusable object?
What happens when I move from a disposable object?
What happens when I assign over an existing object?
What happens when the object dies?
Value categories simply tell you which scenario you’re in.
Here is the practical mapping:
lvalue → the source is reusable → copy
xvalue → the source is disposable → move
prvalue → temporary payload → construct
destruction → release owned resource
class Buffer { int* data{}; size_t n{};
public: Buffer(size_t n) : data(new int[n]), n(n) {}
// lvalue path: duplicate resource Buffer(const Buffer& other) : data(new int[other.n]), n(other.n) { std::copy(other.data, other.data + n, data); }
// xvalue path: transfer ownership Buffer(Buffer&& other) noexcept : data(other.data), n(other.n) { other.data = nullptr; other.n = 0; }
// assignment (copy): release then duplicate Buffer& operator=(const Buffer& other) { if (this == &other) return *this; delete[] data; n = other.n; data = new int[n]; std::copy(other.data, other.data + n, data); return *this; }
// assignment (move): release then steal Buffer& operator=(Buffer&& other) noexcept { if (this == &other) return *this; delete[] data; data = other.data; n = other.n; other.data = nullptr; other.n = 0; return *this; }
~Buffer() { delete[] data; }};Read it with the identity lens:
Copy constructor: “You might still need yours, so I duplicate.”
Move constructor: “You said you’re done with it, so I steal.”
Destructor: “I release what I own.”
This is why value categories matter: they decide whether stealing is legal.
Most experienced C++ developers don’t actively think:
“Is this a glvalue or prvalue?”
They think:
Am I allowed to steal from this?
If yes → move
If no → copy
If new → construct
That mental model matches both modern C++ style and how compilers optimize.
Value categories stop being confusing when you stop treating them as vocabulary and start treating them as ownership signals.
lvalue means “reusable object” → copy is expected
xvalue means “disposable identity” → move is allowed
prvalue means “temporary payload” → construct directly
Once you see them this way, the Rule of Five stops feeling like a rule—and starts feeling like simple resource logic.