This is the module that most self-taught programmers skip and most interviews probe. Move semantics is why modern C++ can be both safe and fast. Get it and you're ahead of most candidates.
lvalues and rvalues
Loosely: an lvalue has a name and a stable address (you can take & of it). An rvalue is a temporary — the result of an expression that's about to disappear.
int x = 5; // x is an lvalue
int y = x + 1; // (x + 1) is an rvalue — a temporary with no name
int& lr = x; // lvalue reference, binds to lvalues
int&& rr = x + 1; // rvalue reference (&&), binds to temporariesCopy vs move
Copying duplicates the resource. Moving steals it — transfers ownership from a temporary that's about to die, leaving the source in a valid-but-empty state. For a type holding a heap buffer (like a string or vector), copying allocates and duplicates; moving just swaps pointers. Moving is often orders of magnitude cheaper.
std::vector<int> a = {1,2,3};
std::vector<int> b = a; // COPY: b gets its own duplicate of the data
std::vector<int> c = std::move(a); // MOVE: c steals a's buffer; a is now emptystd::move does not move anything. It's a cast that says "treat this lvalue as an rvalue, you may steal from it." The actual stealing happens in a move constructor or move assignment operator. After moving from an object, it's valid but unspecified — only safe to destroy or reassign.
The five special members
If your class manages a resource, the compiler generates (or you write) five special functions:
class Buffer {
public:
Buffer(size_t n) : data_(new int[n]), size_(n) {} // constructor
~Buffer() { delete[] data_; } // 1. destructor
Buffer(const Buffer& o) : data_(new int[o.size_]), size_(o.size_) { // 2. copy ctor
std::copy(o.data_, o.data_ + size_, data_);
}
Buffer& operator=(const Buffer& o) { // 3. copy assign
if (this != &o) { // self-assignment guard
delete[] data_;
size_ = o.size_;
data_ = new int[size_];
std::copy(o.data_, o.data_ + size_, data_);
}
return *this;
}
Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) { // 4. move ctor
o.data_ = nullptr; o.size_ = 0; // leave source empty
}
Buffer& operator=(Buffer&& o) noexcept { // 5. move assign
if (this != &o) {
delete[] data_;
data_ = o.data_; size_ = o.size_;
o.data_ = nullptr; o.size_ = 0;
}
return *this;
}
private:
int* data_;
size_t size_;
};If you write any one of {destructor, copy ctor, copy assign, move ctor, move assign}, you almost certainly need all five. Writing a destructor but not a copy constructor is the classic bug: the default copy does a shallow pointer copy, then both objects delete[] the same buffer — a double-free.
The best code writes none of the five. If you store your data in a std::vector or std::unique_ptr instead of a raw int*, those members already do all five correctly, so the compiler-generated versions of your class just work. This is why the whole Buffer above collapses to a class holding a std::vector<int>. Learn the five so you understand what's happening; use the zero in practice.
Why moves make your code fast
When you return a big object from a function, or push_back a temporary into a vector, the compiler uses moves automatically. No expensive copy. This is invisible and free once your types are movable — which they are for free if you follow the Rule of Zero.
Exercise 05.1 — Prove the move
Make copy vs move visible:
- Write the
Bufferclass above, adding a print in each of the five functions (e.g. "copy ctor", "move ctor"). - Create a
Buffer, copy it, and move it. Observe which functions fire. - Put
Bufferobjects into astd::vectorandpush_backa temporary. Watch the move constructor run instead of the copy. - Now rewrite
Bufferto hold astd::vector<int>and delete all five special members. Confirm it still copies and moves correctly — the Rule of Zero in action.
Takeaways
- lvalue = named, addressable; rvalue = temporary.
&&is an rvalue reference. - Copy duplicates; move steals from a dying temporary, leaving it empty-but-valid.
std::moveis just a cast to rvalue; the move ctor/assign does the real work.- Rule of Five: write one of the special members, write all five — or you'll get double-frees.
- Rule of Zero: hold RAII members (vector, unique_ptr) and write none of them. Preferred.
Loading starter code…
Loading starter code…
Checkpoint · Move Semantics
6 questions · pass at 70%
std::move(x) actually do?std::vector<int> c = std::move(a);, what is the state of a?noexcept?Finished Module 05?
Pass the quiz to complete it automatically.