C++ / ERRORS, EXCEPTIONS, AND UNDEFINED BEHAVIOUR
noexcept and when it changes code generation
Mark functions noexcept where the promise is true, predict how it changes call-site cleanup code, and use it to keep vector from copying instead of moving.
What you will learn
- Read noexcept as a runtime promise: break it and std::terminate runs, not a catch.
- Mark move constructors noexcept so vector relocates by moving instead of copying.
- Explain which cleanup path a caller drops when the callee's type says noexcept.
- Use noexcept(noexcept(expr)) to forward a guarantee you cannot know in advance.
Understanding noexcept and when it changes code generation
A noexcept specifier is a claim about what leaves a function, not about what happens inside it. Nothing stops you from calling a throwing function from a noexcept one; the compiler accepts it, and if an exception actually reaches the boundary the runtime calls std::terminate instead of looking for a handler. That termination cannot be caught by anything further up the stack, and whether the locals of the noexcept function were destroyed first is implementation-defined, so a broken promise is not a recoverable error, it is the end of the process.
Since C++17 noexcept is part of the function type, and that is exactly what lets it change generated code. Where a caller holds objects that need destroying, every potentially-throwing call needs a second exit path, a landing pad that runs those destructors and resumes unwinding, plus unwind-table entries mapping the call site to it; a noexcept callee lets the compiler delete that edge, which shrinks code and frees the optimizer from keeping cleanup state alive. The saving depends on the callee's type being visible at the call site, so it evaporates behind a plain void(*)(), a std::function, or a virtual override whose base declaration lacks noexcept. The cost can also move rather than vanish: a noexcept function that calls throwing code needs its own region whose only job is to reach std::terminate.
The effect you can observe without reading assembly is library dispatch: std::vector must give reserve and push_back the strong guarantee, and it cannot do that by moving elements whose move constructor might throw, since a failure halfway through relocation would leave the old buffer full of moved-from objects and no way back. So it asks std::move_if_noexcept, and a missing noexcept on a move constructor quietly turns every reallocation into a deep copy. Put noexcept where failure is genuinely impossible and callers must not have to cope with it, such as moves, swap, destructors and trivial observers, and keep it off code that allocates or validates input, where you would be trading a recoverable exception for termination.
<iostream>
<type_traits>
<vector>
struct Fast {
Fast() = default;
Fast(const Fast&) { std::cout << " copy\n"; }
Fast(Fast&&) noexcept { std::cout << " move\n"; }
};
struct Slow {
Slow() = default;
Slow(const Slow&) { std::cout << " copy\n"; }
Slow(Slow&&) { std::cout << " move\n"; } // no promise, so it may throw
};
template <class T>
void grow_once(const char* name) {
std::cout << name << ": nothrow move = "
<< std::is_nothrow_move_constructible_v<T> << '\n';
std::vector<T> v;
v.emplace_back();
v.reserve(v.capacity() + 1); // one reallocation, one element to relocate
}
int main() {
grow_once<Fast>("Fast");
grow_once<Slow>("Slow");
}
noexcept is an unchecked promise enforced by std::terminate, and its whole value lies in what callers and the standard library are allowed to skip because they can read it in the function's type.
Worked examples
The guarantee lives in the type
Shows that noexcept can be dropped by a pointer conversion but never invented, and that the noexcept operator reads the static type.
<iostream>
void safe() noexcept { std::cout << "safe\n"; }
void risky() { std::cout << "risky\n"; }
int main() {
void (*p)() = safe; // ok: the guarantee may be dropped
void (*q)() noexcept = safe; // ok: the guarantee is kept
// void (*bad)() noexcept = risky; // error: cannot invent the guarantee
std::cout << std::boolalpha << noexcept(p()) << ' ' << noexcept(q()) << '\n';
p();
q();
}
Example explained
Line 1void (*p)() = safe; compiles because losing a guarantee is safe, and this is also where the optimization is lost.
Line 2The commented line is rejected: a throwing function may not be stored in a noexcept function pointer, which is what makes the promise sound.
Line 3noexcept(p()) is false even though p points at safe, because the operator inspects the pointer's type, exactly like the code generator does.
Line 4Both calls print the same text at runtime; only what the compiler knows at the call site differs.
What a broken promise does
An exception escaping a noexcept function reaches std::terminate instead of the surrounding catch.
<cstdlib>
<exception>
<iostream>
<stdexcept>
void thrower() { throw std::runtime_error("boom"); }
void guarded() noexcept { thrower(); } // the throw is not visible here
int main() {
std::set_terminate([] {
std::cout << "terminate handler ran\n" << std::flush;
std::_Exit(0);
});
try {
guarded();
} catch (const std::exception&) {
std::cout << "caught in main\n"; // never printed
}
}
Example explained
Line 1guarded() compiles without complaint: the compiler never proves that thrower() is safe to call here.
Line 2When the exception reaches guarded's boundary the runtime calls std::terminate, so the catch in main is never a candidate handler.
Line 3std::set_terminate installs a handler that prints and calls std::_Exit(0), only so the program ends with a clean status instead of abort.
Line 4Whether locals inside guarded were destroyed before terminate is implementation-defined, so cleanup on this path cannot be relied on.
Forwarding a guarantee you do not own
The noexcept operator computes the noexcept specifier of a template, so a wrapper inherits the guarantee of what it calls.
<iostream>
struct A { void step() noexcept {} };
struct B { void step() {} };
template <class T>
void run(T& t) noexcept(noexcept(t.step())) { t.step(); }
int main() {
A a;
B b;
std::cout << std::boolalpha << noexcept(run(a)) << ' ' << noexcept(run(b)) << '\n';
run(a);
run(b);
}
Example explained
Line 1The outer noexcept(...) is the specifier; the inner noexcept(...) is the operator that answers true or false at compile time.
Line 2Parameter names are in scope in the exception specification, so t may be used there; the operand is unevaluated, so step() is only type-checked.
Line 3run(a) ends up noexcept and its callers can drop cleanup for the call; run(b) does not, so their landing pads stay.
Important notes
Destructors are already noexcept unless a member forces otherwise; declaring ~T() noexcept(false) does not make throwing from a destructor workable, because throwing while unwinding calls terminate regardless.
The specification must match between declaration and definition, so void f() noexcept; with void f() {} is a compile error, and you cannot overload on noexcept: since C++17 it belongs to the function type but not to the signature.
Common mistakes
Marking a function noexcept because today it happens not to throw, then adding a std::string copy or a new inside: std::bad_alloc no longer propagates, it terminates the process, and no catch(...) anywhere can intercept it.
Assuming the compiler verifies the promise. Calling throwing code from a noexcept function compiles cleanly, so the mistake surfaces as a dead process, possibly without local destructors having run.
Writing Widget(Widget&&) = default; and assuming it is noexcept: if any member's move is potentially-throwing the implicit specification is too, and vector<Widget> then deep-copies every element on every reallocation.
Try it yourself
Change, predict, then run
Give Fast and Slow a destructor that prints, then push_back five elements one at a time and count the copies, moves and destructions each type performs. Remove noexcept from Fast's move constructor, re-run, and explain the new counts.
Open the C++ workspaceCheck your understanding
Adding noexcept to a small function whose body allocates memory sometimes makes the generated code for that function larger. Why?
- noexcept disables inlining, so the body can no longer be folded into the caller.
- noexcept makes the compiler insert a runtime check of the exception specification before each return.
- The compiler must emit a path that reaches std::terminate if the allocation throws, and that path now lives inside the noexcept function.
- noexcept wraps every statement of the function body in its own try block.
Show answer
The promise has to be enforced somewhere: callers may drop their cleanup edge for the call, but inside the function each potentially-throwing call needs a region whose landing pad calls std::terminate, so the code can grow rather than shrink. The inlining option is tempting because noexcept is discussed as an optimization hint, but it is not a barrier to inlining at all; if the body is inlined, the terminate region is inlined with it.