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

throw, try и catch

Чему вы научитесь

Чему вы научитесь:

  • бросать исключение через throw и ловить его в try и catch;
  • объяснять, какие функции завершаются и какие деструкторы выполняются по пути от throw к catch;
  • расставлять обработчики в правильном порядке и ловить исключения по ссылке;
  • объяснять, почему деструктор не должен бросать исключения.
Дом в разрезе: мяч throw падает с верхнего этажа мимо погасших комнат в сетку catch на нижнем этаже main

Представьте здание, в котором каждый вызов функции — отдельный этаж. На нижнем этаже работает main. Когда функция вызывает другую, над ней появляется новый этаж, и работа идёт уже там. Функция вернула результат — её этаж исчезает, и работа продолжается этажом ниже.

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

Сначала посмотрим, чем плохо выдуманное число. Функция 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. Какое число ни выбрать, оно может оказаться честным результатом деления.

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

Вместо особого значения функция может бросить исключение — объект, который сообщает об ошибке в обход 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: так на этаже натягивают сетку. За блоком идут обработчики — блоки 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
    }
}

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

  1. divide(7, 2) возвращает 3, и программа его печатает.
  2. divide(1, 0) бросает исключение, поэтому остаток блока try пропускается, и done не выводится.
  3. Выполняется обработчик DivideByZero и печатает division by zero.
  4. После обработчика выполняется код за последним catch.

В задачах курса исключения выключены: код с throw или try там не скомпилируется. Поэтому в этой теме только теория и тесты.

Раскрутка стека

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

Проверим это на объекте, деструктор которого печатает номер своего этажа:

C++
#include <iostream>

struct Lamp {
    int floor;
    ~Lamp() { std::cout << "off " << floor << '\n'; }
};
void upper() { Lamp lamp{2}; throw 42; }
void lower() { Lamp lamp{1}; upper(); std::cout << "lower end\n"; }
int main() {
    try {
        lower();
    } catch (int code) {
        std::cout << "caught " << code << '\n';  // caught 42
    }
}

Программа печатает три строки. Разберём их по порядку:

  1. main вызывает lower, та создаёт лампу этажа 1 и вызывает upper, а upper создаёт лампу этажа 2 и бросает 42.
  2. Первым закрывается верхний этаж: деструктор лампы в upper печатает off 2.
  3. Затем закрывается lower: печатается off 1, а строка lower end так и не выполняется.
  4. Исключение доходит до try в main, и обработчик печатает caught 42.

Пройдите ту же программу по шагам: следите, как снимаются рамки вызовов и в каком порядке печатают деструкторы.

ВживуюРаскрутка стека: от throw к catchшаг 1 из 10 · рамок: 1

main входит в блок try и вызывает lower

Следите, какие рамки снимаются и чьи деструкторы печатают

Здесь аналогия с этажами неточна: закрытый этаж не ждёт, пока мяч поймают, и не откроется снова. Функция, через которую прошло исключение, уже не продолжит работу.

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

Главное

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

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

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

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