20.2 – Основы обработки исключений

Добавлено 4 сентября 2021 в 10:45

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

  1. во-первых, блоки catch могут выводить сообщение об ошибке (либо в консоль, либо в лог-файл);
  2. во-вторых, блоки catch могут возвращать вызывающей функции значение или код ошибки.
  3. в-третьих, блок catch может выбросить другое исключение. Поскольку блок catch находится за пределами блока try, вновь созданное исключение в этом случае не обрабатывается предыдущим блоком try – оно обрабатывается следующим охватывающим блоком try.

Теги

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

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

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