RAII, guarantees and noexcept
An exception is one more way out of a function, and resources must be released on that path too. Stack unwinding runs the destructors of local objects, but a plain pointer has no destructor that would delete its object:
#include <iostream>
#include <memory>
#include <stdexcept>
struct Noisy { ~Noisy() { std::cout << "~Noisy "; } };
int main() {
try {
Noisy* raw = new Noisy; // ✗
auto owned = std::make_unique<Noisy>();
throw std::runtime_error("fail");
delete raw;
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // ~Noisy fail
}
}The destructor ran once, although there are two Noisy objects. During stack unwinding owned is destroyed and deletes its object, but execution never reaches delete raw. The first Noisy stays on the heap—a leak.
That is why resources are handed to RAII owners such as std::unique_ptr instead of wrapping each delete in a try block. An owner's destructor runs on every path out of a function, an exception included, as long as some handler catches it.
Three levels of guarantees
What a function promises in case of an exception is described by three levels of guarantees:
- basic guarantee: nothing leaks, and all objects stay usable, although their values may have changed;
- strong guarantee: if the call ends with an exception, the program's state is the same as before the call;
- no-throw guarantee: the function never throws an exception at all.
RAII owners provide the first part of the basic guarantee: an exception causes no leaks.
The noexcept promise
The no-throw guarantee is written in a function's declaration with the word noexcept:
int twice(int x) noexcept {
return x * 2;
}The compiler does not forbid throw in such a function: GCC and Clang only warn when they see an exception leaving it. If an exception does leave a noexcept function, std::terminate is called and the program ends: no handler outside catches it. An exception thrown and caught inside the function itself does not break the promise.
Destructors are noexcept by default, even when the word is not written. So an exception that leaves a destructor calls std::terminate too.
Common mistake
A destructor reports an error with an exception:
struct File {
bool saved = false;
~File() {
if (!saved) {
throw std::runtime_error("not saved"); // ✗
}
}
};warning: '~File' has a non-throwing exception specification but can still throw [-Wexceptions]The program builds: this is Clang's warning, and GCC gives a similar one. But when a File whose saved is false is destroyed, std::terminate is called, and a try around the object does not help. A destructor must not throw: a separate method such as save(), called before the object is destroyed, reports the error.