К этому моменту вы уже должны были разобраться с тем, как работают исключения в языке С++. На этом уроке мы рассмотрим еще несколько интересных тем, связанных с исключениями.
Непойманные исключения
На предыдущем уроке мы научились делегировать обработку исключений caller-у (или другой функции, которая «находится выше» в стеке вызовов). В следующем примере функция mySqrt() выбрасывает исключение и предполагает, что его кто-то обработает. Но что произойдет, если этого никто не сделает?
Вот наша программа вычисления квадратного корня числа без блока try в функции main():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <cmath> // для sqrt() // Отдельная функция вычисления квадратного корня числа double mySqrt(double a) { // Если пользователь ввел отрицательное число, if (a < 0.0) throw "Can not take sqrt of negative number"; // то выбрасывается исключение типа const char* return sqrt(a); } int main() { std::cout << "Enter a number: "; double a; std::cin >> a; // Здесь нет никакого обработчика исключений! std::cout << "The sqrt of " << a << " is " << mySqrt(a) << '\n'; return 0; } |
Теперь предположим, что пользователь ввел -5
, и mySqrt(-5)
сгенерировало исключение. Функция mySqrt() не обрабатывает свои исключения самостоятельно, поэтому стек начинает раскручиваться, и точка выполнения возвращается обратно в функцию main(). Но, поскольку в main() также нет обработчика исключений, выполнение main() и всей программы прекращается.
Когда main() завершает свое выполнение с необработанным исключением, то операционная система обычно уведомляет нас о том, что произошла ошибка необработанного исключения. Как она это сделает — зависит от каждой операционной системы отдельно:
либо выведет сообщение об ошибке;
либо откроет диалоговое окно с ошибкой;
либо просто сбой.
Это то, что мы не должны допускать, как программисты!
Обработчики всех типов исключений
А теперь загадка: «Функции могут генерировать исключения любого типа данных, и, если исключение не поймано, это приведет к раскручиванию стека и потенциальному завершению выполнения всей программы. Поскольку мы можем вызывать функции, не зная их реализации (и, следовательно, какие исключения они могут генерировать), то как мы можем это предотвратить?».
К счастью, язык C++ предоставляет нам механизм обнаружения/обработки всех типов исключений — обработчик catch-all. Обработчик catch-all работает так же, как и обычный блок catch, за исключением того, что вместо обработки исключений определенного типа данных, он использует эллипсис (...
) в качестве типа данных.
А как мы уже знаем, эллипсисы могут использоваться для передачи аргументов любого типа данных в функцию. В этом контексте они представляют собой исключения любого типа данных. Вот простой пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int main() { try { throw 7; // выбрасывается исключение типа int } catch (double a) { std::cout << "We caught an exception of type double: " << a << '\n'; } catch (...) // обработчик catch-all { std::cout << "We caught an exception of an undetermined type!\n"; } } |
Поскольку для типа int не существует специального обработчика catch, то обработчик catch-all ловит это исключение. Следовательно, результат:
We caught an exception of an undetermined type!
Обработчик catch-all должен находиться последним в цепочке блоков catch. Это делается для того, чтобы исключения сначала могли быть пойманы обработчиками catch, адаптированными к конкретным типам данных (если они вообще существуют). В Visual Studio это контролируется, насчет других компиляторов — не уверен, есть ли такое ограничение.
Часто блок обработчика catch-all оставляют пустым:
1 |
catch(...) {} // игнорируются любые непредвиденные исключения |
Этот обработчик ловит любые непредвиденные исключения и предотвращает раскручивание стека (и, следовательно, потенциальное завершение выполнения всей программы), но здесь он не выполняет никакой обработки исключений.
Использование обработчика catch-all в функции main()
Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int main() { try { runGame(); } catch(...) { std::cerr << "Abnormal termination\n"; } saveState(); // сохраняем текущее состояние игрока return 1; } |
В этом случае, если функция runGame() или любая другая из функций, которые вызываются в runGame(), выбросит исключение, которое не будет поймано функциями в стеке выше, то, в конечном итоге, оно попадет в обработчик catch-all. Это предотвратит завершение выполнения функции main() и даст нам возможность вывести сообщение с указанием ошибки на наше усмотрение, а затем сохранить состояние пользователя до выхода из программы. Это может быть полезно для обнаружения и устранения непредвиденных проблем.
Спецификации исключений
Эту тему можете рассматривать как дополнительное чтиво, так как спецификации исключений редко используются на практике, плохо поддерживаются компиляторами, а Бьёрн Страуструп (создатель языка C++) считает их неудачным экспериментом.
Спецификации исключений — это механизм объявления функций с указанием того, будет ли функция генерировать исключения (и какие именно) или нет. Это может быть полезно при определении необходимости помещения вызова функции в блок try.
Существуют три типа спецификации исключений, каждый из которых использует так называемый синтаксис throw (…).
Во-первых, мы можем использовать пустой оператор throw для обозначения того, что функция не генерирует никакие исключения, которые выходят за её пределы:
1 |
int doSomething() throw(); // не выбрасываются исключения |
Обратите внимание, функция doSomething() все еще может генерировать исключения, только обрабатывать она должна их самостоятельно. Любая функция, объявленная с использованием throw() (как в вышеприведенном примере), должна немедленно прекратить выполнение программы, если она попытается сгенерировать исключение, которое приведет к раскручиванию стека. Другими словами, мы сообщаем, что все исключения функции doSomething(), функция doSomething() будет обрабатывать самостоятельно.
Во-вторых, мы можем использовать оператор throw с указанием типа исключения, которое может генерировать эта функция:
1 |
int doSomething() throw(double); // могут генерироваться исключения типа double |
Наконец, мы можем использовать эллипсис с оператором throw для обозначения того, что функция может генерировать разные типы исключений:
1 |
int doSomething() throw(...); // могут генерироваться любые исключения |
Из-за плохой (неполной) реализации и совместимости с компиляторами, и учитывая тот факт, что спецификации исключений больше напоминают заявления о намерениях, чем гарантии чего-либо, и то, что они плохо совместимы с шаблонами функций, и то, что большинство программистов С++ не знают о их существовании, приводит к тому, что использовать спецификации исключений не рекомендуется.
И ещё, сейчас спецификации throw(…) на некоторых компиляторах, например MinGW вообще не поддерживаются и вызывают ошибку компиляции.
Сейчас лучше немного по другому делать:
1.Если не хотим раскручивания стека в ЛЮБОМ случае:
или
2.Если хотим выбрасывать исключения только определённого типа данных:
или
3.Если хотим выбрасывать исключения любого типа данных:
(просто ничего не добавляем,стандартный вызов функции)
Копнул по спецификациям (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);
В целом, пожалуй, и правда, лучше не использовать.
Круто. Спасибо, что поделились.
Спасибо за четкое разъяснение материала.
Пожалуйста 🙂