Lesson 3 of 5
noexcept on a function says: this will not throw. It is not a request or a hint. If the
function throws anyway, the program is terminated on the spot.
That severity is the point. Because the promise is absolute, the standard library can make decisions based on it — and the difference is measurable.
The one that matters: vector reallocation
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
// Two types identical except for one keyword.
struct MoveThrows {
std::string data;
static int moves, copies;
explicit MoveThrows(std::string d) : data(std::move(d)) {}
MoveThrows(const MoveThrows& o) : data(o.data) { ++copies; }
MoveThrows(MoveThrows&& o) : data(std::move(o.data)) { ++moves; } // NOT noexcept
MoveThrows& operator=(const MoveThrows&) = default;
MoveThrows& operator=(MoveThrows&&) = default;
};
struct MoveNoexcept {
std::string data;
static int moves, copies;
explicit MoveNoexcept(std::string d) : data(std::move(d)) {}
MoveNoexcept(const MoveNoexcept& o) : data(o.data) { ++copies; }
MoveNoexcept(MoveNoexcept&& o) noexcept : data(std::move(o.data)) { ++moves; }
MoveNoexcept& operator=(const MoveNoexcept&) = default;
MoveNoexcept& operator=(MoveNoexcept&&) = default;
};
int MoveThrows::moves = 0;
int MoveThrows::copies = 0;
int MoveNoexcept::moves = 0;
int MoveNoexcept::copies = 0;
template <typename T>
void fill_and_grow(const char* label, int& moves, int& copies) {
moves = 0;
copies = 0;
std::vector<T> v;
for (int i = 0; i < 8; ++i) {
v.push_back(T("item " + std::to_string(i)));
}
std::cout << " " << label << ": " << moves << " moves, " << copies << " copies\n";
}
int main() {
std::cout << std::boolalpha;
std::cout << "is_nothrow_move_constructible:\n";
std::cout << " MoveThrows: " << std::is_nothrow_move_constructible_v<MoveThrows> << "\n";
std::cout << " MoveNoexcept: " << std::is_nothrow_move_constructible_v<MoveNoexcept> << "\n";
std::cout << "\nreallocating a vector to 8 elements:\n";
fill_and_grow<MoveThrows>("throwing move ", MoveThrows::moves, MoveThrows::copies);
fill_and_grow<MoveNoexcept>("noexcept move", MoveNoexcept::moves, MoveNoexcept::copies);
return 0;
}
is_nothrow_move_constructible:
MoveThrows: false
MoveNoexcept: true
reallocating a vector to 8 elements:
throwing move : 8 moves, 7 copies
noexcept move: 15 moves, 0 copies
Two types identical except for one keyword, and the results differ completely.
The type with a throwing move made 7 copies. The type with a noexcept move made 0.
The reason is lesson 15.2. std::vector::push_back offers the strong guarantee: if it throws,
nothing changed. During a reallocation it must relocate existing elements into the new buffer — and
if a move threw halfway through, the old elements would already be gutted and the new buffer
incomplete. There would be no way back.
So vector asks: is this type's move constructor noexcept?
- Yes → move the elements. Fast, and it cannot fail.
- No → copy them. Slow, but the originals survive, so a throw leaves the vector untouched.
This is std::move_if_noexcept, and it is why a move constructor without noexcept silently costs
you the move semantics you thought you had written (lesson 9.9).
Mark your move constructor and move assignment
noexcept. It is the single highest-value use of the keyword.
The counts are worth reading precisely. The noexcept version made 15 moves: 8 moving each
temporary into the vector, plus 7 relocating during the three reallocations (1 + 2 + 4). The
throwing version made 8 moves — only the temporaries, since those are separate from reallocation —
and 7 copies where the other moved.
What happens when you lie
// CRASHES ON PURPOSE — a noexcept function that throws calls std::terminate.
#include <iostream>
#include <stdexcept>
void lies() noexcept {
throw std::runtime_error("this never reaches a handler");
}
int main() {
std::cout << "before\n";
try {
lies();
} catch (const std::exception& e) {
std::cout << "this handler is never reached: " << e.what() << "\n";
}
std::cout << "after\n";
return 0;
}
GCC sees it coming:
warning: 'throw' will always call 'terminate' [-Wterminate]
And running it confirms:
before
terminate called after throwing an instance of 'std::runtime_error'
what(): this never reaches a handler
Aborted
Exit code 134. The catch block never ran. A noexcept violation does not propagate — it calls
std::terminate immediately, and no handler anywhere in the program gets a chance.
That is why noexcept is a promise rather than a wish: the compiler stops generating unwinding
code for the function, so there is no mechanism left to carry an exception out of it.
The noexcept operator
Confusingly, the same keyword is also a compile-time question:
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>
struct Simple { int a, b; };
struct HasString { std::string s; };
struct Throwing {
Throwing() {}
Throwing(Throwing&&) { } // not noexcept
};
// Conditional noexcept: this function is noexcept only if T's move is.
template <typename T>
void relocate(T& dest, T& src) noexcept(std::is_nothrow_move_assignable_v<T>) {
dest = std::move(src);
}
int main() {
std::cout << std::boolalpha;
// The noexcept OPERATOR asks whether an expression can throw.
int x = 0;
std::cout << "noexcept(x + 1): " << noexcept(x + 1) << "\n";
std::cout << "noexcept(new int): " << noexcept(new int) << "\n";
std::cout << "noexcept(std::string(\"a\")): " << noexcept(std::string("a")) << "\n";
std::cout << "\ncompiler-generated moves:\n";
std::cout << " Simple: " << std::is_nothrow_move_constructible_v<Simple> << "\n";
std::cout << " HasString: " << std::is_nothrow_move_constructible_v<HasString> << "\n";
std::cout << " Throwing: " << std::is_nothrow_move_constructible_v<Throwing> << "\n";
std::cout << "\nconditional noexcept on relocate:\n";
Simple s1{1, 2}, s2{3, 4};
HasString h1, h2;
std::cout << " relocate<Simple>: " << noexcept(relocate(s1, s2)) << "\n";
std::cout << " relocate<HasString>: " << noexcept(relocate(h1, h2)) << "\n";
// Destructors are implicitly noexcept.
std::cout << "\nstd::string destructor is noexcept: "
<< std::is_nothrow_destructible_v<std::string> << "\n";
return 0;
}
noexcept(x + 1): true
noexcept(new int): false
noexcept(std::string("a")): false
compiler-generated moves:
Simple: true
HasString: true
Throwing: false
conditional noexcept on relocate:
relocate<Simple>: true
relocate<HasString>: true
std::string destructor is noexcept: true
Three distinct uses of one keyword:
// void f() noexcept; // the SPECIFIER: a promise
// bool b = noexcept(expr); // the OPERATOR: a question
// void g() noexcept(condition); // CONDITIONAL: promise if condition
noexcept(new int) is false because allocation can throw std::bad_alloc. Almost anything
that allocates is potentially throwing, which is why std::string("a") is too.
The compiler generates noexcept moves when it can. Simple and HasString both get
noexcept move constructors for free, because moving an int or a std::string cannot throw.
Write a move constructor by hand and you lose that unless you say so — which is the Throwing
row, and the single most common way to lose move semantics accidentally.
Conditional noexcept is how generic code stays honest. relocate<T> promises not to throw
exactly when T's move assignment does not. Hardcoding noexcept there would be a lie for some
T; omitting it would be a needless pessimisation for others.
Where to put it
Always:
- Move constructor and move assignment. The measured reason above.
swap. Copy-and-swap (lesson 15.2) depends on swap being the commit point that cannot fail.- Destructors. They are already implicitly
noexcept— see below.
Usually:
- Simple accessors:
size(),empty(),data(),operator[]. - Anything that only touches built-in types.
Do not:
- On a function that allocates, or calls one that might.
- On anything whose implementation you expect to change. Removing
noexceptlater is a breaking change for callers who relied on it — including the standard library. - "Just in case." An unmarked function is not a problem; a wrongly marked one terminates the process.
Destructors are already noexcept
Since C++11, a destructor is implicitly noexcept unless you explicitly say otherwise. The output
above confirms it for std::string.
This is not a convenience — it is load-bearing. Recall lesson 15.1: when an exception propagates, the stack unwinds and destructors run. If a destructor threw during that unwinding, there would be two exceptions in flight at once, and the language has no answer for that. So it terminates.
The practical rule: never let an exception escape a destructor. If a destructor must do something that can fail — flushing a buffer, closing a socket — catch and log it:
// ~Logger() {
// try {
// flush();
// } catch (...) {
// // Nothing else to do. Terminating is worse than a lost log line.
// }
// }
That is one of the few legitimate uses of a silent catch (...).
Try it
Take a resource-owning class from Module 9 or 11 and measure the effect of noexcept on its move
constructor: count allocations and copies while filling a std::vector with 16 of them, with and
without the keyword. Then add a conditional noexcept to a generic wrapper.
Checkpoint
noexceptis a promise, not a hint. Breaking it callsstd::terminateimmediately.- A
noexceptviolation does not propagate — no handler anywhere gets a chance. - Mark move constructors and move assignment
noexcept. Measured: 7 copies became 0. std::vectorcopies instead of moving during reallocation when the move can throw, to preserve the strong guarantee.- Writing a move constructor by hand loses the implicit
noexceptunless you restate it. noexcept(expr)is also an operator asking whether an expression can throw.noexcept(condition)is conditional — the only honest choice in generic code.- Every standard-library move is conditionally
noexceptfor exactly that reason. - Mark
swapnoexcept; copy-and-swap needs a commit point that cannot fail. - Destructors are implicitly
noexcept; a throwing destructor during unwinding terminates. - Never let an exception escape a destructor — catch and log instead.
- Removing
noexceptlater is a breaking change; do not add it speculatively.
Questions about this lesson
Log in to ask a question.