Исключение прерывает обычный поток выполнения и передаёт управление ближайшему подходящему обработчику 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.