throw, try и catch
Функция 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. Какое число ни выбрать, оно может оказаться честным результатом деления.
Бросить исключение
Вместо особого значения функция может бросить исключение (exception) — объект, который сообщает об ошибке в обход 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. За ним идут обработчики (handlers) — блоки 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
}
}Второй вызов бросает исключение, поэтому остаток блока try пропускается, и done не выводится. Выполняется обработчик, а за ним — код после последнего catch. В задачах курса исключения выключены: код с throw или try там не скомпилируется.
Пока исключение идёт к обработчику, функции между throw и try завершаются, а их локальные объекты уничтожаются в обратном порядке. Это раскрутка стека (stack unwinding): деструкторы (destructors) локальных объектов выполняются и тогда, когда функцию покидает исключение.
Порядок обработчиков
Обработчики проверяются сверху вниз, и выполняется первый подходящий: лучшего совпадения, как при перегрузке (overloading), никто не ищет. Обработчик подходит, если в скобках стоит тип исключения или его открытый базовый класс (base class) — по значению или по ссылке (reference). Поэтому обработчик базового класса ловит и исключения производных классов (derived classes).
catch (...) ловит исключение любого типа и стоит последним, иначе программа не скомпилируется. Внутри обработчика throw; без операнда бросает то же исключение дальше, к внешнему try. Так обработчик может напечатать сообщение и передать ошибку выше. Если подходящего обработчика нет нигде, вызывается 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 перехватывает первый. Сначала пишите обработчики производных классов, затем — базовых.