Visual walkthrough — Exception safety — basic, strong, no-throw guarantees
This is the picture-first companion to the parent topic. Read that for the definitions; read this to see the machinery.
Step 0 — The vocabulary, drawn before we use it
Before any derivation, three plain-English ideas, each pinned to a picture below.

We will follow one concrete operation the whole way: a class Buffer that owns a heap array, and we append some numbers to it. Appending needs a bigger array, so it must allocate and copy — the classic place an exception hides.
Step 1 — The naive in-place version, and the tripwire inside it
void append_naive(const int* src, std::size_t k) {
int* bigger = new int[n + k]; // (1) allocate — may throw bad_alloc
std::copy(data, data + n, bigger); // (2) copy old — a copy may throw
std::copy(src, src + k, bigger+n); // (3) copy new — may throw
delete[] data; // (4) free old
data = bigger; n += k; // (5) commit pointers
}Here data is the label pointing at our current array, n is how many numbers it holds, src is the incoming numbers and k is how many.

Step 2 — Move the risky work onto a stand-in copy
void append_strong(const int* src, std::size_t k) {
Buffer tmp(*this); // (A) copy — may throw, but only tmp is at stake
tmp.grow_in_place(src, k); // (B) risky work — happens on tmp
// ... commit comes next ...
}tmp is our stand-in — a second jar filled with the same contents as *this.

Step 3 — The commit must be a move that cannot throw
swap(*this, tmp); // (C) commit — noexcept, cannot throw
} // (D) ~tmp runs, freeing the OLD arrayfriend void swap(Buffer& a, Buffer& b) noexcept {
std::swap(a.data, b.data); // trade the two pointers
std::swap(a.n, b.n); // trade the two sizes
}In the figure, watch the two labels cross over. There is no instant in this crossover where a throw could leave us half-committed — the exchange is a sequence of trivial assignments, and if you froze the program between them the object is still fully valid (worst case it points at the new array with an old n for a nanosecond, but no exception can occur to freeze it there).

Step 4 — The complete timeline: every failure point covered
| Throw at | *this (visible) |
tmp (stand-in) |
Verdict |
|---|---|---|---|
| (A) copy | untouched (old) | half-built → auto-freed by ~tmp |
no effect, no leak ✅ |
| (B) grow | untouched (old) | partly grown → auto-freed by ~tmp |
no effect, no leak ✅ |
| (C) swap | cannot throw (noexcept) |
— | impossible ✅ |
| (D) destroy | new state, committed | freeing old array (dtor never throws) | already succeeded ✅ |

Step 5 — The degenerate cases (never skip these)
Case k = 0 (append nothing). The copy in (A) still runs, grow_in_place copies zero new numbers, swap commits an identical-valued buffer. No throw source is removed, no throw source added — still strong. Cost is a wasted copy; correctness is untouched.
Case self-append (src points inside data). Because (A) copies first, tmp holds an independent snapshot of the old numbers before any growing happens. So reading from src while writing into tmp never reads a clobbered value — the copy-first design makes self-reference safe for free, the same way copy-and-swap assignment is self-assignment-safe (see Copy-and-swap idiom).
Case the type's move throws. If we tried to move elements instead of copy them during growth and that move could throw, then a throw mid-growth would gut the source — and for a container like std::vector that means the strong guarantee is lost. This is exactly why the library uses std::move_if_noexcept: it copies unless the move is marked noexcept (see Move semantics and noexcept and std::vector reallocation strategy).

The one-picture summary

The whole idea in one frame: a throw-zone (copy + grow, drawn on the stand-in jar) followed by a safe-zone commit (the label swap). Every tripwire lives left of the swap; the swap itself is a wall no tripwire can cross. Therefore the visible object is either fully old or fully new — never in between.
Recall Feynman retelling — explain the walkthrough with no jargon
You have to renovate a jar without ruining the one on your shelf.
The clumsy way (Step 1): you tip the shelf-jar out onto the table and start pouring in the new stuff. If your phone rings (a throw) mid-pour, you've got a mess on the table that nobody cleans up (a leak) and a shelf-jar that's half-empty (broken).
The safe way: first make a second jar that's a perfect copy of the shelf one (Step 2). Do all the messy pouring into that second jar. Phone rings? Only the second jar is messy — and because it's a "self-cleaning" jar, it tidies itself the moment you drop it (RAII). The shelf-jar was never touched.
When the second jar is finally perfect, you don't pour it back — pouring could spill. Instead you just swap the two labels (Step 3): the shelf label now names the new jar, and the old jar gets the spare label and is thrown out. Swapping labels can never spill, so this last step is a wall no interruption can cross.
Check every possible ring of the phone (Step 4): before the swap it only hurts the spare jar; the swap can't be interrupted; after the swap you're already done. And the awkward cases (Step 5) — pouring nothing, pouring from the jar into itself, or using a "pour" that itself might spill — all behave, because we copied first and only trusted a move when it promised never to spill. That is copy-and-swap, and that is the strong guarantee.