C++ Value Categories and Move Semantics: Trace What Actually Moves
Trace a unique allocation through lvalues, xvalues, prvalues, copy and move constructors, named rvalue references, const, and C++17 copy elision.
KnowledgeGate Team
Exam prep & CS education

std::move(a) can leave a untouched, yet a later constructor call with the same spelling can transfer its allocation. Syntax alone does not reveal when ownership changes.
Payloads [4, 8, 15] and [2, 3] make overload binding, allocation ownership, and C++17 copy elision visible at each step. For a refresher on references, constructors, or lifetime, start with C++ Tutorial: Complete Learning Path in 12 Weeks.
1. C++ value categories: put every expression on one map
Every C++ expression is an lvalue, xvalue, or prvalue. Lvalues and xvalues are glvalues because they identify objects or functions; prvalues and xvalues are rvalues because they can bind to rvalue references. An lvalue is not merely something left of =.
The expression a is an lvalue, std::move(a) is an xvalue, and make_buffer() is a prvalue. In Buffer&& ref = std::move(a), the initializer is an xvalue, but the named expression ref is an lvalue.
std::move is a cast to an xvalue. Alone, it invokes no constructor and transfers nothing. In Coding & DSA, classify the expression first, then inspect the selected function.

2. Overload selection can happen without any move
Start with this exact overload set. Each function only prints which reference bound to the argument.
#include <iostream>
struct Buffer;
void tag(Buffer&) { std::cout << "lvalue\n"; }
void tag(const Buffer&) { std::cout << "const reference\n"; }
void tag(Buffer&&) { std::cout << "rvalue\n"; }Construct Buffer a{101, {4, 8, 15}};. Name its allocation P1, giving a={101,3,P1->[4,8,15]}.
It logs construct 101. P1 names the separate array uniquely owned by data.
tag(a) receives an lvalue, so the Buffer& overload prints lvalue. Next, tag(std::move(a)) receives an xvalue, so the Buffer&& overload prints rvalue. Reference binding explains both choices.
After both calls, the state is still a={101,3,P1->[4,8,15]}. Nothing was copied or transferred: the rvalue-reference overload only observed the argument. Selecting Buffer&& does not prove a move occurred.
Diagnose in order: classify the expression, choose the overload, then read its body.
3. The move constructor is where ownership changes
Buffer owns one allocation through std::unique_ptr<int[]>. The copy constructor allocates fresh storage. The move constructor takes the existing storage and deliberately resets the source fields.
#include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <utility>
struct Buffer {
int resource_id;
std::size_t size;
std::unique_ptr<int[]> data;
Buffer(int id, std::initializer_list<int> values)
: resource_id(id), size(values.size()),
data(size ? std::make_unique<int[]>(size) : nullptr) {
if (size) std::copy(values.begin(), values.end(), data.get());
std::cout << "construct " << resource_id << '\n';
}
Buffer(const Buffer& other)
: resource_id(other.resource_id), size(other.size),
data(size ? std::make_unique<int[]>(size) : nullptr) {
if (size) std::copy_n(other.data.get(), size, data.get());
std::cout << "copy " << resource_id << '\n';
}
Buffer(Buffer&& other) noexcept
: resource_id(std::exchange(other.resource_id, -1)),
size(std::exchange(other.size, 0)),
data(std::move(other.data)) {
std::cout << "move " << resource_id << '\n';
}
};Continue with Buffer b{std::move(a)};. The xvalue selects the move constructor, which exchanges resource_id 101 and size 3 out of a, then moves its std::unique_ptr. P1 passes to b; the source pointer becomes null.
std::exchange(other.resource_id, -1) returns 101 for b and writes -1 into a; the next exchange returns 3 and writes 0. Moving other.data gives b.data the original pointer and leaves a.data null. No elements are copied and no second array is allocated.
The exact post-state is a={-1,0,null} and b={101,3,P1->[4,8,15]}. The log adds move 101. This state change comes from the constructor body and member initialisers, not from std::move alone.
Overload selection enters the constructor; its implementation performs the transfer. Another Buffer&& function could change nothing, as tag did.
The C++ Move Semantics Tutorial: Trace Ownership with Runnable Examples follows a raw-pointer Buffer through copy and move assignment, vector relocation, and noexcept. The P1 diagnostic separates expression category from overload choice before checking whether the selected body transfers ownership.

4. Copy, move, and C++17 copy elision have different traces
Run Buffer c{b};. The lvalue b selects the copy constructor and logs copy 101. The states become b={101,3,P1->[4,8,15]} and c={101,3,P2->[4,8,15]}. The values match, but P1 and P2 are distinct allocations.
Now add:
Buffer make_buffer() {
return Buffer{202, {2, 3}};
}
Buffer d = make_buffer();In C++17 and later, the returned prvalue initialises d directly. The exact state is d={202,2,P3->[2,3]}, and the only new log is construct 202. No copy 202 or move 202 occurs: this is guaranteed copy elision.
Operation | Allocation result | Source result |
|---|---|---|
Copy | Creates P2 with the same values |
|
Move | Keeps P1 and changes its owner |
|
C++17 elision | Constructs P3 directly as | No intermediate source object |
5. Named rvalue references and const change the selected path
Consider Buffer&& ref = std::move(b); tag(ref); tag(std::move(ref));. Binding ref transfers nothing; it names b, which still owns P1. The expression ref is an lvalue, so the first call prints lvalue; std::move(ref) is an xvalue, so the second prints rvalue. Since tag consumes nothing, b, ref, and P1 remain unchanged.
For const Buffer frozen{303, {9}};, tag(std::move(frozen)); passes a const Buffer&& xvalue. It cannot bind to non-const Buffer&&, so const Buffer& prints const reference.
Likewise, with only Buffer(const Buffer&) and Buffer(Buffer&&), Buffer e{std::move(frozen)}; copies because the non-const move overload cannot take ownership. Its allocation is independent of frozen. Predict bindings from both category and full type, including const.
6. Move-semantics traps: state what is guaranteed and what is not
Tempting claim | What goes wrong | Correct rule |
|---|---|---|
| A cast performs no transfer | It produces an xvalue |
An rvalue-reference overload means a move occurred | The callee may only observe | Inspect the function body |
Every moved-from object is empty | The state depends on the type | Use its documented guarantees |
Moving is always faster | A type may copy or do work while moving | Inspect the operations |
Our a is predictably {-1,0,null} because this constructor defines that state and the moved std::unique_ptr becomes null. Do not generalise those values. After surrendering ownership, use the source only within its documented moved-from contract.
7. How interviews and exam-style questions test value categories
Typical checks classify a, std::move(a), make_buffer(), and named ref; predict tag; count logs; draw P1 and P2; test const; or identify guaranteed elision.
Answer this mini-trace first: after construction and both tag calls, where does [4,8,15] live? It is still in P1, owned by a. Only Buffer b{std::move(a)} makes P1 belong to b.
Why does tag(ref) print lvalue although ref is declared Buffer&&? A named variable expression is an lvalue. Keep declaration type, expression category, overload, and callee behaviour separate.
8. C++ move semantics: the short version and next step
For each statement, ask: What is the expression category? Which overload binds? Does it copy or move? Who owns P1 afterward? Reproduce the exact snapshots for a, b, c, and d; explain why tag(std::move(a)) leaves P1 in place but Buffer b{std::move(a)} transfers it.
Use C++ Programming for focused language concepts, or Coding For Placements for the broader C, C++, Java, and Python practice route.
Keep learning

C++ Rule of Three, Five and Zero: Safe Object Copying and Moving
Follow one four-integer Buffer from a dangerous shallow copy to correct copy and move operations, then remove the lifetime code with std::vector.

C++ Object Lifetime and Storage Duration: Trace Construction, Destruction, and Dangling State
Learn to separate scope, storage duration, lifetime, and ownership. Trace seven objects precisely, then repair dangling pointers, references, and views.

C++ Diamond Problem: Ambiguity, Duplicate State and Virtual Inheritance
See why a C++ diamond duplicates base state, how virtual inheritance shares it, and when qualification, interfaces, or composition is the clearer design.

C++ Abstract Classes and Interfaces: Build a Usable Storage Contract
Build a C++17 ScoreStore interface, swap vector and hash-backed implementations behind it, and verify both with the same runnable contract test.