80 Advanced The Hundred
The Destructor That Ends It All
an exception with nowhere to go
struct Connection {
~Connection() { throw std::runtime_error("flush failed"); }
};
int main() {
try {
Connection c;
std::cout << "sending data" << std::endl;
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << "\n";
}
}
Run it. What does it print?
Answer
sending data — then the catch block is skipped and the whole program aborts (GCC):
Why¶
Since C++11, destructors are implicitly noexcept — and an exception escaping a
noexcept function never reaches the enclosing try; it calls std::terminate on
the spot. The rule exists because destructors run during stack unwinding: if one
throws while another exception is already in flight, there are two live exceptions
and nowhere for either to go. Pre-C++11 that collision already meant terminate;
C++11 just baked the consequence into the destructor's signature. ~Connection()
noexcept(false) opts back out — the catch above would then work — but any throw
during unwinding still terminates, so it merely relocates the landmine.
GCC flags this even without -Wall: -Wterminate warns "'throw' will always call
'terminate'" and notes "in C++11 destructors default to 'noexcept'".
The fix¶
Give cleanup that can fail a separate, throwing close(); the destructor is only a fallback:
void close() { // callers who care about failure call this
closed = true;
if (!flush()) throw std::runtime_error("flush failed");
}
~Connection() {
if (!closed) try { close(); } catch (...) {} // last resort: swallow (or log)
}
Takeaway: destructors are noexcept: report failure before the object dies —
during destruction, nobody can hear you throw.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo