Урок по треку C++ 5 мин

Исключения и гарантии безопасности

Раскрутка стека, гарантии безопасности, noexcept, исключения в деструкторах и конструкторах, std::expected.

Исключение прерывает обычный поток выполнения и передаёт управление ближайшему подходящему обработчику catch выше по стеку вызовов. По дороге выполняется раскрутка стека: вызываются деструкторы всех полностью созданных локальных объектов в обратном порядке. Поэтому исключения и RAII работают только вместе.

void process() {
    auto file = open_file("data.csv");   // RAII
    std::lock_guard lock(m);
    parse(file);                          // если бросит, файл закроется, мьютекс отпустится
}

Как ловить

  • Бросайте по значению, ловите по константной ссылке: catch (const std::exception& e). Ловля по значению копирует объект и срезает его до базового типа: what() наследника пропадёт.
  • Обработчики проверяются по порядку, поэтому наследники ставятся раньше базовых.
  • throw; внутри catch перебрасывает тот же объект с его настоящим типом; throw e; бросает копию статического типа e.
  • Если обработчик не найден, вызывается std::terminate.
  • Стандартная иерархия: std::exception → logic_error (invalid_argument, out_of_range), runtime_error (overflow_error, system_error), bad_alloc.

Гарантии безопасности исключений

Каждая операция должна обещать одну из гарантий:

  • Без исключений (nothrow): операция никогда не бросает. Деструкторы, swap, перемещения, освобождение памяти.
  • Строгая: если операция бросила, состояние такое, будто её не вызывали (транзакционность). Пример: push_back у vector, если перемещение элемента noexcept.
  • Базовая: после исключения объект корректен, инварианты целы, утечек нет, но значение могло измениться.

Строгую гарантию обычно получают по схеме «сделай всё на копии, потом обменяй noexcept» (copy-and-swap).

noexcept

noexcept обещает, что функция не бросит. Если всё же бросит, вызовется std::terminate без гарантии раскрутки. Зачем помечать:

  • перемещения и swap: контейнеры выбирают перемещение при реаллокации только для noexcept;
  • деструкторы и функции освобождения неявно noexcept.

Оператор noexcept(expr) проверяет при компиляции, может ли выражение бросить.

Деструкторы и конструкторы

  • Если деструктор бросит во время раскрутки стека (уже летит другое исключение), вызывается std::terminate. Деструкторы по умолчанию noexcept, и бросать из них нельзя.
  • Если конструктор бросил, объект считается не созданным, и его деструктор не вызывается. Уже созданные члены и базы при этом уничтожаются. Поэтому сырой указатель, выделенный в конструкторе до исключения, утечёт, а unique_ptr-член нет.

Цена исключений

Основные компиляторы используют табличную реализацию («zero-cost»): пока исключение не брошено, накладных расходов почти нет. Бросок дорогой: поиск обработчика по таблицам и раскрутка занимают микросекунды. Поэтому исключения для исключительных ситуаций, а не для обычного потока управления. В некоторых средах (игровые движки, embedded) их отключают -fno-exceptions.

Ошибки как значения

Для ожидаемых ошибок (не нашли, не распарсили) часто удобнее вернуть результат с ошибкой:

  • std::optional<T>: значение или ничего;
  • std::expected<T, E> (C++23): значение или ошибка с подробностями, есть монадические and_then, transform, or_else;
  • коды ошибок std::error_code.

Исключения хороши, когда ошибку нужно пронести через много уровней и обработать далеко, особенно из конструкторов и операторов, где вернуть код нельзя. Передать исключение между потоками помогает std::exception_ptr (так работает std::future).

Частые ошибки

  • catch (std::exception e) по значению: срезка.
  • Бросать из деструктора.
  • throw e; вместо throw; при перебросе.
  • Сырые ресурсы в конструкторе, бросающем исключение.
  • Исключения для обычного управления потоком.

Что спросят на собесе

  • Что происходит при броске: раскрутка, деструкторы, terminate.
  • Три гарантии безопасности и как получить строгую.
  • Почему деструкторы не бросают и что значит noexcept.
  • Что будет, если конструктор бросил.
  • Исключения против кодов ошибок и std::expected.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.