21 / 163 · C++11 · 8 min
Destructors, exceptions, and noexcept
Destruction is for reliable cleanup, not for business commits that must report failure. Destructors are usually implicitly non-throwing; an exception that crosses a noexcept boundary terminates the program, and even when throwing is allowed, another exception escaping a destructor during stack unwinding also calls terminate.
In this lesson
Cleanup cannot depend on a successful return
Automatic objects are destroyed both on normal scope exit and during exception stack unwinding. After the destructor body runs, members and bases continue to be destroyed in reverse; an early return inside the body does not skip those steps. Resource cleanup can therefore be bound to lifetime instead of being patched onto every business return path by hand.
When a destructor has no explicit exception specification, it is usually noexcept(true), but if a related member or base destructor is allowed to throw, the implicit specification may also allow throws. So "every destructor is naturally noexcept" is inaccurate; in production you should actively ensure that resource-managing destructors do not propagate exceptions.
Two different termination conditions
An exception that escapes a non-throwing function calls std::terminate. Writing noexcept(false) on a destructor only changes that one constraint; it does not solve a separate problem: if an exception is already unwinding the stack and a destructor of a cleaned-up object exits because of another exception, the program still terminates.
A destructor may throw internally and catch the exception as long as it does not escape; the issue is not whether a throw ever occurred, but whether cleanup can finish normally. Catching is also not an excuse to silently drop important errors; decide in advance how logging, error state, or explicit operations will report them.
Move fallible operations out of the destructor
Actions that must confirm success should expose explicit commit(), flush(), or close() so the caller can handle failure while the object still exists. The destructor only does non-throwing fallback cleanup or rollback. The example models a transaction with integer state: a failed commit keeps the original value, and leaving the scope only increments a close count; it does not try to commit again during cleanup.
This is not swallowing every error; it hands errors to a caller that still has recovery context. If an underlying release can truly fail, the interface should define which failures can be reported early and which can only be logged; fatal errors that cannot keep resources safe need an explicit termination policy rather than accidental exception propagation.
Pitfalls
- Explicit noexcept(false) does not make throwing during stack unwinding safe; two exceptions propagating outward still terminate.
- A try/catch in the ordinary destructor body does not surround the member destruction that follows automatically; members must also have a reliable cleanup contract.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <stdexcept>
class Transaction {
int& target_;
int& closed_;
int pending_;
public:
Transaction(int& target, int& closed, int value)
: target_(target), closed_(closed), pending_(value) {}
Transaction(const Transaction&) = delete;
Transaction& operator=(const Transaction&) = delete;
void commit() {
if (pending_ < 0) throw std::invalid_argument("negative value");
target_ = pending_;
}
~Transaction() noexcept { ++closed_; }
};
int main() {
int value = 4;
int closed = 0;
bool caught = false;
try {
Transaction transaction(value, closed, -1);
transaction.commit();
} catch (const std::invalid_argument&) {
caught = true;
}
assert(caught && value == 4 && closed == 1);
{
Transaction transaction(value, closed, 9);
transaction.commit();
}
assert(value == 9 && closed == 2);
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-destructors.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Why can you not move the example's commit() into the destructor and only catch exceptions in the outer main?
Show a reference answer
The current destructor is explicitly noexcept, so if commit's exception escapes, the program calls terminate immediately and the outer catch cannot recover. Even if it is changed to noexcept(false), the object may be destroyed during another exception's stack unwinding, and throwing again still terminates. Keep an explicit commit so the caller handles failure in normal control flow; the destructor only finishes non-throwing cleanup.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.