Обновить

Комментарии 12

Приветствую, коллега! Я с основной мыслью (не использовать исключения как штатный control flow) в данном конкретном примере скорее согласен. Но позволю кое-где поспорить. __cxa_throw не делает longjmp. В обычной Itanium/DWARF-модели он запускает UnwindRaiseException, SJLJ вообще отдельная модель обработки исключений. Двухфазный поиск и unwind действительно есть, но описывать это как длинный longjump в целом некорректно. .eh_frame тоже не содержит какой-то список созданных объектов, которым надо вызвать деструкторы. Там unwind/CFI-информация в первую очередь. Cleanup выполняется с помощью сгенерированных компилятором landing pads и EH-таблиц. И главный момент - table-based exceptions называются zero-cost не просто так. Если исключение не бросается, нормальный путь обычно практически не платит за try/catch, дорогой тут именно фактический throw, потому рассуждение о производительности без частоты ошибок и бенчмарка не доказывает ничего. Из мелочей - std::optional это не разновидность умного указателя. Значение хранится внутри самого optional, причём предложенная в статье замена в показанном виде даже не скомпилится - вызывается старый VariantToStringVisitor, а затем std::optional<std::string> присваивается непосредственно в std::string.

Ну, то есть, если неподходящий тип конфигурации - это ожидаемый результат парсинга, я бы тоже вернул optional или expected, но из этого не следует, что исключения вообще хуже или что try/catch сам по себе создаёт существенные накладные расходы

пока читал ваши замечания придумал еще один аргумент. Функция с эксепшеном заставляет писать try-catch даже если вам не нужно обрабатывать ошибку - ничего не надо делать в случае ошибки - но try-catch вам все равно полноценный придется писать и в этом случае с пустым catch, а с простым if-ом блок else можно элементарно не писать!

Потом если мне не настолько важна производительность чтобы анализировать какие пути более вероятны, мне тем более не хочется платить ненадежностью кода, который можно сломать просто добавив изменения не туда, потому что в блоке try не видно откуда именно приходит прерывание, оно же ветвление в catch. Фактически в этом случае блок try превращается в блок if с неопределенной границей завершения. А еще надо искать по стеку а какой тип эксепшена писать в параметры catch...

Функция с exception не заставляет писать try/catch в месте вызова. Если здесь ошибку обрабатывать нечем - не ловишь её вообще, она уходит выше по стеку. Пустой catch - баг, он просто проглатывает ошибку. И с optional/expected как раз больше ручной работы, их надо явно протаскивать через каждый промежуточный уровень. Exception же это делает автоматически. Аргумент про "в try непонятно, откуда прилетело" это проблема слишком широкого try, а не механизма исключений

опечатки поправил! вызывается старый VariantToStringVisitor
Спасибо!

Да, способ явного возврата кода ошибки лично мне больше нравится, он более чистый что-ли. Особенно если в языке присутствует соответствующий синтаксический сахар (в C++ с его шаблонами очень уж громоздко, в Go с его отсутствием сахара для этого случая - очень уж занудно, а вот если язык поддерживает опционалы и варианты на уровне синтаксиса то все достаточно красиво и при этом явно).

Но все равно существуют "естественные" эксепшены типа деления на ноль. А раз они существуют, то сразу возникает вопрос - что с ними делать? Если бы обработки исключений вообще не было бы, единственный вариант - падение программы, незакрытые/неосвобожденные ресурсы и прочее. Нужно корректно закрыть ресурсы? Значит нужна какая-то обработка, вызвать деструкторы, раскрутить стек, а это уже механизм обработки исключений. И насколько я понимаю, он присутствует в том или ином виде даже в языках без явных исключений. Ну а раз есть механизм, то следующий шаг в этой логике - почему бы не дать программисту пользоваться им явно?

эксепшены типа деления на ноль. А раз они существуют, то сразу возникает вопрос - что с ними делать?

написать перед делением проверку делителя на ноль? и придумать как вернуть ошибку тоже с использованием обычной логики, не пробовали? Тут конечно зависит от того какую часть кода вам разрешено менять-есть доступ... Нельзя конечно сказать что эксепшены совсем не нужны, но по моему, у них достаточно узкая область применения. Просто так их пихать куда попало точно не стоит.

Угу, есть два подхода:

  1. После каждого вызова любой функции делать кучу проверок что там вернулось, что-то правильное, или случилась ошибка и забивать код всеми этими проверками на каждый чих.

  2. Ожидать, что функция вернет то, что от нее попросили. Если она завершилась, то вернула нормальный результат. А уже на самом верху стека сделать один единственный обработчик всех ошибок, который будет смотреть тип эксепшена и что там вернулось в сообщениях эксепшена.

Во втором случае код сильно чище и проще читается. Ну а если у вас прям настолько часто случаются исключительные ситуации, что прям надо бороться с падением производительности на них, это всего-лишь означает, что вы используете их не по назначению.

Исключительные ситуации они придуманы для исключительных ситуаций. :) Не для бизнес-логики. Пример: мы хотим проверить, что пользователь есть в базе данных, если да, то использовать какое-то его значение, если нет, то добавить пользователя с дефолтным значением и использовать уже его (пожалуйста, не пинайте ногами, что можно и нужно одной транзакцией сделать, пример специально надуманный). Для этого, мы можем использовать функцию "получить пользователя из базы". Но она бросит эксепшен, если пользователя нет. Просто вызвать ее в try/catch и затем в catch уже добавлять нового пользователя - это неправильное использование исключений. Они не должны использоваться бизнес-логикой, они для ошибок, когда бизнес-логика нарушена и что-то делать дальше нет смысла. Для нашего примера как-раз лучше использовать функцию "получить пользователя из базы, если такой есть, или null если нет". А вот если у нас аутентификация, и не найденый пользователь означает, что дальше ничего делать нельзя, то в этом случае первая функция будет работать сильно лучше.

Предложение для некритичной функции возвращать Optional имеет право на жизнь, и - более то6о - даже используется в других языках программирования.

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

Естественно, эти усложнения в примере чуть более сложном, чем hello world, не проявляются.

В Go пошли подобным путём, но вместо вариантного контейнера функция возвращает несколько значений - для результата, для ошибки, для диагностики ошибки и т.д. это работает, хотя выразительность языка снижается.

В Java для веб-приложений наоборот эксепшен зачастую ловится уже на слое веб-контроллера, потому что всё просто откатится само по себе. На производительность неудачного вызова по сравнению с удачным (поход в бд, в пару-тройку других сервисов и т.п.) можно не смотреть - это относительно быстрый отказ.

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

Писал тут же на подобную тему. Считаю, что исключения должны возникать в исключительных ситуация, которые в большинстве случаев требуют особого внимания разработчика, который должен будет это исправить. Бросать исключения где не попадя - это банальная лень. Хуже этого только 100500 try-catch вместо одного глобального, которые в разных местах пытаются среагировать на разные исключения, созданные ранее. Поэтому предпочитаю паттерн Result, который и null считает значением, и в случае ошибки вернёт конкретное сообщение.

только это не просто goto, это gotoмежду функциями, то есть longjump, что гораздо хуже чем простой локальный goto.

longjump, кстати, достаточно, (если не очень) старый. Но как известно все новое это хорошо забытое старое, то есть ваша статья точно не врет!

fn to_mystr<T: ToString>(t: &T) -> String {
    t.to_string()
}

// или
trait VariantToStringVisitor {
    fn to_mystr(&self) -> String;
}

impl<T: ToString> VariantToStringVisitor for T {
    fn to_mystr(&self) -> String {
        self.to_string()
    }
}

fn main() {
    let _s0 = String::from("привет").to_mystr();
    let _s1 = "привет".to_mystr();
    let _s2 = 123.to_mystr();
    let _s3 = 3.14.to_mystr();
}

Главное достоинство исключений в языках типа C++ - в связке с RAII они обеспечивают автоматическое освобождение ресурсов при раскрутке стека: не нужно вручную прописывать очистку на каждом пути ошибки. Но это работает только для ресурсов, инкапсулированных в объекты. Сырые ресурсы и ресурсы в языках с GC - остаются на совести программиста.

Плюс передача управления через несколько уровней. Но это, иногда, становится проблемой, нежели плюсом.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации