Урок №184. Непойманные исключения и обработчики catch-all

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 25858

 ǀ   6 

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

Непойманные исключения

На предыдущем уроке мы научились делегировать обработку исключений caller-у (или другой функции, которая «находится выше» в стеке вызовов). В следующем примере функция mySqrt() выбрасывает исключение и предполагает, что его кто-то обработает. Но что произойдет, если этого никто не сделает?

Вот наша программа вычисления квадратного корня числа без блока try в функции main():

Теперь предположим, что пользователь ввел -5, и mySqrt(-5) сгенерировало исключение. Функция mySqrt() не обрабатывает свои исключения самостоятельно, поэтому стек начинает раскручиваться, и точка выполнения возвращается обратно в функцию main(). Но, поскольку в main() также нет обработчика исключений, выполнение main() и всей программы прекращается.

Когда main() завершает свое выполнение с необработанным исключением, то операционная система обычно уведомляет нас о том, что произошла ошибка необработанного исключения. Как она это сделает — зависит от каждой операционной системы отдельно:

   либо выведет сообщение об ошибке;

   либо откроет диалоговое окно с ошибкой;

   либо просто сбой.

Это то, что мы не должны допускать, как программисты!

Обработчики всех типов исключений


А теперь загадка: «Функции могут генерировать исключения любого типа данных, и, если исключение не поймано, это приведет к раскручиванию стека и потенциальному завершению выполнения всей программы. Поскольку мы можем вызывать функции, не зная их реализации (и, следовательно, какие исключения они могут генерировать), то как мы можем это предотвратить?».

К счастью, язык C++ предоставляет нам механизм обнаружения/обработки всех типов исключений — обработчик catch-all. Обработчик catch-all работает так же, как и обычный блок catch, за исключением того, что вместо обработки исключений определенного типа данных, он использует эллипсис (...) в качестве типа данных.

А как мы уже знаем, эллипсисы могут использоваться для передачи аргументов любого типа данных в функцию. В этом контексте они представляют собой исключения любого типа данных. Вот простой пример:

Поскольку для типа int не существует специального обработчика catch, то обработчик catch-all ловит это исключение. Следовательно, результат:

We caught an exception of an undetermined type!

Обработчик catch-all должен находиться последним в цепочке блоков catch. Это делается для того, чтобы исключения сначала могли быть пойманы обработчиками catch, адаптированными к конкретным типам данных (если они вообще существуют). В Visual Studio это контролируется, насчет других компиляторов — не уверен, есть ли такое ограничение.

Часто блок обработчика catch-all оставляют пустым:

Этот обработчик ловит любые непредвиденные исключения и предотвращает раскручивание стека (и, следовательно, потенциальное завершение выполнения всей программы), но здесь он не выполняет никакой обработки исключений.

Использование обработчика catch-all в функции main()

Рассмотрим следующую программу:

В этом случае, если функция runGame() или любая другая из функций, которые вызываются в runGame(), выбросит исключение, которое не будет поймано функциями в стеке выше, то, в конечном итоге, оно попадет в обработчик catch-all. Это предотвратит завершение выполнения функции main() и даст нам возможность вывести сообщение с указанием ошибки на наше усмотрение, а затем сохранить состояние пользователя до выхода из программы. Это может быть полезно для обнаружения и устранения непредвиденных проблем.

Спецификации исключений


Эту тему можете рассматривать как дополнительное чтиво, так как спецификации исключений редко используются на практике, плохо поддерживаются компиляторами, а Бьёрн Страуструп (создатель языка C++) считает их неудачным экспериментом.

Спецификации исключений — это механизм объявления функций с указанием того, будет ли функция генерировать исключения (и какие именно) или нет. Это может быть полезно при определении необходимости помещения вызова функции в блок try.

Существуют три типа спецификации исключений, каждый из которых использует так называемый синтаксис throw (…).

Во-первых, мы можем использовать пустой оператор throw для обозначения того, что функция не генерирует никакие исключения, которые выходят за её пределы:

Обратите внимание, функция doSomething() все еще может генерировать исключения, только обрабатывать она должна их самостоятельно. Любая функция, объявленная с использованием throw() (как в вышеприведенном примере), должна немедленно прекратить выполнение программы, если она попытается сгенерировать исключение, которое приведет к раскручиванию стека. Другими словами, мы сообщаем, что все исключения функции doSomething(), функция doSomething() будет обрабатывать самостоятельно.

Во-вторых, мы можем использовать оператор throw с указанием типа исключения, которое может генерировать эта функция:

Наконец, мы можем использовать эллипсис с оператором throw для обозначения того, что функция может генерировать разные типы исключений:

Из-за плохой (неполной) реализации и совместимости с компиляторами, и учитывая тот факт, что спецификации исключений больше напоминают заявления о намерениях, чем гарантии чего-либо, и то, что они плохо совместимы с шаблонами функций, и то, что большинство программистов С++ не знают о их существовании, приводит к тому, что использовать спецификации исключений не рекомендуется.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (177 оценок, среднее: 4,93 из 5)
Загрузка...

Комментариев: 6

  1. Илья:

    И ещё, сейчас спецификации throw(…) на некоторых компиляторах, например MinGW вообще не поддерживаются и вызывают ошибку компиляции.

  2. Илья:

    Сейчас лучше немного по другому делать:
    1.Если не хотим раскручивания стека в ЛЮБОМ случае:

    или

    2.Если хотим выбрасывать исключения только определённого типа данных:

    или

    3.Если хотим выбрасывать исключения любого типа данных:

    (просто ничего не добавляем,стандартный вызов функции)

  3. kmish:

    Копнул по спецификациям (https://en.cppreference.com/w/cpp/language/except_spec):

    throw( ) (удалена в C++20)
    throw(typeid, typeid, …) (удалена в C++17)
    throw(…) просто избыточна:
    int doSomething() throw(…);
    равно
    int doSomething();

    Начиная с С++17 рекомендуется использовать вместо throw() noexcept(true):
    int doSomething() noexcept(true);
    В целом, пожалуй, и правда, лучше не использовать.

    1. Анастасия:

      Круто. Спасибо, что поделились.

  4. Дмитрий Тормосин:

    Спасибо за четкое разъяснение материала.

    1. Фото аватара Юрий:

      Пожалуйста 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *