Move Semantics Intuition
Why returning a big vector is cheap, what std::move actually does, and how to think about ownership transfer.
Copying a std::vector<int> with a million elements would mean
allocating a new million-element buffer and copying every byte.
Yet C++ programmers happily write functions that return such
vectors. Why is that fast?
The answer is move semantics: the language can transfer ownership of internal storage from one object to another without copying the contents. This chapter is about the intuition, not the full template machinery — that comes later if you go deep.
Copy vs. move, in pictures
Suppose a std::vector<int> owns a heap buffer of 1,000,000 ints.
A copy allocates a new buffer and duplicates every element:
A move just steals the pointer:
The buffer is unchanged. a is left in a valid but empty state
(safe to destroy or reassign). b now owns the data. Total work:
copying a few pointers and integers. Done.
std::move is not the operation — it's a cast
The name is a bit misleading. std::move(x) does not itself
move anything. It just tells the compiler "treat x as if it were
an rvalue (a temporary, eligible for moving from)."
The actual move happens when something accepts an rvalue — like a move constructor or move assignment operator. Many standard library types (vector, string, unique_ptr, ...) have move operations defined that pillage the source's internals.
std::vector<int> a = make_a_huge_vector(); // returns by value — typically moved
std::vector<int> b = std::move(a); // a's buffer transferred to b
// a is now valid but unspecified; safe to assign to or destroy.Why this matters for unique_ptr
std::unique_ptr cannot be copied (because then there would be two
owners). It can be moved, transferring ownership:
After the move, a is null and b is the sole owner. Exactly what
we want.
Return-by-value is now cheap
This is the practical takeaway every beginner needs:
Returning a large container by value is essentially free in modern C++. Either the compiler elides the copy entirely (RVO/NRVO), or the move constructor transfers the buffer.
So write:
std::vector<int> read_numbers(); // good — return by valuenot:
void read_numbers(std::vector<int>& out); // unnecessary obfuscation
std::vector<int>* read_numbers(); // avoid raw owning pointersThe simpler signature is also the more efficient one.
What about lvalues and rvalues?
Briefly, because the names matter:
- An lvalue is something that has a name and an address — a
variable, a member access. You can take
&x. - An rvalue (more precisely, an xvalue or prvalue) is a
temporary or an expression treated as one — a function result, a
literal, the result of
std::move.
Move operations are bound to rvalues, so casting an lvalue with
std::move lets you opt-in to moving. Use it when you genuinely
want to give up an object's contents — and don't use it on
return values that the compiler can already optimize, or on const
objects (it silently downgrades to a copy).
The Rule of Five
When you write your own resource-owning class, the special member functions you might want to define are:
- Destructor
- Copy constructor
- Copy assignment
- Move constructor
- Move assignment
If you define one of these, you usually need to consider all five.
The good news: by composing your class out of types that already
manage their own resources (std::vector, std::string,
std::unique_ptr, ...) you almost never need to write any of them.
The compiler-generated defaults will do the right thing.
This is sometimes called the Rule of Zero, and is the modern ideal: let the standard library own the resources, and your class needs no special members.
Test your understanding
What does std::move(x) actually do?
It moves x immediately and zeros it out.
It is a cast that lets x bind to an rvalue reference, enabling a move constructor or move assignment to transfer ownership from x.
It deletes x.
It marks x as const.
Why is returning a large std::vector by value cheap in modern C++?
The compiler stores it in a register.
The standard library uses a global pool.
The vector is either constructed directly into the caller's location (copy elision) or its internal pointer is moved instead of copying the elements.
Because std::vector is small.
Next: how to think about algorithms and data structures — what they cost and when to reach for which.