Перейти к содержимому
Теория · Исключения · урок 5 из 7

RAII, гарантии и noexcept

Исключение — ещё один путь выхода из функции, и на нём тоже нужно освободить ресурсы (resources). Раскрутка стека вызывает деструкторы локальных объектов, но у обычного указателя нет деструктора, который удалил бы объект:

C++
#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:

C++
int twice(int x) noexcept {
    return x * 2;
}

Компилятор не запрещает throw в такой функции: GCC и Clang только предупреждают, если видят, что исключение из неё выйдет. Если исключение всё же покидает noexcept-функцию, вызывается std::terminate, и программа завершается: обработчик снаружи его не поймает. Исключение, которое брошено и поймано внутри самой функции, обещания не нарушает.

Деструкторы по умолчанию — noexcept, даже если это слово не написано. Поэтому исключение, покинувшее деструктор, тоже вызывает std::terminate.

Частая ошибка

Деструктор сообщает об ошибке исключением:

C++
struct File {
    bool saved = false;
    ~File() {
        if (!saved) {
            throw std::runtime_error("not saved");  // ✗
        }
    }
};
TEXT
warning: '~File' has a non-throwing exception specification but can still throw [-Wexceptions]

Программа собирается: это предупреждение Clang, и GCC выдаёт похожее. Но когда уничтожается объект File с saved, равным false, вызывается std::terminate, и try вокруг объекта не поможет. Деструктор не должен бросать: об ошибке сообщает отдельный метод вроде save(), который вызывают до уничтожения объекта.