20.2 – Основы обработки исключений
В предыдущем уроке о необходимости исключений мы говорили о том, как использование кодов возврата приводит к смешиванию порядка выполнения программы и порядка обработки ошибок, ограничивая и то и другое. Исключения в C++ реализуются с использованием трех ключевых слов, работающих вместе: throw
, try
и catch
.
Выбрасывание исключений
В реальной жизни, чтобы отметить, что произошли определенные события, мы постоянно используем сигналы. Например, во время американского футбола, если игрок совершил фол, судья бросает флаг на землю и дает свисток об остановке игры. Затем назначается и исполняется наказание. Как только наказание снято, игра, как правило, возобновляется в обычном режиме.
В C++ для сигнализации о том, что произошло исключение или ошибка (пример с выбрасыванием флага штрафа) используется инструкция throw
. Сигнализация о возникновении исключения также обычно называется генерацией или выбрасыванием исключения.
Чтобы использовать инструкцию throw
, просто используйте ключевое слово throw
, за которым следует значение любого типа данных, который вы хотите использовать, чтобы сигнализировать о возникновении ошибки. Обычно это значение будет кодом ошибки, описанием проблемы или пользовательским классом исключения.
Вот несколько примеров:
// выбрасываем литеральное целочисленное значение
throw -1;
// выбрасываем значение перечисления
throw ENUM_INVALID_INDEX;
// выбрасываем литеральную строку в стиле C (const char*)
throw "Can not take square root of negative number";
// выбрасываем переменную double, определенную ранее
throw dX;
// выбрасываем объект класса MyException
throw MyException("Fatal Error");
Каждая из этих инструкций действует как сигнал о том, что возникла какая-то проблема, которую необходимо обработать.
Поиск исключений
Создание исключений – это только одна часть процесса обработки исключений. Вернемся к нашей аналогии с американским футболом: что происходит после того, как судья выбросил штрафной флажок? Игроки замечают штраф и останавливают игру. Нарушается нормальный ход футбольного матча.
В C++ мы используем ключевое слово try
для определения блока инструкций (называемого блоком try
). Блок try
действует как наблюдатель, ищущий любые исключения, которые вызываются любой из инструкций в блоке try
.
Вот пример блока try
:
try
{
// Инструкции, которые могут вызывать исключения,
// которые вы хотите обработать, помещаются сюда
throw -1; // простая инструкция throw
}
Обратите внимание, что блок try
не определяет, КАК мы будем обрабатывать исключение. Он просто сообщает программе: «Эй, если какая-либо из инструкций внутри этого блока try
вызовет исключение, захвати его!».
Обработка исключений
Наконец, конец нашей аналогии с американским футболом: после объявления штрафа и остановки игры судья определяет наказание и исполняет его. Другими словами, наказание должно быть выполнено до возобновления нормальной игры.
Фактически обработка исключений – это работа блока (ов) catch
. Ключевое слово catch
используется для определения блока кода (называемого блоком catch
), который обрабатывает исключения для одного типа данных.
Вот пример блока catch
, который перехватывает исключения со значениями int
:
catch (int x)
{
// Здесь обрабатываем исключение типа int
std::cerr << "We caught an int exception with value" << x << '\n';
}
Блоки try
и блоки catch
работают вместе – блок try
обнаруживает любые исключения, которые вызываются инструкциями в блоке try
, и направляет их для обработки в соответствующий блок catch
. Блок try
должен иметь сразу после себя, по крайней мере, один блок catch
, но он также может иметь несколько блоков catch
, идущих последовательно.
Как только исключение было перехвачено блоком try
и направлено в блок catch
для обработки, исключение считается обработанным, и выполнение возобновится в обычном режиме после блока catch
.
Параметры catch
работают так же, как параметры функции, причем параметр доступен в последующем блоке catch
. Исключения базовых типов можно перехватывать по значению, но исключения небазовых типов следует перехватывать по константной ссылке, чтобы избежать ненужного копирования.
Как и в случае с функциями, если параметр не будет использоваться в блоке catch
, имя переменной можно не указывать:
// обратите внимание: нет имени переменной,
// так как мы не используем ее в блоке catch ниже
catch (double)
{
// Здесь обрабатываем исключение типа double
std::cerr << "We caught an exception of type double" << '\n';
}
Это может помочь предотвратить предупреждения компилятора о неиспользуемых переменных.
throw
, try
и catch
– собираем всё вместе
Вот полная программа, в которой используются инструкции throw
, блок try
и несколько блоков catch
:
#include <iostream>
#include <string>
int main()
{
try
{
// Инструкции, которые могут вызывать исключения,
// которые вы хотите обработать, помещаются сюда
throw -1; // простой пример
}
catch (int x)
{
// Любые исключения типа int, возникшие в блоке try выше, отправляются сюда
std::cerr << "We caught an int exception with value: " << x << '\n';
}
// без имени переменной, так как мы не используем само исключение в блоке catch ниже
catch (double)
{
// Любые исключения типа double, выданные в блоке try выше, отправляются сюда
std::cerr << "We caught an exception of type double" << '\n';
}
catch (const std::string& str) // отлавливаем классы по константной ссылке
{
// Любые исключения типа std::string, возникшие в блоке try выше, отправляются сюда
std::cerr << "We caught an exception of type std::string" << '\n';
}
std::cout << "Continuing on our merry way\n";
return 0;
}
Выполнение показанного выше блока try
/catch
приведет к следующему результату:
We caught an int exception with value -1
Continuing on our merry way
Инструкция throw
использовалась для вызова исключения со значением -1, которое имеет тип int
. Затем эта инструкция throw
была перехвачена включающим ее блоком try
и направлена в соответствующий блок catch
, который обрабатывает исключения типа int
. Этот блок catch
напечатал соответствующее сообщение об ошибке.
Как только исключение было обработано, программа продолжила работу в обычном режиме после блоков catch
, напечатав "Continuing on our merry way".
Обзор обработки исключений
Обработка исключений на самом деле довольно проста, и следующие два абзаца охватывают большую часть того, что вам нужно помнить о ней.
Когда выбрасывается исключение (с помощью throw
), выполнение программы сразу же переходит к ближайшему, включающему эту инструкцию throw
, блоку try
(при необходимости с распространением вверх по стеку, чтобы найти охватывающий блок try
– мы обсудим это более подробно в следующем уроке). Если какой-либо из обработчиков catch
, прикрепленных к этому блоку try
, обрабатывает этот тип исключения, то этот обработчик выполняется, и исключение считается обработанным.
Если подходящих обработчиков catch
нет, выполнение программы переходит к следующему охватывающему блоку try
. Если до конца программы не удается найти подходящего обработчика catch
, то программа завершится с ошибкой исключения.
Обратите внимание, что при сопоставлении исключений с блоками catch
компилятор не будет выполнять неявные преобразования или расширяющие преобразования (продвижения)! Например, исключение char
не будет соответствовать блоку catch int
. Исключение типа int
не соответствует блоку catch float
. Однако приведение производного класса к одному из его родительских классов будет выполнено.
Вот и всё, что нужно помнить. Остальная часть этой главы будет посвящена демонстрации работы этих принципов.
Исключения обрабатываются немедленно
Вот небольшая программа, демонстрирующая, как исключения обрабатываются немедленно:
#include <iostream>
int main()
{
try
{
throw 4.5; // генерируем исключение типа double
std::cout << "This never prints\n";
}
catch(double x) // обрабатываем исключение типа double
{
std::cerr << "We caught a double of value: " << x << '\n';
}
return 0;
}
Эта программа проста настолько, насколько это возможно. Вот что происходит: инструкция throw
, первая выполняемая инструкция, вызывает исключение типа double
. Выполнение немедленно переходит к ближайшему охватывающему ее блоку try
, который является единственным блоком try
в этой программе. Затем на совпадение проверяются обработчики catch
. Наше исключение относится к типу double
, поэтому мы ищем обработчик catch
типа double
. У нас есть один такой, поэтому он и выполняется.
Следовательно, результат этой программы будет следующим:
We caught a double of value: 4.5
Обратите внимание, что "This never prints" никогда не печатается, потому что исключение привело к немедленному переходу порядка выполнения к обработчику исключения для значений double
.
Более реалистичный пример
Давайте посмотрим на пример, который не совсем академичен:
#include <cmath> // для функции sqrt()
#include <iostream>
int main()
{
std::cout << "Enter a number: ";
double x {};
std::cin >> x;
try // Ищем исключения, которые происходят в блоке try,
{ // и направляем к прикрепленным блокам catch
// Если пользователь ввел отрицательное число, это условие ошибки
if (x < 0.0)
{
// генерируем исключение типа const char*
throw "Can not take sqrt of negative number";
}
// В противном случае выводим ответ
std::cout << "The sqrt of " << x << " is " << std::sqrt(x) << '\n';
}
catch (const char* exception) // ловим исключения типа const char*
{
std::cerr << "Error: " << exception << '\n';
}
}
В этом коде пользователя просят ввести число. Если он вводит положительное число, оператор if
не выполняется, исключение не генерируется и печатается квадратный корень из этого числа. Поскольку в этом случае исключение не возникает, код внутри блока catch
никогда не выполняется. Результат будет примерно таким:
Enter a number: 9
The sqrt of 9 is 3
Если пользователь вводит отрицательное число, мы генерируем исключение типа const char*
. Поскольку мы находимся в блоке try
и соответствующий обработчик исключений найден, управление немедленно передается обработчику исключений const char*
. Результат:
Enter a number: -4
Error: Can not take sqrt of negative number
К настоящему моменту вы должны понять основную идею исключений. В следующем уроке мы приведем еще немало примеров, чтобы показать, насколько гибкими они являются.
Что обычно делают блоки catch
Если исключение направлено в блок catch
, оно считается «обработанным», даже если этот блок catch
пуст. Однако обычно вы захотите, чтобы блоки catch
делали что-то полезное. Блоки catch
выполняют три общих действия, когда перехватывают исключение:
- во-первых, блоки
catch
могут выводить сообщение об ошибке (либо в консоль, либо в лог-файл); - во-вторых, блоки
catch
могут возвращать вызывающей функции значение или код ошибки. - в-третьих, блок
catch
может выбросить другое исключение. Поскольку блокcatch
находится за пределами блокаtry
, вновь созданное исключение в этом случае не обрабатывается предыдущим блокомtry
– оно обрабатывается следующим охватывающим блокомtry
.