Move Semantics & Smart Pointers
Reviewed & published by Brayan K
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 worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Tell lvalues apart from rvalues, and read an rvalue reference (T&&)
- Write a move constructor and move assignment that transfer ownership
- Use std::move to turn a copy into a cheap move
- Explain why moving avoids the cost of duplicating big buffers
- Apply the Rule of Five so all your special members agree
- See how std::vector moves elements when it reallocates
💡 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.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// A class that owns a "big" buffer so copies are visibly expensive.
class Buffer {
string label;
vector<int> data; // imagine this holds millions of ints
public:
// Normal constructor
Buffer(string l, int n) : label(l), data(n, 0) {
cout << label << ": constructed (" << n << " ints)" << endl;
}
// Copy constructor — runs when you COPY. It duplicates the whole buffer.
Buffer(const Buffer& other) : label(other.label), data(other.data) {
cout << label << ": COPIED (expensive — duplicated the buffer)" << endl;
}
// Move constructor — runs when you MOVE. It STEALS the buffer (a pointer
// swap), leaving 'other' empty but valid. noexcept lets vector use it.
Buffer(Buffer&& other) noexcept
: label(move(other.label)), data(move(other.data)) {
cout << label << ": MOVED (cheap — stole the buffer)" << endl;
}
int size() const { return (int)data.size(); }
};
int main() {
Buffer a("A", 1000000); // construct A
cout << "---" << endl;
Buffer b = a; // a is an lvalue -> COPY constructor runs
cout << "b has " << b.size() << " ints; a still has " << a.size() << endl;
cout << "---" << endl;
Buffer c = move(a); // move(a) is an rvalue -> MOVE constructor runs
cout << "c has " << c.size() << " ints" << endl;
cout << "a now has " << a.size() << " ints (moved-from = empty)" << endl;
// Expected output (order):
// A: constructed (1000000 ints)
// ---
// A: COPIED (expensive — duplicated the buffer)
// b has 1000000 ints; a still has 1000000
// ---
// A: MOVED (cheap — stole the buffer)
// c has 1000000 ints
// a now has 0 ints (moved-from = empty)
return 0;
}
// ✅ Expected output:
// A: constructed (1000000 ints)
// ---
// A: COPIED (expensive — duplicated the buffer)
// b has 1000000 ints; a still has 1000000
// ---
// A: MOVED (cheap — stole the buffer)
// c has 1000000 ints
// a now has 0 ints (moved-from = empty)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.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Box {
vector<int> data;
public:
Box(int n) : data(n, 1) {}
// Copy constructor — already written for you.
Box(const Box& other) : data(other.data) {
cout << "COPIED" << endl;
}
// 🎯 YOUR TURN — finish the MOVE constructor.
// Steal 'other.data' instead of copying it, and don't throw.
Box(Box&& other) ___ : data(___) { // 👉 add 'noexcept', then move(other.data)
cout << "MOVED" << endl;
}
int size() const { return (int)data.size(); }
};
int main() {
Box a(5);
Box b = move(a); // should trigger your MOVE constructor
cout << "b size: " << b.size() << endl;
// ✅ Expected output:
// MOVED
// b size: 5
return 0;
}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.
#include <iostream>
#include <string>
#include <utility> // std::move lives here
using namespace std;
int main() {
// 🎯 YOUR TURN — move a string instead of copying it.
string from = "a very long sentence pretending to be huge";
string to;
// 1) Move 'from' into 'to' (steal its characters, don't copy them)
to = ___; // 👉 move(from)
cout << "to = \"" << to << "\"" << endl;
cout << "from = \"" << from << "\" (moved-from: empty but valid)" << endl;
// ✅ Expected output:
// to = "a very long sentence pretending to be huge"
// from = "" (moved-from: empty but valid)
return 0;
}🔎 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.
string a = "Hello, this is a long string";
string b = move(a); // b steals a's buffer (pointer swap)
// b == "Hello, this is a long string"
// a == "" (valid but empty — its buffer now belongs to b)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.
class Widget {
int* data;
public:
Widget(int n) : data(new int[n]) {} // 1. constructor
~Widget() { delete[] data; } // 2. destructor
Widget(const Widget& o); // 3. copy constructor
Widget& operator=(const Widget& o); // 4. copy assignment
Widget(Widget&& o) noexcept // 5a. move constructor
: data(o.data) { o.data = nullptr; }
Widget& operator=(Widget&& o) noexcept { // 5b. move assignment
if (this != &o) { // guard against self-move
delete[] data; // free what we hold
data = o.data; // steal o's pointer
o.data = nullptr; // leave o safe to destroy
}
return *this;
}
};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.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
struct Item {
string name;
Item(string n) : name(n) { cout << name << ": built" << endl; }
Item(const Item& o) : name(o.name) { cout << name << ": COPIED" << endl; }
Item(Item&& o) noexcept : name(move(o.name)) { cout << "moved" << endl; }
};
int main() {
vector<Item> v;
v.reserve(2); // room for 2 — no reallocation yet
cout << "push #1" << endl; v.push_back(Item("one"));
cout << "push #2" << endl; v.push_back(Item("two"));
cout << "push #3 (capacity exceeded -> reallocate)" << endl;
v.push_back(Item("three")); // vector grows; existing items are MOVED
// Because Item's move ctor is noexcept, the vector MOVES the old items
// into the bigger buffer instead of COPYING them. Drop 'noexcept' and
// the vector falls back to copying for safety.
return 0;
}
// ✅ Expected output:
// push #1
// one: built
// moved
// push #2
// two: built
// moved
// push #3 (capacity exceeded -> reallocate)
// three: built
// moved
// moved
// movedCommon 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 |
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.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 🎯 MINI-CHALLENGE: a move-aware "Document" class
// 1. Give it a private vector<string> lines; and a string title;
// 2. Constructor: Document(string t) sets title and prints "<t>: created".
// 3. Add a COPY constructor that prints "<title>: copied".
// 4. Add a MOVE constructor (mark it noexcept) that move()s both members
// and prints "<title>: moved".
// 5. In main(): build d1, do Document d2 = d1; // should print "copied"
// then Document d3 = move(d1); // should print "moved"
//
// ✅ Expected output:
// Report: created
// Report: copied
// Report: moved
class Document {
// your code here
};
int main() {
// your code here
return 0;
}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
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 & Smart Pointer Internals — The C++ memory model, atomic operations, and smart pointer internals
- Quick reference: C++ cheat sheet › Memory & Ownership
Frequently asked questions
What is the difference between an lvalue and an rvalue?
An lvalue has a name and a stable address you can take with & — like a variable x. An rvalue is a temporary with no lasting identity — like the result of x + 1 or a literal. Moving is safe from rvalues because nobody else will use them afterwards.
What does std::move actually do?
Nothing at runtime — it does not move anything. std::move is just a cast that turns an lvalue into an rvalue reference, which makes the compiler pick the move constructor or move assignment instead of the copy versions. The actual stealing happens inside those move operations.
Why must a move constructor be noexcept?
std::vector only uses your move constructor during reallocation if it promises not to throw. Without noexcept, the vector copies every element instead (for the strong exception guarantee), silently throwing away the performance you wrote the move constructor to get.
Is it safe to use an object after I move from it?
You may assign to it or destroy it, but you must not assume anything about its value — a moved-from object is valid but unspecified. For standard types like string and vector it is typically left empty, but never rely on that across all types.
What is the Rule of Five?
If your class manually manages a resource and you define any one of the destructor, copy constructor, copy assignment, move constructor, or move assignment, you almost certainly need to define all five — they have to agree on how the resource is owned, copied, moved, and freed.