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

RAII и гарантии при исключениях

Пока исключение летит вниз, каждый пройденный этаж закрывается, и лампы его локальных объектов гаснут сами: деструкторы выполняются при раскрутке стека. Но у объекта из new такого выключателя нет. Эту лампу гасят только вручную, через delete, а до этой строки выполнение может не дойти.

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

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 два:

  1. raw получает адрес первого Noisy из кучи.
  2. owned владеет вторым Noisy через std::unique_ptr.
  3. throw прерывает блок try, и до delete raw выполнение не доходит.
  4. При раскрутке стека owned уничтожается и удаляет свой объект: печатается ~Noisy .
  5. Первый Noisy остаётся в куче — это утечка.

Владелец на каждом пути

Чтобы исключение не оставляло утечек, ресурсы отдают RAII-владельцам вроде std::unique_ptr, а не окружают каждый delete блоком try. Деструктор владельца выполняется на любом пути выхода из функции, в том числе при исключении, если его где-то поймают.

Три уровня гарантий

На случай исключения функция может дать одну из трёх гарантий. Их часто спрашивают на собеседовании, поэтому разберём каждую.

Базовая гарантия

Базовая гарантия: ничего не утекает, а все объекты остаются пригодными к работе, хотя их значения могли измениться. RAII-владельцы дают первую часть базовой гарантии: при исключении утечек нет.

Строгая гарантия

Строгая гарантия: если вызов завершился исключением, состояние программы такое же, как до вызова. Обычно её добиваются так: новое значение собирают в локальной переменной и только в конце отдают его наружу.

C++
void add_pair(std::vector<int>& v, int a, int b) {
    std::vector<int> copy = v;
    copy.push_back(a);
    copy.push_back(b);
    v = std::move(copy);
}

Разберём по строкам:

  1. copy — копия вектора v, и дальше функция меняет только её.
  2. Любой push_back может бросить исключение, если не хватит памяти. Тогда раскрутка стека уничтожит copy, а v останется прежним.
  3. v = std::move(copy) переносит готовый результат в v и исключений не бросает.

Если бы функция добавляла элементы прямо в v, исключение во втором push_back оставило бы в нём только a. Это базовая гарантия, но не строгая.

Гарантия отсутствия исключений

Гарантия отсутствия исключений: функция не бросает исключений совсем. Такое обещание записывают в объявлении функции словом noexcept.

Главное

  • Исключение — ещё один путь выхода из функции, и ресурсы на нём освобождает только деструктор.
  • Объект из new без владельца утекает, если исключение обойдёт delete; RAII-владелец освобождает его сам.
  • Базовая гарантия: утечек нет, объекты пригодны к работе; строгая: при исключении состояние не изменилось.
  • Строгую гарантию получают, собирая результат в копии и отдавая его наружу в конце.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом