Есть ли смысл в порядке catch блоков при обработке исключений?
Что проверяют
Понимаете ли вы, как выбирается обработчик и какие преобразования типов при этом допустимы. Следом часто спрашивают, почему ловить нужно по ссылке и чем throw; отличается от throw e;.
Как отвечать
Порядок важен. Обработчики одного try проверяются сверху вниз во время выполнения, и срабатывает первый подходящий, а не самый точный. Компилятор лишь предупреждает о явно недостижимых блоках: GCC и Clang пишут, что исключение будет поймано более ранним обработчиком.
Правила сопоставления строже, чем при вызове функции:
- допустимо: совпадение типа с точностью до
const/volatileи ссылки, приведение к публичному однозначному базовому классу, стандартные преобразования указателей; - недопустимо: арифметические и пользовательские преобразования.
throw 42;не поймается вcatch (long); catch (...): ловит всё и обязан стоять последним, иначе код не скомпилируется.
Отсюда порядок от частного к общему. Для стандартной иерархии: std::invalid_argument, затем std::logic_error, затем std::exception, в конце catch (...). Ловить стоит по const&: по значению исключение копируется и срезается до типа обработчика, и поля производного класса вместе с его what() теряются.
Для проброса дальше используйте throw; без аргумента: он перебрасывает исходный объект с исходным динамическим типом. throw e; создаёт копию статического типа e. Передать исключение между потоками помогают std::current_exception() и std::rethrow_exception().
Где спотыкаются
Ставят catch (const std::exception&) первым «на всякий случай», и все специальные обработчики ниже становятся мёртвым кодом.
Ловят std::exception по значению и получают от what() безличное «std::exception» вместо сообщения, а throw e; в таком обработчике отправляет наверх уже срезанный объект.
Забывают про неоднозначную базу: если класс наследует std::exception дважды без виртуального наследования, catch (const std::exception&) его не поймает вовсе.