Обработка исключений в 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 |
дороже на пути throw | std::expected<T,E> без раскрутки стека |
В embedded часто компилируют с -fno-exceptions — 21.
Типичные ошибки
| Ошибка | Последствие |
|---|---|
catch в неверном порядке | узкий тип никогда не ловится |
catch (...) не первым, но единственный «широкий» | ок; но внутри catch (...) нельзя узнать тип без повторного throw |
Деструктор с throw | риск terminate при двойном исключении |
| Бросать сырой указатель | кто владеет памятью — неясно |
Связанные материалы
- Иерархия std::exception
- RTTI — bad_cast
- ООП в C++ — конструкторы, RAII
- Идиомы — exception safety
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). C++ как язык системного программирования - ключевые принципы, область применения и инженерные требования к коду. Экосистема приложений на C++ - области применения языка от системного ПО до высоконагруженных вычислений. C++ — это мощный язык программирования общего назначения, который обеспечивает прямой доступ к аппаратным ресурсам компьютера при сохранении высокой производительности. Гайд по установке и настройке с написанием первой программы и её запуском. Директива препроцессора include используется для подключения заголовочных файлов в исходный код. Она сообщает компилятору вставить содержимое указанного файла в текущее место перед началом компиляции. Конфигурация — это набор правил и переменных, которые управляют процессом превращения исходного текста в исполняемый продукт. Примеры простых и полезных консольных приложений с демонстрацией концепций языка. Минимальный кроссплатформенный проект C++17 с CMake — структура, сборка и разбор CMakeLists построчно. Модульные тесты с GTest и Catch2 в CMake-проекте — зачем отдельный target, примеры и запуск ctest. Набор мини-проектов для закрепления C++ — консоль, RAII, CMake, Qt, ranges и опционально Vulkan. Набор советов, правил, принципов и обычаев в разработке на этом языке. Типизация, набор правил определения типа данных значений языка.C++ - язык системного программирования
Экосистема приложений на C++
Что требуется знать перед началом изучения языка программирования C++
Первая программа на C++
Начало работы с C++
Конфигурация и сборка в C++
Простые приложения на C++
CMake — первая программа
Google Test и Catch2 в C++
Практические задания по C++
Рекомендации по разработке на C++
Типы данных в C++