5.2.30 · D2C++ Programming

Visual walkthrough — noexcept specifier

1,732 words8 min readBack to topic

This page rebuilds the parent noexcept idea from the ground up, in pictures. We start with a totally empty box — a piece of memory — and watch, step by step, why the compiler and std::vector end up caring about a single promise. Every symbol is earned before it is used.


Step 1 — What "an exception escaping" even looks like

WHAT. A function is just a room you walk into, do some work, and walk out of. Normally you leave through the front door (return). An exception is an alternate exit — a trapdoor that yanks you out sideways, skipping the rest of the work.

WHY show this first. Everything about noexcept is a claim about which exits are allowed. We cannot talk about the promise until we can picture the two doors.

PICTURE. The room has two ways out. The black arrow is the normal return. The red arrow is the exception trapdoor — the thing noexcept promises will never be used.

Figure — noexcept specifier

Step 2 — The promise, drawn as a sealed door

WHAT. Writing noexcept after a function's signature welds the red trapdoor shut. You are telling the compiler: "this room has only the front door."

  • void f() — the function (the room).
  • noexcept — the specifier; it seals the exception exit. It is exactly noexcept(true).

WHY. Once the compiler believes the trapdoor is gone, it can throw away the cleanup scaffolding it would otherwise build around the room to safely handle a sideways exit. Less scaffolding = smaller, faster code. That saving is the entire reward for making the promise.

PICTURE. Same room as Step 1, but the red trapdoor is now bolted shut. The scaffolding around it (the dashed cleanup machinery) has been removed.

Figure — noexcept specifier

Step 3 — Breaking the promise: the terminate landmine

WHAT. Suppose you sealed the trapdoor (noexcept) but then, inside the room, you throw an exception that tries to escape anyway. The exception rushes toward an exit that no longer exists — the cleanup scaffolding was deleted in Step 2. There is nowhere for it to go.

WHY. The runtime cannot let a half-escaped exception wander through a room with no unwinding information — that is undefined chaos. So it does the only safe thing: it detonates.

  • left side — the broken promise (a real escape, not a caught one).
  • right side — std::terminate fires: the whole program dies immediately, with no guarantee of running destructors above this point.

PICTURE. The exception (red) slams into the welded trapdoor. Because the escape route is gone, the impact triggers std::terminate() — the program ends.

Figure — noexcept specifier

Step 4 — Asking the promise a question: the noexcept operator

WHAT. The same word has a second job. noexcept(expr) is a question the compiler answers at compile time: "is this expression guaranteed not to throw?" It hands back a bool.

  • expr — a piece of code, but it is unevaluated (like sizeof): nothing actually runs.
  • the result is a compile-time value, so it can feed a constexpr (see constexpr).

WHY. We need a way for generic code to look at some type T and ask "can I trust your operations?" without running them. The operator is that inspection tool.

PICTURE. A magnifying glass (red) hovers over a function and reads its door: sealed → answers true; open trapdoor → answers false.

Figure — noexcept specifier

Step 5 — The scene where it pays off: a vector runs out of room

WHAT. A std::vector keeps its elements in one solid block of memory. When it fills up, it must grab a bigger block and transfer every element across.

WHY set this up. This transfer is the exact moment where the promise from Step 2 changes the machine's decision. We first need to see the transfer to see the danger.

PICTURE. The old buffer (black, full) and a new, larger buffer (black, empty). Elements must travel from old to new. The element in flight is the object we are about to worry about (red).

Figure — noexcept specifier

Step 6 — The danger: a throwing move mid-transfer

WHAT. To transfer, the vector can move each element (steal its guts — cheap; see Move semantics) or copy it (duplicate — costly). Suppose it moves and, halfway through, one move throws.

WHY this is a disaster. Now some elements live in the new buffer, and the originals in the old buffer were already gutted by the move. There is no clean state to fall back to — the data is torn in half. This breaks the Strong exception guarantee (the rule that a failed operation must leave things exactly as they were).

PICTURE. Mid-transfer, element 3's move throws (red burst). Left buffer: gutted originals. Right buffer: a few survivors. The dashed line marks the corruption — no way back.

Figure — noexcept specifier

Step 7 — The fix the vector chooses, decided by the promise

WHAT. So the vector asks the Step-4 question about T's move constructor:

  • T(std::move(x)) — constructing a T by moving from x.
  • the operator returns the promise on T's move constructor.
  • true → moving cannot throw → safe to gut originals → move.
  • false → a move might throw mid-transfer (Step 6) → keep originals intact → copy, and destroy originals only after all copies succeed.

WHY. If moving can never throw, the Step-6 disaster is impossible, so the vector fearlessly takes the fast lane. If it might throw, the vector plays safe: copies leave the originals untouched, so a thrown copy loses nothing. This exact decision is bottled into the standard helper std::move_if_noexcept.

PICTURE. A fork in the road. The red signpost is the noexcept answer; true steers onto the MOVE track, false onto the COPY track.

Figure — noexcept specifier

Step 8 — The degenerate cases (so no scenario surprises you)

WHAT & WHY. Corners the fast/slow story must also survive:

  • Empty vector (0 elements): reallocation transfers nothing, so the move-vs-copy choice never fires. Both branches do the same "nothing." No danger, no decision.
  • A type with no move constructor (only a copy): the noexcept(...) question about the move falls back to copying anyway — same safe track as false.
  • Throw caught inside the move: if T's move throws internally but catches it and is declared noexcept, the operator returns true — the promise is what's read, not the internals. (If it lies and escapes, Step 3's landmine fires.)
  • A noexcept(false) move that never actually throws: still read as "might throw," so the vector copies. The promise, not the runtime behaviour, drives the choice — an honest noexcept is what unlocks speed.

PICTURE. Four tiny panels: empty box (no transfer), copy-only type, caught-inside, and the honest-but-unmarked type — each routed to its track, the deciding label in red.

Figure — noexcept specifier

The one-picture summary

One diagram compresses the whole chain: seal the trapdoor → compiler drops scaffolding → the operator can read the seal → vector takes the fast MOVE track. Break the seal and the same path leads to std::terminate().

Figure — noexcept specifier

seals trapdoor

true

false

promise broken

mark move noexcept

compiler drops cleanup scaffolding

noexcept operator reads the seal

vector MOVES elements fast

vector COPIES elements safe

std terminate program dies

Recall Feynman retelling — the whole walkthrough in plain words

A function is a room with two exits: the normal door and a secret trapdoor for emergencies (exceptions). Saying noexcept welds the trapdoor shut, and because the builder trusts there's no trapdoor, he removes all the safety scaffolding around it — that's why your code gets leaner and faster. But if an emergency does try to use the sealed trapdoor, there's no safe way out, so the building is demolished on the spot (std::terminate). Now picture a vector that outgrew its box and must carry every item to a bigger box. Carrying by "moving" is fast but empties the old box as you go — if you drop something halfway, both boxes are ruined. So before choosing, the vector reads the seal on each item's move: sealed (noexcept true) means dropping is impossible, so it moves fearlessly and fast; unsealed means it plays safe and copies, keeping the originals until every copy lands. One honest promise, and the whole machine speeds up.