Visual walkthrough — Move constructor and move assignment — Rule of Five
This is the picture-first companion to the parent topic. Read that for the code; read this to see it.
Step 0 — The vocabulary, drawn (nothing assumed)
Before line one of any derivation, three words must become pictures.
WHAT we drew: a stack box b containing two fields — data_ (an arrow) and size_ (a count) — with the arrow reaching across to a heap array of integers.
WHY we need this: a "copy" and a "move" differ only in what happens to these arrows. You cannot see the difference until you can see the arrows.
PICTURE:

Step 1 — The wasteful way: a deep copy
Let us first draw the thing we want to avoid, so the win is obvious.
WHAT happens in a copy Buffer c = b; — the copy constructor Buffer(const Buffer& o) runs. It (1) asks the heap for a brand-new array of the same size, then (2) copies every integer across, one by one.
WHY it must be this expensive: after the copy, the original b is still alive and must keep working. Two independent owners means two independent buffers — you cannot share, or destroying one would wreck the other.
PICTURE: two separate heap arrays, two separate arrows. The red array is the freshly allocated one.

Step 2 — When is copying pure waste? Meet the rvalue
We only steal from something that is already dying. So we must learn to see "dying."
WHAT we drew: two arrows into the heap — a solid one from the named b (safe, keeps living) and a fading one from a temporary that has no name and is about to vanish.
WHY this matters: stealing memory from something that lives on = disaster (you'd rip the rug out). Stealing from something already vanishing = free lunch. The C++ compiler recognises rvalues automatically and calls the move constructor for them.
PICTURE:

Step 3 — The move, frame 1: steal the arrow
Now the payoff. Buffer c = std::move(b); invokes the move constructor Buffer(Buffer&& o).
WHAT the first line does:
The new object c copies the arrow itself (8 bytes) and the count — not the array. No new, no loop.
WHY this is safe: o is an rvalue (Step 2), doomed anyway. We don't need a second array because the source won't need its own copy — it's leaving.
PICTURE: for one dangerous instant, both arrows point at the same red heap array.

Step 4 — The move, frame 2: sterilize the source
WHAT the second pair of lines does:
nullptr is the "arrow that points at nothing" — the null pointer. We aim the source's arrow into the void and zero its count.
WHY we must: when o's destructor eventually runs delete[] o.data_, deleting nullptr is defined to do nothing. So the array we stole is freed exactly once, by c, later. Ownership is now unique again.
PICTURE: the source box is now empty (arrow dangling into nothing); the red array has exactly one owner, c.

Step 5 — Degenerate case A: moving into an object that already owns memory
The constructor (Steps 3–4) had nothing to clean up. But move assignment c = std::move(b) runs when c is already alive and already owns an array. Extra care needed.
WHAT move assignment does, in order:
delete[] data_;— free our own current array first (else it leaks — nobody else points to it).data_ = o.data_; size_ = o.size_;— steal (Step 3).o.data_ = nullptr; o.size_ = 0;— sterilize (Step 4).
WHY the order: if we stole first and freed second, we'd lose the pointer to our old array before freeing it → memory leak.
PICTURE: the destination's old array is freed (crossed out), then the source's array is adopted.

Step 6 — Degenerate case B: self-move x = std::move(x)
The nastiest edge case. It looks absurd but arises inside std::sort, std::swap on equal elements, etc.
WHAT goes wrong without a guard: Step 5's line 1 does delete[] data_ — but data_ and o.data_ are the same array here. We just deleted it. Then line 2 reads o.data_, now a dangling pointer to freed memory. Undefined behaviour.
WHY the fix works: the guard if (this != &o) compares the two objects' addresses. If they are the same object, we skip the whole steal-and-free dance and leave x untouched — which is exactly correct.
PICTURE: one box, one arrow — this and &o are the same address; the guard turns the operation into a no-op.

Step 7 — The full case table (never hit an unshown scenario)
WHY all five members exist: each row above needs its matching member, plus the destructor to free at the end. Miss one and that row silently misbehaves (see Rule of Three for the pre-move ancestor, and Rule of Zero to avoid writing any of them).
The one-picture summary

This single frame stacks the whole story: copy builds a second red array (expensive, both live); move re-aims one arrow and empties the source (cheap, ownership transferred); the crossed-out array in the corner is the leak/double-free you avoid by freeing-then-stealing and nulling the source.
Recall Feynman: the whole walkthrough in plain words
Every object is a little box holding an arrow to its big stuff sitting in a warehouse (the heap). Copying means renting a second warehouse unit and hauling identical boxes into it — slow, and only worth it if both people keep living. But when the seller's box is being thrown in the bin anyway (an rvalue), you don't rent anything: you just grab their arrow (Step 3 — "steal"), so now your box points at their warehouse. For one scary moment two arrows point at the same unit, so you immediately snap the seller's arrow off and point it at nothing (Step 4 — "sterilize"), so when the bin-truck crushes their box it doesn't accidentally empty your warehouse. If your box already had its own warehouse, empty that one out first so it isn't forgotten and left paying rent forever (Step 5). And if — through some weird machinery — the seller and buyer turn out to be the same person, just check their address and do nothing (Step 6). Do all five chores and nobody ever fights over one warehouse, and nobody pays double rent.
Connections
- std::move and rvalue references — Step 2's "permission sticker" explained fully
- Copy elision and RVO — when even the cheap move in Step 3 is skipped entirely
- Rule of Three — the pre-C++11 subset (destructor, copy-ctor, copy-assign)
- Rule of Zero — let
std::vector/Smart pointers (unique_ptr / shared_ptr) do all five for you - Exception safety guarantees — why the
noexcepton the move matters to containers