20.4 – Неперехваченные исключения и универсальные обработчики
К этому моменту вы должны иметь представление о том, как работают исключения. В этом уроке мы рассмотрим еще несколько интересных, связанных с ними случаев.
Неперехваченные исключения
В нескольких последних примерах было довольно много случаев, когда функция предполагает, что вызывающая ее функция (или другая функция где-то в стеке вызовов) обработает исключение. В следующем примере mySqrt()
предполагает, что кто-то будет обрабатывать генерируемое ею исключение, но что произойдет, если на самом деле этого никто не сделает?
Вот снова наша программа извлечения квадратного корня, но без блока try
в main()
:
#include <iostream>
#include <cmath> // для функции sqrt()
// Модульная функция извлечения квадратного корня
double mySqrt(double x)
{
// Если пользователь ввел отрицательное число, это состояние ошибки
if (x < 0.0)
{
// генерировать исключение типа const char*
throw "Can not take sqrt of negative number";
}
return sqrt(x);
}
int main()
{
std::cout << "Enter a number: ";
double x;
std::cin >> x;
// Смотрите, никакого обработчика исключений!
std::cout << "The sqrt of " << x << " is " << mySqrt(x) << '\n';
return 0;
}
Теперь предположим, что пользователь вводит -4, а mySqrt(-4)
вызывает исключение. Функция mySqrt()
не обрабатывает это исключение, поэтому стек программы раскручивается и управление возвращается к main()
. Но и здесь нет обработчика исключений, поэтому main()
завершается. На этом этапе мы только что закрыли наше приложение!
Когда main()
завершается с необработанным исключением, операционная система обычно уведомляет вас о том, что произошла ошибка необработанного исключения. Как это происходит, зависит от операционной системы, это может быть печать сообщения об ошибке, всплывающее диалоговое окно с ошибкой или просто сбой. Некоторые ОС менее изящны, чем другие. Как правило, этого следует избегать!
Универсальные обработчики
И теперь у нас проблема: функции потенциально могут генерировать исключения любого типа данных, и если исключение не будет обнаружено, оно будет распространяться по вашей программе вверх и вызывать ее завершение. Поскольку можно вызывать функции, даже не зная, как они реализованы (и, следовательно, какие типы исключений они могут вызывать), как мы можем предотвратить это?
К счастью, C++ предоставляет нам механизм для перехвата всех типов исключений. Он известен как универсальный обработчик. Универсальный обработчик работает так же, как обычный блок catch
, за исключением того, что вместо использования для перехвата определенного типа он в качестве типа для перехвата использует оператор многоточия (...
). По этой причине универсальный обработчик также иногда называют «обработчиком перехвата многоточия».
Если вы помните из урока «11.12 – Многоточия (и почему их следует избегать)», многоточие ранее использовалось для передачи в функцию аргументов любого типа. В этом контексте они представляют исключения любого типа данных. Вот простой пример:
#include <iostream>
int main()
{
try
{
throw 5; // генерируем исключение типа int
}
catch (double x)
{
std::cout << "We caught an exception of type double: " << x << '\n';
}
catch (...) // универсальный обработчик
{
std::cout << "We caught an exception of an undetermined type\n";
}
}
Поскольку для типа int
нет специального обработчика исключений, универсальный обработчик перехватывает это исключение. Этот пример дает следующий результат:
We caught an exception of an undetermined type
Универсальный обработчик должен быть помещен в цепочку блоков catch
последним. Это необходимо для того, чтобы исключения могли быть перехвачены обработчиками, адаптированными к конкретным типам данных, если эти обработчики существуют.
Часто блок универсального обработчика оставляют пустым:
catch(...) {} // игнорируем любые непредвиденные исключения
Это перехватит любые непредвиденные исключения и предотвратит раскручивание ими стека в начало вашей программы, но не будет обрабатывать конкретные ошибки.
Использование универсального обработчика для обертывания main()
Один интересный вариант использования универсального обработчика – обернуть содержимое main()
:
#include <iostream>
int main()
{
try
{
runGame();
}
catch(...)
{
std::cerr << "Abnormal termination\n";
}
saveState(); // сохранить игру пользователя
return 1;
}
В этом случае, если runGame()
или любая из вызываемых ею функций генерирует исключение, которое не перехватывается, это исключение раскручивает стек и в конечном итоге перехватывается универсальным обработчиком. Это предотвратит завершение main()
и даст нам возможность распечатать выбранную нами ошибку, а затем, перед выходом, сохранить состояние пользователя. Это может быть полезно для выявления и решения проблем, которые могут быть непредвиденными.