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

Представьте здание, в котором каждый вызов функции — отдельный этаж. На нижнем этаже работает main. Когда функция вызывает другую, над ней появляется новый этаж, и работа идёт уже там. Функция вернула результат — её этаж исчезает, и работа продолжается этажом ниже.
Что делать, если на верхнем этаже случилась беда, с которой там не справиться? Можно отправить вниз выдуманное число, но там его примут за настоящее. Честнее бросить вниз мяч тревоги. Он летит мимо этажей, которые не знают, что с ним делать: там гасят свет и уходят. Поймает мяч сетка на том этаже, где заранее приготовились к такой беде.
Сначала посмотрим, чем плохо выдуманное число. Функция divide не может разделить на ноль, но тип результата обязывает её вернуть какое-то значение типа int. Пусть для нулевого делителя она возвращает -1:
#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, и строку, но обычно для ошибки объявляют отдельный класс:
struct DivideByZero {};
int divide(int a, int b) {
if (b == 0) {
throw DivideByZero{};
}
return a / b;
}После throw функция не возвращает значения, и строка return a / b не выполняется. Исключение покидает divide, затем функцию, которая её вызвала, и так далее — пока не встретит код, готовый его принять.
Поймать исключение
Код, в котором может возникнуть исключение, помещают в блок try: так на этаже натягивают сетку. За блоком идут обработчики — блоки catch с типом исключения в скобках:
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
}
}Давайте разберём, что здесь происходит:
divide(7, 2)возвращает3, и программа его печатает.divide(1, 0)бросает исключение, поэтому остаток блокаtryпропускается, иdoneне выводится.- Выполняется обработчик
DivideByZeroи печатаетdivision by zero. - После обработчика выполняется код за последним
catch.
В задачах курса исключения выключены: код с throw или try там не скомпилируется. Поэтому в этой теме только теория и тесты.
Раскрутка стека
Вернёмся к зданию. Пока мяч летит вниз, каждый этаж между throw и try закрывается: функция завершается, а её локальные объекты уничтожаются в обратном порядке. Это раскрутка стека: деструкторы локальных объектов выполняются и тогда, когда функцию покидает исключение.
Проверим это на объекте, деструктор которого печатает номер своего этажа:
#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
}
}Программа печатает три строки. Разберём их по порядку:
mainвызываетlower, та создаёт лампу этажа1и вызываетupper, аupperсоздаёт лампу этажа2и бросает42.- Первым закрывается верхний этаж: деструктор лампы в
upperпечатаетoff 2. - Затем закрывается
lower: печатаетсяoff 1, а строкаlower endтак и не выполняется. - Исключение доходит до
tryвmain, и обработчик печатаетcaught 42.
Пройдите ту же программу по шагам: следите, как снимаются рамки вызовов и в каком порядке печатают деструкторы.
1#include <iostream>2struct Lamp {3 int floor;4 ~Lamp() {5 std::cout << "off " << floor << "\n";6 }7};8 9void upper() {10 Lamp lamp{2};11 throw 42;12}13void lower() {14 Lamp lamp{1};15 upper();16 std::cout << "lower end\n";17}18 19int main() {20 try {21 lower();22 } catch (int code) {23 std::cout << "caught " << code << "\n";24 }25}
main входит в блок try и вызывает lower
Здесь аналогия с этажами неточна: закрытый этаж не ждёт, пока мяч поймают, и не откроется снова. Функция, через которую прошло исключение, уже не продолжит работу.
Этот вопрос часто задают на собеседовании: что происходит с локальными объектами, когда летит исключение? Ответ — их деструкторы выполняются, от последнего созданного к первому, пока исключение не поймают.
Главное
throwбросает исключение — объект, который сообщает об ошибке в обходreturn.- Код, который может бросить исключение, помещают в
try, а ловят исключение обработчикиcatch. - После
throwостаток функции и остаток блокаtryне выполняются. - При раскрутке стека функции между
throwиcatchзавершаются, и деструкторы их локальных объектов выполняются в обратном порядке.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.