Exercises — noexcept specifier
Throughout, remember the single contract we derive everything from: no exception may escape a noexcept function — break it and the runtime calls std::terminate.
Level 1 — Recognition
Goal: read a declaration and state what it promises. No reasoning chains yet.
Recall Solution L1.1
(i) true — noexcept alone means noexcept(true).
(ii) true — explicit noexcept(true).
(iii) false — noexcept(false) explicitly says "may throw".
(iv) false — the default for an ordinary function is may-throw, i.e. noexcept(false).
Picture: noexcept is a stamp on the function. Only (i) and (ii) carry the "sealed, no leaks" stamp.
Recall Solution L1.2
(i) Specifier — it follows the signature void f(); it's a promise.
(ii) Operator — noexcept(f()) sits in an expression and produces a bool.
(iii) Both. The outer noexcept( ... ) is the specifier (follows void g()); the inner noexcept(f()) is the operator computing the bool that fills it in.
Rule of thumb: after a function head → specifier; inside an expression → operator.
Level 2 — Application
Goal: evaluate the noexcept operator on real expressions.
Recall Solution L2.1
The operator asks: "is every operation in this expression known non-throwing?" Remember expr is unevaluated — nothing runs.
- (i) true —
safeis stampednoexcept. - (ii) false —
riskycarries no promise, so the operator must assume it might throw. - (iii) true — both calls are
noexcept, andint + intcannot throw. - (iv) false — one throwing operation (
risky()) poisons the whole expression. It's an AND over all sub-operations.
Recall Solution L2.2
No, it never prints. liar promised noexcept, then let an exception escape. The moment the exception tries to leave liar, the runtime calls std::terminate — before control ever returns to the try/catch in main.
The catch is downstream of a broken promise, so it is unreachable for this throw. The program aborts.
Contrast with Exception handling in C++ where an un-noexcept function would let the exception unwind into that catch.
Level 3 — Analysis
Goal: trace why the contract is broken (or not) and predict the consequence.
Recall Solution L3.1
The only question that matters: does an exception cross the function boundary?
- (i) terminate — the throw escapes directly.
- (ii) fine — thrown and caught inside; nothing escapes. The
noexceptpromise is about escaping, not about the mere presence ofthrow. - (iii) terminate — the
catch(int)re-throws with barethrow;, and that re-thrown exception now escapes. - (iv) fine —
if (false)is never taken; no exception is ever produced, so nothing escapes. (The promise is about runtime behaviour; athrowthat never runs never breaks it.)
Recall Solution L3.2
Since C++11, Destructors are implicitly noexcept (i.e. ~S() noexcept even though you didn't write it). So this destructor has secretly signed the contract — and then throws.
Runtime result: when s is destroyed at the end of use(), the throw escapes an implicitly-noexcept function → std::terminate.
Why the language forces this: if a destructor threw while the stack was already unwinding from another exception, you'd have two exceptions in flight — undefined chaos. Banning throwing destructors (via implicit noexcept) closes that hole.
Level 4 — Synthesis
Goal: combine noexcept with move semantics and containers to explain performance.
Recall Solution L4.1
The figure below shows the two rows; here is what it depicts in words so you can follow without the image.

Top row (Fast, teal): four teal boxes 0 1 2 3 in an "old buffer" on the left; an orange arrow labelled move points to four teal boxes in a "new buffer" on the right. Because Fast's move constructor is noexcept, moving can never throw, so the vector safely moves each element (cheap — it just steals ownership; see Move semantics).
Bottom row (Slow, plum): four filled plum boxes in an "old buffer (kept intact)" on the left; a plum arrow labelled copy points to four empty outlined boxes in a "new buffer (copies)" on the right — the outlines signal the originals are duplicated, not gutted. A caption reads: if a Slow copy throws, originals untouched → strong guarantee held.
On reallocation std::vector must transfer its N existing elements from the old buffer to the new one, and it wants the Strong exception guarantee: if anything throws, the vector is left exactly as it was before.
Fast: its move constructor isnoexcept, so moving can never throw mid-transfer. The vector moves all elements.Slow: its move might throw, so the vector copies instead. Copying leaves every original untouched.
The critical edge case — exactly how "copy" rolls back. Suppose the vector is copying N elements and the copy of element k throws:
- The
k−1copies already written into the new buffer must be cleaned up. The vector destroys those partially-constructed new elements (running their Destructors — which arenoexcept, so cleanup itself cannot throw). - It then frees the new buffer's raw storage.
- The old buffer is still fully intact (copy reads from it, never modifies it), so the vector simply keeps using it and re-throws the exception to the caller. Result: the vector looks exactly as it did before the reallocation attempt — the strong guarantee.
Why move can't do this rollback. If instead the vector had moved and the move of element k threw, the first k−1 originals are already gutted (their contents stolen). Moving them back could throw again, and there's no untouched source left. There is no safe rollback — which is exactly why the vector only dares to move when the move is noexcept. This is what std::move_if_noexcept decides: move if the move is noexcept, otherwise copy.
Recall Solution L4.2
template<class T>
void mySwap(T& a, T& b)
noexcept( noexcept(T(std::move(a))) && // move-construct can't throw
noexcept(a = std::move(b)) ) // move-assign can't throw
{
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}Reading it: the outer noexcept(...) is the specifier (the promise); the two inner noexcept(...) are operators producing bools. The && demands both the move-construction and the move-assignment be non-throwing. If either could throw, the whole promise collapses to false — honest, no landmine. The body uses exactly those two operations, so the promise matches reality precisely.
Level 5 — Mastery
Goal: design and reason at the edges — propagation, type identity, overload resolution, and subtle escapes.
Recall Solution L5.1
- (i)
noexcept(base())istrue(base is stampednoexcept), somidisnoexcept(true). - (ii)
noexcept(mid())is nowtrue(mid resolved tonoexcept(true)), sotopisnoexcept(true). - (iii)
noexcept(top())is therefore true.
The promise propagates: because the leaf base is non-throwing, the whole chain inherits true. Flip base to may-throw and every conditional above it collapses to false — the property flows upward through the operator.
Recall Solution L5.2
The guiding rule: noexcept narrows the promise, so conversions are allowed only in the "safe" direction — a stronger promise may stand in for a weaker one, never the reverse.
- (a) Legal.
nothrowpromises more (never throws) than a plain function pointer requires (may throw). Handing a no-throw function where a may-throw one is expected is safe — you're over-delivering. Avoid(*)() noexceptimplicitly converts tovoid(*)(). - (b) Illegal — compile error.
p2promises its target isnoexcept, butplainmakes no such promise. Storing&plainthere would be a silent lie the type system refuses. There is no conversion fromvoid(*)()tovoid(*)() noexcept. - (c) Selects overload B (
void(*)() noexcept).¬hrowhas typevoid(*)() noexcept, which matches B exactly (no conversion), while matching A only after the noexcept→may-throw conversion. An exact match beats a conversion, so overload resolution picks B.
The big picture: because noexcept is part of the type, void() and void() noexcept are distinct types. This is why generic code (like std::function, callback tables, or template dispatch) can branch on the noexcept-ness of a callable — the information survives in the type, not just as a comment.
Recall Solution L5.3
(a) Yes, independent. They answer different questions:
- constexpr answers "can this run at compile time?"
noexceptanswers "can this throw at runtime?"
You may have either, both, or neither. Here square has both.
(b)
constexprletssquare(5)be folded to25during compilation (usable in array sizes, template args, etc.).noexceptlets callers querynoexcept(square(5)) == trueand lets the compiler skip exception-cleanup machinery for it.
They are orthogonal tools that happen to sit in the same signature.
Recall Solution L5.4
The destructor is implicitly noexcept, so if logAppend throws, the exception escapes → std::terminate. Fix: never let it escape — swallow it inside the destructor:
struct Guard {
~Guard() noexcept { // keep the promise honestly
try { logAppend("closed"); }
catch (...) { /* log failure ignored — cannot crash us */ }
}
};Why this is correct: the throw is now contained (caught inside), so nothing escapes and the noexcept promise holds. This is the canonical pattern: destructors and other "must-not-throw" operations that internally do risky work must catch everything themselves. Recall L3.1(ii): a caught-inside throw never violates the contract.
Recall Self-test checklist
Rate yourself L1→L5.
Can I read noexcept(false) and know it means may-throw? ::: L1 done.
Can I evaluate noexcept(safe() + risky()) and know the operand is unevaluated? ::: L2 done.
Can I decide whether a body triggers terminate by asking "does it escape?" ::: L3 done.
Can I explain the vector move-vs-copy branch via the strong guarantee? ::: L4 done.
Can I explain why a noexcept function pointer converts to plain but not vice versa? ::: L5 done.