Перейти к основному содержимому

Обработка исключений в C++

Разработчику Архитектору

:::tip О чём эта статья Исключение — способ сообщить «здесь операция не удалась» и передать обработку наверх по стеку вызовов, минуя промежуточные return. В C++ это связано с RAII: при раскрутке стека вызываются деструкторы локальных объектов. Иерархия стандартных типов — 191. Раскрутка и архитектура: ошибки и исключения, память. Zero-cost EH: 28. :::

Загрузка демо исключений…

Исключение простыми словами

Функция divide(a, b) не может вернуть число, если b == 0. Варианты:

ПодходКак выглядитПлюсМинус
Код ошибкиif (!ok) return -1;предсказуемо в embeddedлегко забыть проверить
Исключениеthrow ...;ошибка «всплывает» к catchстоимость на пути throw

throw создаёт объект-исключение и ищет ближайший catch, подходящий по типу. Пока ищет — выполнение покидает текущую функцию, вызывая деструкторы локальных переменных.


Базовый синтаксис

#include <stdexcept>
#include <iostream>

double divide(double a, double b) {
if (b == 0.0)
throw std::invalid_argument("division by zero");
return a / b;
}

int main() {
try {
std::cout << divide(10, 0) << '\n';
} catch (const std::invalid_argument& ex) {
std::cerr << "Logic error: " << ex.what() << '\n';
} catch (const std::exception& ex) {
std::cerr << "Other std error: " << ex.what() << '\n';
} catch (...) {
std::cerr << "Unknown exception\n";
}
return 0;
}

Разбор конструкций

КонструкцияРоль
throw std::invalid_argument("...");создать объект исключения и начать поиск обработчика
try { ... }охраняемый блок — отсюда исключение может «вылететь»
catch (const std::invalid_argument& ex)перехватить этот тип и производные
catch (const std::exception& ex)перехватить любой стандартный exception
catch (...)перехватить любой тип (последний обработчик)
ex.what()человекочитаемое сообщение (const char*)

Порядок catch важен

Обработчики смотрят сверху вниз. Сначала — узкий тип, потом — широкий:

catch (const std::invalid_argument& ex) { } // сначала
catch (const std::exception& ex) { } // потом

Если поменять местами, ветка invalid_argument никогда не выполнится: invalid_argument является std::exception, и сработает первый подходящий catch.


Объект исключения

Исключение — значение (часто объект класса). При throw e; для lvalue может происходить копирование; для временных — перемещение (C++11).

Рекомендация: бросать типы, наследующие std::exception, с понятным what():

#include <exception>
#include <string>

class ConfigError : public std::exception {
std::string message_;
public:
explicit ConfigError(std::string msg) : message_(std::move(msg)) {}
const char* what() const noexcept override {
return message_.c_str();
}
};

Разбор ConfigError:

  • std::string message_ — текст хранится в объекте (строка переживёт выход из конструктора).
  • explicit ConfigError(...) — запрет неявного преобразования из string в ConfigError.
  • noexcept override на what() — стандартное требование: what() не бросает.

Избегайте throw "error"; и throw 42; — ловить по типу const char* или int неудобно, легко ошибиться в catch.


Раскрутка стека (stack unwinding)

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

#include <fstream>

void inner() {
std::ifstream file("missing.txt");
throw std::runtime_error("fail");
}

void outer() {
try {
inner();
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}

Цепочка:

main → outer() → inner() → throw
↑ unwinding: ~file, ~локальные в inner
catch в outer()

std::ifstream file(...)RAII: при unwinding файл закроется в деструкторе, даже если throw случился сразу после открытия. Поэтому в C++ предпочитают RAII вместо try/finally как в Java: 30, 14 — жизненный цикл.

Если исключение нигде не поймано — вызывается std::terminate (аварийное завершение).


Повторный throw

try {
risky();
} catch (const std::exception& ex) {
log(ex.what());
throw; // проброс того же объекта
}

throw; без операнда — только внутри catch. Пробрасывается тот же объект, что поймали.


noexcept и деструкторы

Деструкторы по умолчанию noexcept. Если деструктор бросает исключение во время раскрутки (когда уже летит другое исключение), вызывается std::terminate.

Практическое правило: деструкторы только освобождают ресурсы; ошибки внутри — логировать или глотать, но не throw. Подробнее: 191.


Гарантии безопасности (кратко)

ГарантияСмысл для новичка
Базоваяпри исключении ресурсы освобождаются (деструкторы)
Сильнаяпосле ошибки объект как до операции
Нет выбросаоперация гарантированно не бросает

Контейнеры STL документируют гарантии для push_back, resize и т.д. — см. 14 — STL, 19.


Исключения и коды ошибок

ИсключенияКоды возврата / std::expected (C++23)
ошибку сложнее «проглотить» молчапроверка на каждом шаге явная
удобны при глубокой вложенности вызововпредсказуемы в real-time / embedded
дороже на пути throwstd::expected<T,E> без раскрутки стека

В embedded часто компилируют с -fno-exceptions21.


Типичные ошибки

ОшибкаПоследствие
catch в неверном порядкеузкий тип никогда не ловится
catch (...) не первым, но единственный «широкий»ок; но внутри catch (...) нельзя узнать тип без повторного throw
Деструктор с throwриск terminate при двойном исключении
Бросать сырой указателькто владеет памятью — неясно

Связанные материалы


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).