Порядок обработчиков
В прошлом уроке за блоком try стоял один обработчик. Их может быть несколько, и каждый ждёт исключение своего типа.
Вернёмся к зданию с этажами. На этаже, где поставили try, сетки висят одна под другой, и на каждой табличка с типом исключения. Мяч падает в первую сетку, чья табличка ему подходит, и до нижних уже не долетает. Табличка «ошибка ввода» подходит и мячу «пустой ввод»: пустой ввод — частный случай ошибки ввода.
Первый подходящий
Обработчики проверяются сверху вниз, и выполняется первый подходящий. Лучшее совпадение здесь не ищут, в отличие от перегрузки.
Обработчик подходит, если в скобках стоит тип исключения или его открытый базовый класс — по значению или по ссылке. Поэтому обработчик базового класса ловит и исключения производных классов:
#include <iostream>
struct InputError {};
struct EmptyInput : public InputError {};
int main() {
try {
throw EmptyInput{};
} catch (const EmptyInput&) {
std::cout << "empty input\n"; // empty input
} catch (const InputError&) {
std::cout << "input error\n";
}
}Давайте разберём, что здесь происходит:
EmptyInputобъявлен производным отInputError.- Блок
tryбросаетEmptyInput. - Первым проверяется обработчик
const EmptyInput&: тип совпадает, и программа печатаетempty input. - До обработчика
InputErrorдело не доходит. Он поймал бы другую ошибку ввода, например самInputError.
Отсюда правило: сначала пишите обработчики производных классов, затем — базовых.
Ловить всё: catch (...)
Обработчик catch (...) с тремя точками в скобках ловит исключение любого типа. Это самая широкая сетка, поэтому она висит последней. Если поставить её выше другого обработчика, программа не скомпилируется: Clang сообщает catch-all handler must come last.
Узнать, что именно поймал catch (...), нельзя: у исключения в нём нет имени. Обычно такой обработчик печатает общее сообщение или передаёт исключение дальше.
Передать дальше: throw;
Внутри обработчика выражение throw; без операнда бросает то же исключение дальше, к внешнему try. Так обработчик может напечатать сообщение и передать ошибку выше:
try {
try {
throw EmptyInput{};
} catch (const InputError&) {
std::cout << "log ";
throw;
}
} catch (const EmptyInput&) {
std::cout << "empty input\n"; // log empty input
}Разберём по шагам:
- Внутренний обработчик ловит
EmptyInputчерез ссылку наInputErrorи печатаетlog. throw;отправляет дальше тот же объект исключения, а не его копию.- Внешний обработчик
EmptyInputловит его, потому что настоящий тип объекта сохранился, и печатаетempty input.
Здесь аналогия с сетками неточна: настоящая сетка не подбрасывает мяч, а обработчик может бросить исключение дальше.
Если обработчика нет
Если подходящего обработчика нет нигде, вызывается std::terminate, и программа завершается. Раскрутка стека при этом не гарантирована: деструкторы локальных объектов могут и не выполниться.
Частая ошибка
Обработчик базового класса стоит раньше обработчика производного:
struct InputError {};
struct EmptyInput : public InputError {};
void report() {
try {
throw EmptyInput{};
} catch (const InputError&) {
std::cout << "input error\n"; // input error
} catch (const EmptyInput&) { // ✗
std::cout << "empty input\n";
}
}warning: exception of type 'const EmptyInput &' will be caught by earlier handler [-Wexceptions]Программа собирается, но второй обработчик не выполнится никогда: первый обработчик перехватывает EmptyInput раньше него. Это вопрос с собеседований: «что выведет программа с такими обработчиками?» Поменяйте обработчики местами, и empty input напечатается.
Главное
- Обработчики проверяются сверху вниз, и выполняется первый подходящий, а не лучший.
- Обработчик базового класса ловит и исключения производных, поэтому производные пишут выше.
catch (...)ловит исключение любого типа и стоит последним.throw;внутри обработчика бросает дальше тот же объект исключения; если обработчика нет нигде, вызываетсяstd::terminate.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.