Move Semantics
By the end of this lesson you'll know exactly why std::move makes code faster, how to write a move constructor and move assignment that steal resources instead of copying them, and how the standard library uses moves under the hood — so your own types stop paying for needless copies.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Imagine moving house. A copy is photocopying every book, re-buying every piece of furniture, and rebuilding it all at the new address — slow and wasteful. A move is loading the moving truck and driving it over: the same stuff arrives at the new place, and the old house is now empty. Nothing was duplicated; ownership of your belongings simply transferred . That is exactly what a move constructor does — it hands the internal buffer to the new object and leaves the old one empty but still safe to clean up.
1. lvalues, rvalues, and T&&
Every expression in C++ is either an lvalue or an rvalue . An lvalue has a name and a lasting address — like a variable x ; you can take its address with &x . An rvalue is a temporary with no lasting identity — like the literal 42 , or the result of x + 1 ; it vanishes at the end of the line. This matters because you can safely steal from an rvalue: nothing else will ever look at it again.
A plain reference T& binds to lvalues. The new tool is the rvalue reference , written T&& (two ampersands), which binds to rvalues — to temporaries. That double-ampersand parameter is how a function says "I will only run for throwaway values, so I'm allowed to gut them."
2. std::move and the Move Constructor
std::move sounds like it moves data, but it does nothing at runtime . It is just a cast that turns an lvalue into an rvalue reference, so the compiler picks the move constructor ( T(T&&) ) instead of the copy constructor ( T(const T&) ). The move constructor is where the real work happens: it steals the source's internal buffer — usually a single pointer swap — instead of duplicating millions of elements. The source is left empty but valid.
Read this worked example and run it. The class prints whether a copy or a move ran, so you can see exactly which constructor the compiler chose.
Your turn. The class below has a working copy constructor; the move constructor is missing two pieces. Fill in the ___ blanks so it steals the buffer and promises not to throw.
Now practise calling std::move yourself. Moving a std::string hands its character buffer to the destination and leaves the original empty — no characters are copied.
🔎 Deep Dive: why moving is cheap
A std::string or std::vector is a small handle (a pointer to a heap buffer, plus a size and capacity). Copying allocates a brand-new buffer and copies every byte — O(n). Moving copies just the pointer, size, and capacity, then zeroes out the source so it doesn't free the buffer you stole — O(1), no matter how big the data is.
That is the whole point: a move transfers ownership of the existing resource instead of building a second copy of it.
3. Move Assignment and the Rule of Five
A move constructor builds a new object from a temporary. Move assignment ( operator=(T&&) ) instead replaces an existing object: it must first free whatever it already holds, then steal the source's resource. The Rule of Five says that once you manually manage a resource and write any one of these five special members, you should write all five so they agree on ownership:
The five special members
Destructor, copy constructor, copy assignment, move constructor, move assignment. Notice the move assignment guards against self-move with if (this != &o) before it deletes anything — without that check, assigning an object to itself would free its own buffer first.
In real code you'd usually let members like std::vector manage the memory so the compiler generates all five correctly for you. Write them by hand only when you own a raw resource.
4. How std::vector Uses Moves
When a std::vector runs out of capacity, it allocates a bigger buffer and transfers the existing elements into it. If your type's move constructor is marked noexcept , the vector moves each element (cheap). If it isn't, the vector copies them instead — it needs the strong exception guarantee, and a move that might throw could leave it in a broken state. That one keyword is the difference between fast and slow growth.
Common Errors (and the fix)
- Using a moved-from object: after auto b = move(a); , reading a 's value is a bug — it is valid but unspecified. Only assign to it or let it be destroyed.
- Missing noexcept on the move constructor: vector silently falls back to copying during reallocation, so your "fast" type quietly runs slow. Always write Buffer(Buffer&&) noexcept .
- Self-move without a guard: in move assignment, delete -ing before checking if (this != &o) frees the very buffer you're about to steal. Guard against x = move(x); .
- Moving a const object: move(c) on a const T c; produces a const T&& , which binds to the copy constructor — you silently get a copy, not a move. Don't mark movable values const .
- Returning move(local) : return move(x); for a local actually disables copy elision (RVO). Just return x; — the compiler already moves or elides.
📋 Quick Reference
Concept
Syntax
Meaning
lvalue reference
T& r = x;
Binds to a named object
rvalue reference
T&& r = T();
Binds to a temporary
Cast to rvalue
std::move(x)
Enables a move (no runtime cost)
Move constructor
T(T&&) noexcept
Build by stealing resources
Move assignment
T& operator=(T&&)
Replace by stealing resources
Rule of Five
~T, copy x2, move x2
Define all five together
Frequently Asked Questions
Mini-Challenge: a move-aware Document
No blanks this time — just a brief and an outline. Build a class with both a copy and a move constructor, then prove which one runs by copying once and moving once. Check your output against the comments.
Pro Tips
- 💡 Always mark move operations noexcept so std::vector and friends actually move during reallocation.
- 💡 Prefer the Rule of Zero: if your members ( vector , string , smart pointers) already manage their resources, write none of the five and let the compiler generate them.
- 💡 Don't return move(local) : plain return local; lets the compiler elide the move entirely.
- 💡 Never read a moved-from value — treat it as empty until you reassign it.
🎉 Lesson Complete
- ✅ lvalues have a name and address; rvalues are temporaries you can safely steal from
- ✅ T&& is an rvalue reference — it binds to temporaries
- ✅ std::move is a cast that selects the move constructor / move assignment
- ✅ Moving transfers ownership of a buffer in O(1) instead of copying it
- ✅ The Rule of Five: define the destructor, both copies, and both moves together
- ✅ Mark moves noexcept so std::vector moves (not copies) on reallocation
- ✅ Next lesson: the Modern C++ Memory Model — how the compiler and CPU order memory
Practice quiz
What is the difference between an lvalue and an rvalue?
- An lvalue is always const; an rvalue is mutable
- An lvalue is on the heap; an rvalue is on the stack
- An lvalue has a name and a stable address; an rvalue is a temporary with no lasting identity
- There is no difference
Answer: An lvalue has a name and a stable address; an rvalue is a temporary with no lasting identity. An lvalue (like a variable x) has a name and address; an rvalue (like 42 or x+1) is a throwaway temporary you can steal from.
What does std::move actually do at run time?
- Nothing at run time — it is just a cast to an rvalue reference
- It copies the data to a new location
- It frees the source object
- It allocates new memory
Answer: Nothing at run time — it is just a cast to an rvalue reference. std::move moves nothing; it casts an lvalue to an rvalue reference so the compiler picks the move constructor/assignment.
Which reference type binds to a temporary (rvalue)?
- int&
- const int*
- int*&
- int&&
Answer: int&&. int&& is an rvalue reference and binds to temporaries; int& binds to named lvalues.
After string b = std::move(a); for a std::string a, what is a's state?
- a still holds its original characters
- a is valid but unspecified — for string typically empty
- a is destroyed and unusable
- a now points to b
Answer: a is valid but unspecified — for string typically empty. A moved-from object is valid but unspecified; std::string is typically left empty. Don't rely on its value.
Why does moving a std::vector cost O(1) while copying costs O(n)?
- Moving copies just the pointer, size, and capacity, then zeroes the source
- Moving compresses the data
- Copying skips the elements
- Moving uses a faster memcpy of every element
Answer: Moving copies just the pointer, size, and capacity, then zeroes the source. A move steals the heap buffer by copying the small handle (pointer/size/capacity); copying duplicates every element.
Why must a move constructor be marked noexcept?
- It is required for the class to compile
- It makes the move faster by skipping checks
- So std::vector moves (rather than copies) elements when it reallocates
- It prevents the destructor from running
Answer: So std::vector moves (rather than copies) elements when it reallocates. std::vector only uses your move ctor on reallocation if it promises not to throw; without noexcept it copies for the strong guarantee.
What are the five special members in the Rule of Five?
- Constructor, destructor, copy ctor, copy assignment, swap
- Destructor, copy ctor, copy assignment, move ctor, move assignment
- Constructor, destructor, operator+, operator==, operator<<
- Two constructors and three destructors
Answer: Destructor, copy ctor, copy assignment, move ctor, move assignment. The Rule of Five: destructor, copy constructor, copy assignment, move constructor, and move assignment all go together.
Why does move assignment guard with if (this != &o) before deleting?
- To skip the work when objects are equal in value
- To make the operation noexcept
- It is purely stylistic
- To avoid freeing its own buffer on a self-move like x = std::move(x);
Answer: To avoid freeing its own buffer on a self-move like x = std::move(x);. Without the self-move guard, deleting before stealing would free the very buffer it is about to take.
What happens if you call std::move on a const object, like move(c) where c is const?
- It moves as usual
- It produces a const T&&, which binds to the copy constructor — you silently get a copy
- It is a compile error
- It throws at run time
Answer: It produces a const T&&, which binds to the copy constructor — you silently get a copy. move(const T) yields const T&&, which can't bind to a non-const move ctor, so the copy constructor runs instead.
For a local variable, what should you write to return it most efficiently?
- return std::move(local);
- return &local;
- return local; — letting the compiler elide (RVO)
- return *local;
Answer: return local; — letting the compiler elide (RVO). return local; lets the compiler apply copy elision/RVO; return std::move(local); actually disables that optimisation.
Continue this course
- Previous: Memory Management
- Next: Modern C++ Memory Model