Мне что-то часто стал встречаться код в котором вместо обычной логики используются конструкции на основе генерации эксепшенов (exception). Как оценить такой код? Может нам стоит стараться использовать эксепшены как можно чаще?
Я решил проанализировать реальные примеры, вот например
https://github.com/openbmc/dbus-sensors/blob/ce706f061f2b6877e953fa0ad8f5200e99f15854/src/VariantVisitors.hpp#L53 и посмотреть на сколько код с эксепшенами лучше или хуже некоторой альтернативной версии кода. Заодно посмотрим есть ли альтернатива коду с эксепшенами.
Код c эксепшенами
Вот тут у нас одна из функций которая генерирует эксепшен в случае ошибки:
она достаточно короткая и вполне подходит для нашего примера:
struct VariantToStringVisitor{ template <typename T> std::string operator()(const T& t) const { if constexpr (std::is_same_v<T, std::string>) { return t; } else if constexpr (std::is_arithmetic_v<T>) { return std::to_string(t); } throw std::invalid_argument( "Cannot translate type " + boost::typeindex::type_id<T>().pretty_name() + " to string"); }};
но чтобы понять логику, которая здесь как раз основана на генерации эксепшена нам надо включить в анализ код вызова этой функции и обработки этого сгенерированного в этой функции эксепшена:
этот код тоже достаточно простой:
try { labelTypeName = std::visit(VariantToStringVisitor(), findCustomName->second); } catch (const std::invalid_argument&) { lg2::error("Unable to parse '{NAME}'", "NAME", keyName); continue; }
Что мы видим? Фактически мы создали функцию которая возвращает разные типы в зависимости от того произошла ошибка в ходе выполнения функции или нет! То есть функция либо возвращает валидное значение либо она возвращает… Стоп! Но тут, мне кажется, не очень корректно будет говорить что функция что-то возвращает! Механика обработки-генерации эксепшенов построена на том, что эксепшен прерывает работу функции. Прервать работу функции и вернуться куда-то вверх по стеку в подходящий catch блок задача отнюдь не тривиальная, а главное очень не быстро выполнимая.
Конструкция throw это вызов пары специальных функций библиотеки ран-тайма! Если вам интересно вы можете посмотреть здесь очень краткое описание одной из возможных реализаций процесса.
Конструкцияthrow (по крайней мере в Linux/Unix ) на уровне ассемблерного кода разворачивается в последовательный вызов двух функций рантайма: __cxa_allocate_exception и __cxa_throw .
Вторая функция (__cxa_throw) делает «длинный безусловный переход» longjump, но далеко не сразу. Ее выполнение разделено на две очень важные фазы.
Что на самом деле происходит внутри __cxa_throw
Если бы она делала безусловный переход мгновенно, программа бы ломалась, так как деструкторы локальных переменных не успевали бы вызваться. Полная картина выглядит так:
-
Вызов
__cxa_allocate_exception: выделяется память под создаваемыйобъект исключения. -
Вызов
__cxa_throw: в нее передается указатель на объект эксепшена, информация о его типе и указатель на деструктор этого объекта. -
Фаза 1 внутри
__cxa_throw(Поиск / Search Phase):-
Функция запускает процесс Scavenging (поиск по таблицам
eh_frame). -
Онавиртуально «шагает» вверх по кадрам стека, сопоставляя адреса возврата с таблицами компилятора.
-
Цель этой фазы: Просто найти блок
catch, который согласится принять этот тип исключения. На этом этапе деструкторы локальных переменных еще не вызываются, стек физически не уничтожается. Если блок не найден — прямо здесь будет вызванstd::terminate().
-
-
Фаза 2 внутри
__cxa_throw(Раскрутка / Cleanup Phase):-
Как только блок
catchуспешно найден на Фазе 1,__cxa_throwзапускает раскрутку стека. -
Она снова идет по тем же кадрам стека, но теперь для каждого кадра она находит в таблице список созданных объектов и принудительно вызывает их деструкторы.
-
-
Безусловный переход (Прыжок):
-
Только после того, как все деструкторы в уничтожаемых кадрах стека отработали,
__cxa_throwвосстанавливает регистры процессора под состояние целевой функции. -
Она совершает тот самый безусловный переход (jump) на адрес так называемого Landing Pad — физического начала нашего блока
catch.
-
С точки зрения последовательности исполнения нашего кода, throw — это действительно просто вызов функции. Для вызывающего контекста функция __cxa_throw никогда не возвращает управление стандартным путем (через инструкцию RET). Вместо этого она перехватывает поток управления процессора, наводит порядок в памяти, который сломан как раз ее вызовом и ожидаемым нештатным завершением функции и «телепортирует» выполнение программы в catch-блок.
Обратите внимание в нашем коде вызова в блоке
catch (const std::invalid_argument&)
стоит оператор continue; то есть это штатная ветка какого-то анализа. Представляете какую задержку на этой ветке создает генерация эксепшена: создание алокация структуры эксепшена /поиск этого catch-блока / анализ стека на предмет объектов, которым надо вызвать деструкторы !
Но может быть код вызова у нас в случае с эксепшенами получается проще чем при использовании классической логики???
Давайте попробуем написать-реализовать ту же функциональность без использования эксепшенов!
Код БЕЗ эксепшенов
Оказывается в этом случае достаточно поменять возвращаемое значение функции которая раньше генерировала эксепшен на std::optional !
struct VariantToOptionalStringVisitor{ template <typename T> std::optional<std::string> operator()(const T& t) const { using V = std::remove_cvref_t<T>; if constexpr (std::is_same_v<V, std::string>) { return t; } else if constexpr (std::is_arithmetic_v<V>) { return std::to_string(t); } return std::nullopt; }};
Представляете! Вместо генерации эксепшена можно просто написать return std::nullopt; Как же так? Может тогда что-то плохое случится с вызовом нашей функции? Давайте посмотрим как придется изменить вызов ведь мы все таки поменяли возвращаемое значение std::optional<std::string> вместо просто std::string!
Auto tmpStr = std::visit(VariantToStringVisitor(), findCustomName→second);if(tmpStr) { labelTypeName = tmpStr; } else { lg2::error("Unable to parse '{NAME}'", "NAME", keyName); continue; }
тут нам понадобилась временная переменная — разновидность умного указателя, но мне кажется это лучше чем целая структура под Эксепшен при очевидном упрощении логики. Ведь блок try сам по себе создает некоторую неопределенность и риск того что кто-то добавит какой-то код в этот блок и тогда не будет понятно, а где же в этом блоке try происходит собственно ветвление, на какой строчке внутри этого блока с добавленным кодом.
Мне кажется очевидно что этот код без обработки эксепшена получился абсолютно наглядным и поэтому надежным. Если вы хоть немного знаете С++ вы не можете перепутать ветку с обработкой ошибки от ветки с обработкой валидного значения!
В общем для меня большой вопрос зачем в обычный код парсинга, анализа данных какой-то конфигурации, например, нужно обязательно впихнуть генерацию и обработку эксепшенов. Может у кого то есть версии зачем это все таки делают? Это мода что ли какая-то или может это задание преподавателя студентам прокачать работу с эксепшенами?
ссылка на оригинал статьи https://habr.com/ru/articles/1104158/