20.4 – Неперехваченные исключения и универсальные обработчики

Добавлено 6 сентября 2021 в 13:17

К этому моменту вы должны иметь представление о том, как работают исключения. В этом уроке мы рассмотрим еще несколько интересных, связанных с ними случаев.

Неперехваченные исключения

В нескольких последних примерах было довольно много случаев, когда функция предполагает, что вызывающая ее функция (или другая функция где-то в стеке вызовов) обработает исключение. В следующем примере 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() и даст нам возможность распечатать выбранную нами ошибку, а затем, перед выходом, сохранить состояние пользователя. Это может быть полезно для выявления и решения проблем, которые могут быть непредвиденными.

Теги

C++ / CppException / ИсключениеLearnCppДля начинающихОбработка ошибокОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.