Перейти к содержимому
Теория · Исключения · урок 2 из 9

Порядок обработчиков

В прошлом уроке за блоком try стоял один обработчик. Их может быть несколько, и каждый ждёт исключение своего типа.

Вернёмся к зданию с этажами. На этаже, где поставили try, сетки висят одна под другой, и на каждой табличка с типом исключения. Мяч падает в первую сетку, чья табличка ему подходит, и до нижних уже не долетает. Табличка «ошибка ввода» подходит и мячу «пустой ввод»: пустой ввод — частный случай ошибки ввода.

Первый подходящий

Обработчики проверяются сверху вниз, и выполняется первый подходящий. Лучшее совпадение здесь не ищут, в отличие от перегрузки.

Обработчик подходит, если в скобках стоит тип исключения или его открытый базовый класс — по значению или по ссылке. Поэтому обработчик базового класса ловит и исключения производных классов:

C++
#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";
    }
}

Давайте разберём, что здесь происходит:

  1. EmptyInput объявлен производным от InputError.
  2. Блок try бросает EmptyInput.
  3. Первым проверяется обработчик const EmptyInput&: тип совпадает, и программа печатает empty input.
  4. До обработчика InputError дело не доходит. Он поймал бы другую ошибку ввода, например сам InputError.

Отсюда правило: сначала пишите обработчики производных классов, затем — базовых.

Ловить всё: catch (...)

Обработчик catch (...) с тремя точками в скобках ловит исключение любого типа. Это самая широкая сетка, поэтому она висит последней. Если поставить её выше другого обработчика, программа не скомпилируется: Clang сообщает catch-all handler must come last.

Узнать, что именно поймал catch (...), нельзя: у исключения в нём нет имени. Обычно такой обработчик печатает общее сообщение или передаёт исключение дальше.

Передать дальше: throw;

Внутри обработчика выражение throw; без операнда бросает то же исключение дальше, к внешнему try. Так обработчик может напечатать сообщение и передать ошибку выше:

C++
try {
    try {
        throw EmptyInput{};
    } catch (const InputError&) {
        std::cout << "log ";
        throw;
    }
} catch (const EmptyInput&) {
    std::cout << "empty input\n";  // log empty input
}

Разберём по шагам:

  1. Внутренний обработчик ловит EmptyInput через ссылку на InputError и печатает log .
  2. throw; отправляет дальше тот же объект исключения, а не его копию.
  3. Внешний обработчик EmptyInput ловит его, потому что настоящий тип объекта сохранился, и печатает empty input.

Здесь аналогия с сетками неточна: настоящая сетка не подбрасывает мяч, а обработчик может бросить исключение дальше.

Если обработчика нет

Если подходящего обработчика нет нигде, вызывается std::terminate, и программа завершается. Раскрутка стека при этом не гарантирована: деструкторы локальных объектов могут и не выполниться.

Частая ошибка

Обработчик базового класса стоит раньше обработчика производного:

C++
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";
    }
}
TEXT
warning: exception of type 'const EmptyInput &' will be caught by earlier handler [-Wexceptions]

Программа собирается, но второй обработчик не выполнится никогда: первый обработчик перехватывает EmptyInput раньше него. Это вопрос с собеседований: «что выведет программа с такими обработчиками?» Поменяйте обработчики местами, и empty input напечатается.

Главное

  • Обработчики проверяются сверху вниз, и выполняется первый подходящий, а не лучший.
  • Обработчик базового класса ловит и исключения производных, поэтому производные пишут выше.
  • catch (...) ловит исключение любого типа и стоит последним.
  • throw; внутри обработчика бросает дальше тот же объект исключения; если обработчика нет нигде, вызывается std::terminate.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом