latency.lab
Module 05

Rule of Five & Move Semantics

Level: core→advancedTime: ~2.5 hrsPrereq: Module 04

By the end you can

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.

value.cpp
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 temporaries

Copy 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.

movecopy.cpp
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 empty
What std::move actually is

std::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:

buffer.cpp
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_;
};
The Rule of Five

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 Rule of Zero (what you'll actually do)

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.

Build

Exercise 05.1 — Prove the move

Make copy vs move visible:

  • Write the Buffer class 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 Buffer objects into a std::vector and push_back a temporary. Watch the move constructor run instead of the copy.
  • Now rewrite Buffer to hold a std::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::move is 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.
Live

Loading starter code…

Live

Loading starter code…

Checkpoint · Move Semantics

6 questions · pass at 70%

Q01
What does std::move(x) actually do?
Whystd::move is an unconditional cast to rvalue. It performs no movement itself — it just enables a move constructor or move assignment to steal the resource.
Q02
You wrote a destructor that calls delete[] but did not write a copy constructor. What bug results when the object is copied?
WhyThe compiler-generated copy does a shallow pointer copy. Two objects now own the same buffer, and both delete[] it — a double free. This is why the Rule of Five exists.
Q03
After std::vector<int> c = std::move(a);, what is the state of a?
WhyA moved-from standard object is left in a valid but unspecified state — for vector, typically empty. You may safely assign to it or let it be destroyed.
Q04
The Rule of Zero recommends:
WhyIf your members manage their own resources, the default copy/move/destroy do the right thing, so you write zero special members. This is the preferred modern approach.
Q05
Why should move constructors be marked noexcept?
Whystd::vector will fall back to copying during growth unless the move constructor is noexcept, because it must preserve the strong exception guarantee. noexcept moves are what make vector growth cheap.
Q06
Which is an rvalue?
Whyx + 1 produces a nameless temporary — an rvalue. Named variables are lvalues.

Finished Module 05?

Pass the quiz to complete it automatically.

+ Note