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

throw, try и catch

Функция divide не может разделить на ноль, но тип результата обязывает её вернуть какое-то значение типа int. Пусть для нулевого делителя она возвращает -1:

C++
#include <iostream>

int divide(int a, int b) {
    if (b == 0) {
        return -1;
    }
    return a / b;
}

int main() {
    std::cout << divide(-7, 7) << ' ' << divide(1, 0) << '\n';  // -1 -1
}

Вызывающий код не отличит ошибку от настоящего частного: -7 / 7 тоже равно -1. Какое число ни выбрать, оно может оказаться честным результатом деления.

Бросить исключение

Вместо особого значения функция может бросить исключение (exception) — объект, который сообщает об ошибке в обход return. Его создаёт выражение throw. Бросить можно и int, и строку, но обычно для ошибки объявляют отдельный класс:

C++
struct DivideByZero {};

int divide(int a, int b) {
    if (b == 0) {
        throw DivideByZero{};
    }
    return a / b;
}

После throw функция не возвращает значения, и строка return a / b не выполняется. Исключение покидает divide, затем функцию, которая её вызвала, и так далее — пока не встретит код, готовый его принять.

Поймать исключение

Код, в котором может возникнуть исключение, помещают в блок try. За ним идут обработчики (handlers) — блоки catch с типом исключения в скобках:

C++
int main() {
    try {
        std::cout << divide(7, 2) << '\n';  // 3
        std::cout << divide(1, 0) << '\n';
        std::cout << "done\n";
    } catch (const DivideByZero&) {
        std::cout << "division by zero\n";  // division by zero
    }
}

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

Пока исключение идёт к обработчику, функции между throw и try завершаются, а их локальные объекты уничтожаются в обратном порядке. Это раскрутка стека (stack unwinding): деструкторы (destructors) локальных объектов выполняются и тогда, когда функцию покидает исключение.

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

Обработчики проверяются сверху вниз, и выполняется первый подходящий: лучшего совпадения, как при перегрузке (overloading), никто не ищет. Обработчик подходит, если в скобках стоит тип исключения или его открытый базовый класс (base class) — по значению или по ссылке (reference). Поэтому обработчик базового класса ловит и исключения производных классов (derived classes).

catch (...) ловит исключение любого типа и стоит последним, иначе программа не скомпилируется. Внутри обработчика throw; без операнда бросает то же исключение дальше, к внешнему try. Так обработчик может напечатать сообщение и передать ошибку выше. Если подходящего обработчика нет нигде, вызывается 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 перехватывает первый. Сначала пишите обработчики производных классов, затем — базовых.