RAII, гарантии и noexcept
Исключение — ещё один путь выхода из функции, и на нём тоже нужно освободить ресурсы (resources). Раскрутка стека вызывает деструкторы локальных объектов, но у обычного указателя нет деструктора, который удалил бы объект:
#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
}
}Деструктор выполнился один раз, хотя объектов Noisy два. При раскрутке стека owned уничтожается и удаляет свой объект, а до delete raw выполнение не доходит. Первый Noisy остаётся в куче (heap) — это утечка.
Поэтому ресурсы отдают RAII-владельцам вроде std::unique_ptr, а не окружают каждый delete блоком try. Деструктор владельца выполняется на любом пути выхода из функции, в том числе при исключении, если его где-то поймают.
Три уровня гарантий
То, что функция обещает на случай исключения, описывают три уровня гарантий:
- базовая гарантия (basic guarantee): ничего не утекает, а все объекты остаются пригодными к работе, хотя их значения могли измениться;
- строгая гарантия (strong guarantee): если вызов завершился исключением, состояние программы такое же, как до вызова;
- гарантия отсутствия исключений (no-throw guarantee): функция не бросает исключений совсем.
RAII-владельцы обеспечивают первую часть базовой гарантии: при исключении утечек нет.
Обещание noexcept
Гарантию отсутствия исключений записывают в объявлении (declaration) функции словом noexcept:
int twice(int x) noexcept {
return x * 2;
}Компилятор не запрещает throw в такой функции: GCC и Clang только предупреждают, если видят, что исключение из неё выйдет. Если исключение всё же покидает noexcept-функцию, вызывается std::terminate, и программа завершается: обработчик снаружи его не поймает. Исключение, которое брошено и поймано внутри самой функции, обещания не нарушает.
Деструкторы по умолчанию — noexcept, даже если это слово не написано. Поэтому исключение, покинувшее деструктор, тоже вызывает std::terminate.
Частая ошибка
Деструктор сообщает об ошибке исключением:
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]Программа собирается: это предупреждение Clang, и GCC выдаёт похожее. Но когда уничтожается объект File с saved, равным false, вызывается std::terminate, и try вокруг объекта не поможет. Деструктор не должен бросать: об ошибке сообщает отдельный метод вроде save(), который вызывают до уничтожения объекта.