Intuition Why a "scenario matrix" for a keyword?
noexcept has no numbers, but it does have a fixed set of case classes : the two roles (specifier vs operator), the two truth values (true/false), the escape-vs-caught boundary, the reallocation branch in std::vector , and the degenerate corners (destructors, throw() from a deleted function, empty expressions). If we hit every cell , you will never meet a noexcept situation you haven't already seen worked out. This page is the exhaustive tour.
This page is a child of noexcept specifier . If a term below feels unearned, the parent builds it first.
Every question noexcept can ask falls into one of these cells. Each worked example is tagged with the cell(s) it covers.
#
Case class
The concrete question
Expected outcome
A
Operator on a promised function
noexcept(safe()) where safe is noexcept
true
B
Operator on a default function
noexcept(risky()) where risky may throw
false
C
Escape (contract broken)
throw leaves a noexcept function
std::terminate()
D
Caught inside (contract kept)
throw is caught within the noexcept function
fine, no terminate
E
Conditional true branch
a template whose move can't throw
noexcept(...) → true
F
Conditional false branch
a template whose move might throw
noexcept(...) → false
G
std::vector reallocation
grow a vector of a move-noexcept type vs not
move vs copy
H
Degenerate: destructor
implicit noexcept on ~S
throwing → terminate
I
Degenerate: empty / non-throwing ops
noexcept(1 + 2), noexcept(x) on plain reads
true
J
Exam twist: nested operator
noexcept(noexcept(x)) — which is specifier?
outer = specifier
Rows A/B are the two truth values of the operator; C/D are the two sides of the escape boundary; E/F are the two branches of a conditional; G is the payoff; H/I/J are the degenerate and trick corners. Nothing about noexcept lives outside this table.
Worked example Query a promised and a default function
int safe () noexcept { return 42 ; } // promises: never throws
int risky () { throw 1 ; } // no promise (default may-throw)
constexpr bool s = noexcept ( safe ()); // ?
constexpr bool r = noexcept ( risky ()); // ?
Forecast: guess s and r before reading on.
Look at safe's declaration. It carries the noexcept specifier.
Why this step? The operator noexcept(x) does not run x — it reads the declared promise . safe promises no throw, so the operator answers true. → s == true.
Look at risky's declaration. No specifier, so it is noexcept(false) by default.
Why this step? The operator must assume the worst for anything not promised non-throwing. The throw 1 inside is irrelevant — even an empty default function gives false, because the operator trusts declarations , not bodies. → r == false.
Verify: s is true, r is false. Because both are constexpr the compiler proves this at compile time — you could put static_assert(s && !r); and it holds.
Cell A gave true, Cell B gave false. Both truth values now exist.
Worked example Break the promise
void liar () noexcept { throw std :: runtime_error ( "oops" ); }
int main () {
try { liar (); }
catch (...) { /* does this run? */ }
}
Forecast: does the catch catch it?
liar promised noexcept. The compiler, trusting this, emitted no unwinding tables for liar (see Exception handling in C++ ).
Why this step? The whole cost saving of the promise is skipping that machinery. Now there is nothing to walk the stack with.
The throw fires and tries to leave liar. It reaches the function boundary — that is the moment of escape .
Why this step? Escape is the trigger, not the throw itself. Look at std::terminate : the boundary crossing is what the runtime watches.
Runtime calls std::terminate(). The program dies.
Why this step? With no unwinding info, rethrowing is impossible; the only defined action is to stop. The catch (...) in main never runs .
Verify (mental sanity check): if the catch had run we'd have recovered from a promise-break — which would make the promise meaningless. Terminate is the only self-consistent outcome.
Worked example Throw and swallow, all within the function
void tidy () noexcept {
try {
throw 1 ; // thrown...
} catch ( int ) {
// ...caught right here. Nothing escapes.
}
}
int main () { tidy (); } // returns normally
Forecast: terminate or not?
Identify what the contract forbids. Re-read noexcept specifier : it forbids an exception escaping , not the mere existence of throw.
Why this step? People confuse "has a throw" with "throws out". The word in the contract is escape .
Trace the exception's path. throw 1 → immediately matched by catch (int) in the same function → destroyed there.
Why this step? Because it never reaches the function boundary, step 2 of Example 2 never happens.
Function returns normally. No terminate.
Verify: the boundary is never crossed by a live exception, so the promise ("nothing escapes") is kept . Contrast with Example 2 where the throw reached the boundary — same keyword, opposite fate, decided purely by escape vs caught .
noexcept computed from the type
struct Fast { Fast (Fast && ) noexcept ; }; // move promises no throw
struct Slow { Slow (Slow && ); }; // move may throw
template < class T >
void relocate ( T & x ) noexcept ( noexcept ( T ( std :: move (x))) ) { /* ... */ }
Forecast: is relocate<Fast> noexcept? Is relocate<Slow> noexcept?
Split the two noexcepts. Outer noexcept( ... ) right after the () = the specifier (the promise). Inner noexcept( T(std::move(x)) ) = the operator (a compile-time bool).
Why this step? This is the Move semantics idiom: "I promise exactly when moving T can't throw." Getting the roles straight is Cell J's whole point, previewed here.
Instantiate with Fast. Inner operator asks: is Fast(std::move(x)) non-throwing? Fast's move ctor is declared noexcept → inner is true → specifier is noexcept(true). Cell E.
Instantiate with Slow. Inner operator asks the same of Slow. Its move ctor has no promise → inner is false → specifier is noexcept(false). Cell F.
Verify: noexcept(relocate<Fast>(f)) == true and noexcept(relocate<Slow>(s)) == false. The single template produced both truth values, driven entirely by the type — exactly the conditional the parent promised.
Worked example Growing a vector — move or copy?
A std::vector holding 4 elements is full and must grow. It allocates a bigger buffer and must transfer the 4 elements across.
Forecast: for vector<Fast> vs vector<Slow>, does it move or copy the elements?
State the danger. Transferring element 3 of 4 could throw. If we were moving , elements 0–2 are already gutted in the old buffer and element 3 is half-built in the new one — corrupted state, the Strong exception guarantee is dead.
Why this step? The strong guarantee says: if an operation throws, the container is unchanged. Look at the red buffer in the figure — a thrown move leaves it unrecoverable.
vector<Fast>: moves are noexcept. They cannot throw, so the corruption scenario is impossible. vector uses std::move_if_noexcept, which returns an rvalue → move (the green fast path).
vector<Slow>: moves may throw. To stay safe, move_if_noexcept returns an lvalue → copy . If a copy throws, the originals in the old buffer are untouched, so the container is unchanged (blue safe path).
Verify (count the work): with N = 4 elements, vector<Fast> does 4 cheap moves; vector<Slow> does 4 full copies plus keeps the old buffer alive until success. Same result, different cost — decided by one keyword on the move constructor.
Worked example A throwing destructor
struct S {
~S () { throw std :: runtime_error ( "bad" ); } // no explicit noexcept written
};
int main () { S s; } // s is destroyed at end of scope
Forecast: does the missing noexcept mean "may throw", so this is fine?
Recall the implicit rule. Since C++11, Destructors are implicitly noexcept even when you write nothing.
Why this step? So ~S() behaves as if you wrote ~S() noexcept. The absence of the keyword does not make it may-throw here (unlike ordinary functions in Cell B).
The destructor throws → escapes. This is Cell C in disguise: an escape from a noexcept function.
Why this step? The escape rule doesn't care how the function got its promise — implicit or explicit, escape = terminate.
std::terminate() fires.
Verify: the language bans throwing destructors precisely because a destructor may run during the unwinding of another exception — two live exceptions at once is undefined chaos. Making ~S noexcept by default converts that chaos into a clean, immediate terminate.
Worked example Pure computations and plain reads
int x = 5 ;
constexpr bool a = noexcept ( 1 + 2 ); // ?
constexpr bool b = noexcept (x); // ?
constexpr bool c = noexcept (x * x - 3 ); // ?
Forecast: guess all three.
Check each operation for a throw path. Built-in int arithmetic and reading a variable have no throwing operations — no allocation, no user code, no function call that could throw.
Why this step? The operator's rule (parent [!formula]): true iff every operation in the expression is known non-throwing.
All three expressions are pure built-ins. So each operator yields true. → a == b == c == true.
Why this step? These are the degenerate baseline: with nothing that can throw, the answer is trivially true. This is why noexcept on a leaf function is often justified.
Verify: a && b && c is true. Even integer overflow (which is UB, not an exception) doesn't make the operator false — the operator only tracks throwing , not all misbehaviour. Ties back to the mistake "noexcept ≠ safe."
Worked example Which noexcept is which?
template < class T >
void ping ( T && t ) noexcept ( noexcept ( f ( std :: forward < T >(t)) ) );
Forecast: in noexcept( noexcept(...) ), which is the promise and which is the bool?
Locate the position of the outer noexcept. It sits immediately after the parameter list (T&& t).
Why this step? Position decides role. A noexcept(...) in the specifier slot (after the signature) is always the specifier — the promise.
Read the inner noexcept. It is inside the parentheses of the outer one, so it is an operator producing a bool: "is f(std::forward<T>(t)) non-throwing?"
Why this step? An operator can appear anywhere a bool is needed; here it feeds the specifier.
Combine. "ping promises no-throw exactly when calling f on the forwarded argument can't throw." Outer = specifier, inner = operator.
Verify: substitute a concrete f. If f is declared noexcept, inner → true, so ping is noexcept(true). If f may throw, inner → false, so ping is noexcept(false) and propagates the throw normally (no terminate — because the promise was honestly false). Both branches self-consistent.
Recall Did every cell get hit?
Which examples cover the two operator truth values? ::: Example 1 (Cells A true, B false).
Which two examples are the escape-vs-caught pair? ::: Example 2 (Cell C, escape → terminate) and Example 3 (Cell D, caught → fine).
Which example produces both conditional-noexcept branches? ::: Example 4 (Cells E and F).
Which example is the vector move-vs-copy payoff? ::: Example 5 (Cell G).
Which examples are the degenerate corners? ::: Example 6 (Cell H, destructor), Example 7 (Cell I, pure expressions).
Which example is the specifier-vs-operator exam twist? ::: Example 8 (Cell J).
Mnemonic The whole page in one line
"Ask, Escape, Choose." The operator asks (A/B/E/F/I/J), the boundary decides escape (C/D/H), and vector chooses move or copy (G).
What decides whether a throw in a noexcept function calls terminate? Whether it escapes the function; caught internally is fine, crossing the boundary is fatal.
For vector<Fast> (move noexcept) vs vector<Slow> (move may throw), what does reallocation do? Moves the Fast elements (fast path), copies the Slow elements (safe path) to preserve the strong guarantee.
In noexcept(noexcept(x)), which is the specifier? The outer one (it sits in the specifier slot after the signature); the inner is the operator.
Is noexcept(1 + 2) true or false? true — built-in arithmetic has no throwing operation.
Are destructors that omit noexcept may-throw? No — since C++11 they are implicitly noexcept; throwing from one calls terminate.