Как стать автором
Обновить

Почему я предпочитаю исключения, а не значения ошибок

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров23K
Всего голосов 65: ↑57 и ↓8+67
Комментарии214

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

... склонны применять сообщения об ошибках в функциональном стиле и кодировать ошибки в возвращаемый тип
... почему более новые языки наподобие Rust или Go не позволяют применять исключения.

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

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

Возможно, я что-то не знаю, но я не вижу, какие фичи Rust'а "становятся тыквой", какие фичи невозможно реализовывать при наличии исключений.

Тем более, что сам механизм исключений вполне есть, он называется паникой. Паники можно ловить (при помощи хука на обработку паник std::panic::set_hook, либо оборачиванием замыкания std::panic::catch_unwind - это как try/catch, только не в виде бранча, а с возвратом Result - хоть его и не рекомендуется использовать в качестве обычного try/catch'а).

Скрытый текст

Пример первого можно запустить и поиграться здесь:
https://play.rust-lang.org/?gist=11441f98ceff617e64250cc51fa449a0

fn main() {
    std::panic::set_hook(Box::new(|_| {
        println!("Custom panic hook");
    }));
    panic!("PANIC");
}

Пример второго - здесь:
https://play.rust-lang.org/?gist=f5e59bd64effde990618433712baf9b7

fn main(){
    match std::panic::catch_unwind(|| {panic!("PANIC")}) {
        Ok(_) => println!("Fine, no panic"),
        Err(_) => println!("Panic happened")
    }
}

(Если интересно, код подлиннее, с печатью текста паники и несколькими примерами https://play.rust-lang.org/?gist=dc005b0663247da459a6180e0751d565 )

Поэтому, думается, решение о том, чтобы не поддерживать синтаксис try/catch - идеологическое, отражение принципа Rust'а, что потенциально проблемные вещи должны делаться менее удобно, и с максимально явным отображением того, что именно происходит.

(Кстати, утверждение автора о том, что код на Rust'е с Result в два раза медленнее, чем код с исключениями, у меня вызывает сомнения - он ни кода не привёл, ни методологии сравнения. Хотя гипотетически, конечно, случаи, когда исключения критически быстрее, представить можно, на практике это маловероятно.)

хоть его и не рекомендуется использовать в качестве обычного try/catch'а)

catch_unwind - не то же самое, что try/catch:

This function only catches unwinding panics, not those that abort the process.

А ещё не всякий тип можно безопасно раскрутить, см UnwindSafe.

catch_unwind - не то же самое, что try/catch

В контексте нашего обсуждения - то же самое.

This function only catches unwinding panics, not those that abort the process.

Обычные паники - они вполне unwidnging panics. Иное нужно либо в конфиге прописывать, либо это уже не совсем паники - типа instrinsics::abort.

А ещё не всякий тип можно безопасно раскрутить, см UnwindSafe.

UnwindSafe - это не об ограничениях в принципе, это требование того, чтобы небезопасный код реализовывал этот трейт и обеспечивал безопасность раскрутки. Это продолжение концепции расстановки unsafe для небезопасных конструкций: минимизация случайных выстрелов себе в ногу. Если вы приняли решение стрелять себе в ногу, то вы сможете это сделать - через некорректные конструкции в unsafe и некорректную реализацию трейта UnwindSafe :)

Впрочем, в контексте обсуждения всё написанное выше, в любом случае - не имеющие значения детали. В статье ведь говорится об обычных пользовательских исключениях. Которые catch_unwind'ом ловятся и обрабатываются аналогично, просто с другим синтаксисом...

Ну и если вернуться к тому сообщению, на которое я отвечал, то ещё раз подчеркну, что по моему мнению, написанный в нём тезис, будто бы из-за try/catch'а какие-то фичи Rust'а "превращались бы в тыкву", мне кажется несостоятельным.

будто бы из-за try/catch'а какие-то фичи Rust'а "превращались бы в тыкву", мне кажется несостоятельным.

Ну про тыкву писал я. И мое утверждение заключается в том, что создатели Rust просто не смогли реализовать проверки перехода владения и заимствования при наличии try/catch и в следствии этого объявили исключения идеологически неверными и якобы отсутствующими в языке.
Но так как исключения (прерывания потока выполнения, в том числе и из-за ошибок) все равно никуда не деваются, то и возникает весь этот сыр бор с std::panic::set_hook и std::panic::catch_unwin для обработки того, чего в языке нет :-)

Так я ж, вроде, и показал, что Вы не правы. :) Вполне они смогли реализовать: catch_unwind делает в данном контексте то же самое, что try/catch.

Не очень понимаю, почему "сыр-бор"? std::panic::set_hook - это просто аналог std::set_terminate в C++. А std::panic::catch_unwinding - аналог try/catch, но реализованный так, чтобы уменьшить известные проблемы с использованием этого механизма, чтобы программисты осознавали, чего они делают, и какова цена этого. Аналогично как с unsafe... Что не так?

Аналогично как с unsafe... Что не так?

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

Я сам ничего не имею против Rust. Более того, часть его идей я взял и продолжил развитие в своем проекте. И мне больно смотреть, когда разработчики держатся за идеологию, но не хотят признавать некоторых шероховатостей при изначальном проектировании языка.

Но в настоящий момент идеология Rust уже сформирована и её нельзя переделать без коренных изменений в языке и сознании всего комьюнити, так как «бытие определяет сознание». Вот вы, вместо того, чтобы понять собеседника, ставите минус его мнению, потому что оно идет в разрез с вашими убеждениями.

Rust моей мечты — несостоявшийся язык / Хабр
Как легко перейти с Java на Rust: Особенности и советы / Комментарии / Хабр

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

Что Вы имеете в виду? Ничто не мешает при желании catch_unwind'ы вкладывать, и продолжать нормальное выполнение после возникновения паники. И по-разному обрабатывать разные паники тоже можно.

Вот вы, вместо того, чтобы понять собеседника, ставите минус его мнению, потому что оно идет в разрез с вашими убеждениями.

Учитывая, что минус вашей реплике поставил вовсе не я - я её не оценивал, а Вы сделали смелый необоснованный вывод, возможно предубеждение у Вас, а не у меня :)

В Rust'е кучу всего хочется улучшить. Но не думаю, что try/catch а-ля С++ был бы улучшением.

Пока я вижу, что высказанное Вами предположение явно неверно, но вы продолжаете упорствовать. Ок, жду вашего комментария на вопрос "что Вы имеете в виду?" выше - возможно, я что-то упустил.

Ок, жду вашего комментария на вопрос "что Вы имеете в виду?"

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

Поставил плюс Вашему комментарию :)

Опять-таки, при желании, использовать паники для программирования логики приложения можно, а написав тройку несложных макросов - и без бойлерплейта, не менее комфортно, чем в C++.

Но да, согласен с вами, что это дело вкуса - а точнее, идеологическое решение по уменьшению случайных выстрелов себе в ногу - что в Rust'е специально сделано, чтобы желание так использовать паники возникало как можно реже. Исключения переименованы в паники, чтобы люди осознавали, что это исключительные, а не нормальные вещи. Стандартные паники сделаны строками, а не типизированными - типизировать и различать потом по типу программист может только свои пользовательские паники, используя panic_any! вместо panic! - а стандартные паники по типам не различаются.

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

В гугловском стайлгайде использование исключений нежелательно и в C++-коде, и там есть разъяснения, почему:
https://google.github.io/styleguide/cppguide.html#Exceptions

Гугл рекомендует не использовать исключения из-за нарушения совместимости с существующим легаси кодом, в котором исключений как правило нет, из-за чего подобное изменение архитектуры будет очень трудозатратно. Поэтому там и написано "Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. "

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

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

Хотя как я и писал в самом начале, изначально их не сделали, потому что при их наличии сложно реализовать основную киллер фичу языка

Вы так уверенно об этом настаиваете как о причине изначального отказа от try/catch. Тогда это не Ваша гипотеза, а Вы знаете, где об этом можно почитать? Дайте, пожалуйста, ссылочку.

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

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

В тех ссылках на Грейдона, которые Вы дали, я тоже не вижу подтверждений Вашей гипотезы.

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

И каким же образом вы это выявили, только по названию ключевых слов?

Что же касается технической реализации, то будьте добры ссылку или напишите своими словами, как реализуется контроль владения и заимствований во время компиляции при наличии исключений в языке?

Так же как и сейчас он реализуется при наличии в языке паник.

С точки контроля владения, возбуждение исключения - всего лишь ещё один способ вернуть значение из функции.

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

А какие нибудь подробности или описание реализации, кроме "честное слово" у вас будет?

Я просто не вижу что именно тут требует доказательства.

Вы знаете, дяденька, я не настоящий сварщик, я маску на стройке нашёл, и экзамен Вам сдавать не готов. :)

Посмотрите сами код и комментарии некоторых из примечательных ревизий эволюции механизма раскручивания стека (а речь - о нём; ничего специфического для контроля заимствования, на мой взгляд, здесь нет - просто досрочный выход из функции, как ещё один "return", но рекурсивный - выход не только из самой функции, но и родительских):

Ссылки на код

Также посмотрите различные обсуждения, в которых нигде не фигурируют какие-то конфликты между раскручиванием стека и владением (я так понимаю, что именно его вы характеризуете как "киллер-фича"):

Ссылки на обсуждения

Наверное, самое любопытное в списке ссылок выше - это ссылка на публикацию Unwind considered harmful? Nicholas Matsakis. Он там упоминает, о чём они думали с Грейдоном, когда делали раскручивание. Никаких признаков справедливости Вашей гипотезы там нет.

Дальше чуть-чуть есть про borrow checker, но в очень гипотетическом ключе - что без раскручивания можно было бы сделать лучше, чем сейчас.


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

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

Вы потратили время на свои фантазии, которые совершенно не связаны с заданным мной вопросом. Выше я писал, что мне интересна реализация "контроля владения и заимствований во время компиляции при наличии исключений в языке"

Ключевым тут является во время компиляции. Ведь именно данная фича является главным аргументом апологетов за рекламирование Rust?
Но согласитесь, что раскрутка стека, ссылки на реализацию которую вы несколько дней искали в коде, никак не связаны с контролем владения и заимствований во время компиляции?

Раз вы реально не понимаете, давайте я еще раз попробую расписать проблему на пальцах.

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

Это можно попытаться исправить, например за счет введения в язык checked исключений, как в Java, но даже в этом случае остается класс unchecked исключений, которые компилятор никак не контролирует и которые можно обработать только в рантайме, т.е. при раскручивании стека, как в ваших примерах.

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

Но согласитесь, что раскрутка стека, ссылки на реализацию которую вы несколько дней искали в коде, никак не связаны с контролем владения и заимствований во время компиляции?

Ура, наконец-то вы это признали!

Осталось понять, каким образом они мешают контролю владения и заимствований, будучи никак не связанными с ними.

Осталось понять, каким образом они мешают контролю владения и заимствований, будучи никак не связанными с ними.

Так ведь это вы про нее писали, осталось понять, при чем тут вообще раскрутка стека?

Несовместимо с линейной логикой. Множество путей исполнения программы не даст проанализировать владение должным образом.

Может, будет когда нибудь rust++

Коллега, я не подписывался разъяснять то, что Вам интересно. Я обратил внимание исключительно на одно-единственное Ваше не соответствующее действительности утверждение, будто какая-то киллер-фича Rust'а не дала реализовать try-catch. Я убедился, что это исключительно Ваш домысел, фактов в пользу которого мне не удалось найти ни одного.

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

Я рекомендую всё же не побрезговать теми ссылками, которые я выше привёл, и уж как минимум прочитать публикацию Нико Матсакиса, приведённую там последней. Он там пишет, как можно дополнительно усовершенствовать borrow checker, если отказаться от ловли исключений, а вместо этого завершать поток.

Выглядит так, что Вы столкнулись с какой-то проблемой, которую сами решить не смогли, и сделали слишком далеко идущие выводы. Возможно, Вы что-нибудь почерпнёте из прочтения вот этого https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html - и, в особенности RFC, на который там даётся ссылка.

В большинстве случаев, если что-то отвалилось - мне и не нужно ничего анализировать. Ну отвалилось и отвалилось - или ретраим в цикле, или смиряемся и идем дальше с потерянными данными. Зачастую, весь try-catch заключается в том, что тебе и та часть, которая "catch" - совсем не нужна.

То есть, идея с тем, чтобы назвать по-настоящему панические случаи "паниками" - хороша, безусловно. Показывает, куда смореть и когда дергаться.

Но рядом нужна точно такая же система, которая позволяет делать "удобное goto", игнорирующее суть проблемы, которую можно проигнорировать (таких ооочень много).

В целом, есть идея, что серверное приложение никогда, ни при каких условиях, не должно останавливаться. 500-ка в браузере - это не то, что хочет видеть пользователь. Поэтому приложение и так всегда начинается и заканчивается top-level try-catch, с бесконечными ретраями, киллом показавшихся некорректными контекстов, и показом заглушек вместо 500-ок.

Вместо этого ты можешь делать fail fast и падать до обработчика-рестартера контекста в любой момент времени и абсолютно автоматически.

Я новичок в Rust и не могу уверенно судить о том, как правильно нужно работать с catch-unwind. Но мне кажется, что эти вопросы гораздо лучше проработаны в системах типа Scala+Akka, Erlang+OTP, да и даже в исходной Java с ее исключениями, которыми "неубиваемое" ошибками приложение тоже делается на раз и без включения мозга.

Ну, это не идеологией вызвано, не какими-то фундаментальными особенностями языка. Так что интересно, конечно, но вряд ли "поэтому".

Исключения обеспечивают более высокую производительность

Вот с этим утверждением я не совсем согласен, поскольку это зависит от многих факторов, хотя синтетический Фибоначчи и показывает ускорение. Пару лет назад я расширял библиотеку-wrapper для использования OpenCV в LabIVEW, где изначально обработка ошибок (размер картинок, тип, нулевой указатель и т.п.) была сделана как раз на исключениях. Это было красиво с точки зрения архитектуры, удобно и элегантно, но отжирало несколько процентов от общей производительности, а там где это использовалось был реалтайм и потоковое видео с детектора, мне была важна каждая миллисекунда. Я попробовал пооптимизировать всяко разно, но по итогу мне пришлось перейти к "Си-стилю" обработки ошибок на кодах возврата. К сожалению у меня под рукой нет кода, чтоб продемонстрировать. Это разумеется не значит, что утверждение абсолютно неверно, но это повод в случаях необходимости по крайней мере практически проверить и сделать трассировку и бенчмарк.

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

Поставить exception trap тоже не дармовая операция.

someCode() vs try{someCode();}catch(...) {} тоже может иметь значимые отличия, особенно если вызов someCode будет очень быстрым.

Угу, но этих строчек, по идее, должно быть на порядки меньше, нежели явных if, так что оно не должно сказываться на производительности.

Ифы быстрые и стек не задействуют.

Так exceptiontrap тоже сами по себе стек не задействуют.
А ifы иногда сильно мешают оптимизациям внутри ядра процессора

Ну так все правильно - серебряной пули не существует.

Наличие в коде потенциальных альтернативных путей исполнения делает невозможным ряд оптимизаций.

Явные if'ы обычно не уводят далеко и быстро синхронизируются с основным потоком.

Суть статьи кратко: почему делать плохо лучше, чем делать очень плохо. Почему не сделать нормально - умалчивается.

Дано: заголовок функции на С++ или Питон. Требуется: не читая весь код функции, всех функций, что она вызывает рекурсивно, и всех функций стандартной библиотеки, что в них вызываются - составить список всех исключений, которые в процессе могут выпасть.
Никак. Не возможно это. У джентльменов верят на слово документации, а программно перепроверить может только ИИ.

По мне, это куда больший пипец, чем проблема цветных функций, с которой все носятся сейчас. Открывая файл на Питоне, остаётся просто надеяться, что все исключения, которые при этом могут вылететь, наследуются от OSError ( срочно в номер: НЕТ), чтобы можно было их перехватить.

Например, по моему опыту, в коде на Rust встречается больше вызовов unwrap, чем мне хочется. Проблема здесь в том, что простое развёртывание результатов приводит к вылету программы на ошибках, которые в случае использования исключений могли бы быть видимыми для пользователя сообщениями об ошибках.

Мсье не знает ?, expect(), anyhow Рекомендую мсье дочитать хотя бы базовый учебник Rustbook до конца, потом уже говорить про Rust.

Итого: считаю исключения в той же группе, что и NULL - они ни в коем случае не ошибка, а ошибкой было забирать их из С++ в более развитые языки.

я после заголовка на деда морозе

Память небесконечна, CPU не могут обрабатывать все целые числа, а Деда Мороза не существует. Хоть моя точка зрения определённо не полностью объективна, я считаю, что от подобного подхода могут выиграть и другие системы. 

подумал pro реальное использование метода исключения

Пункты 1 и 3 тоже спорны, но отвечу по 2: не могут, потому что целых чисел бесконечно много, а элементарных частиц во Вселенной конечно много.

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

Исключения нужны, чтобы подсветить косяк разработки. Мол, да, тут не доглядел, тут не проверил - придётся переделывать.

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

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

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

Есть дизайн, где исключения как раз дают гарантии, что их отловили (checked exception в Java), правда на поверку оказалось, что это неудобно.
В "нормальных" языках как раз есть гарантии в том, что исключения отлавливаемы стандартными конструкциями (за это отвечает банальный try catch).
Более того, ошибки в стиле Go с гораздо большей вероятностью приведут к их пропуску, чем исключения (просто из-за больших шансах при очередной явной обработке пропустить ошибку, а не прокинуть выше).

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

ожидание подвоха является частью логики программы

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

Если ваш код работает на контроллере реактора ОС, то наоборот полезно для нервов.

Спросите Java как сделать ещё хуже (спойлер: checked exceptions)

Ну, checked exceptions - очень похожи на обязательные коды возврата )

Давно перешёл на котлин с явы, и до сих пор считаю ошибкой разработчиков котлина решение полностью избавиться от checked exceptions

Мсье не знает ?, expect(), anyhow

И запретить / предупреждать про unwrap на уровне проекта бесплатно без смс:

# Cargo.toml
[lints.clippy]
unwrap_used = "warn" # или "deny"

Требуется... составить список всех исключений, которые в процессе могут выпасть.

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

Мне важно открылся файл или нет.

Если нет - скорее всего, моя функция уже тоже бесполезна.

Ага. В конечном счете ошибки делятся на три группы:
"можно попробовать еще раз", "можно попросить пользователя попробовать еще раз с другими данными", "ничего нельзя сделать". И большая часть возни с ошибками - это подсказки пользователю для второй группы (или программисту для третьей).

Хм, а зачем нужно составлять список всех исключений, как с решением этой же задачи поможет переход на C-style и причем тут плохой дизайн Python (в той же Java нет проблем с перехватом всех возможных исключений)

Никак. Не возможно это. У джентльменов верят на словодокументации, а программно перепроверить может только ИИ.

В общем случае не может, следует из теоремы останова.

Особенно в динамических языках типа питона, где код может синтезироваться на ходу.

То ли дело в расте Result<(), dyn Error> или Err в Go - сразу понятно какие ошибки могут быть, да?

1) Так даже не скомпилируется, нужно как минимум Result<(), Box<dyn Error>>.
2) Ни разу за 8 лет не видел чтобы этим пользовались с трейтом https://doc.rust-lang.org/std/error/trait.Error.html, т.к. это аналогично перехвату всех исключений через try/catch в С++.

3) Вообще трейт Error существует в std ровно по одной причине: чтобы можно было распечатать любую ошибку в #[panic_handler]

4) И в библиотеках этим не пользуются (почти) с кастомными трейтами, т.к. это не удобно. Ведь единственное что с такой ошибкой можно будет сделать, это распечатать. Или городить возврат С-style enum из трейта. Но гораздо проще реализовать свою ошибку через anyhow или thiserror.

Точно так же если будет anyhow::Result<()>, как вам это поможет предугадать какая ошибка будет выкинута? По факту все опять сводится к "пиши хорошо, а плохо не пиши", но это так же и к исключениям применимо.

Anyhow используется только в конечном приложении, и только если вам не нужно знать тип ошибки. Об этом написано в документации к Anyhow, которую вы вероятно даже не читали:

Use Anyhow if you don't care what error type your functions return, you just want it to be easy. This is common in application code. Use thiserror if you are a library that wants to design your own dedicated error type(s) so that on failures the caller gets exactly the information that you choose.

Во всех остальных случаях используется thiserror. Авторы библиотек об этом знают, и вряд ли вы найдете хоть одну библиотеку, которая возвращает anyhow::Result.

Ну т.е. кроме "пиши хорошо, а плохо не пиши" аргументов нет. С исключениями точно так же библиотека может гарантировать только определенный тип исключений просто сделав catch с враппом, так в чем разница?

так в чем разница?

catch с врапом забудешь сделать, компилятор промолчит.

Это те самые, которые есть только в джаве, и задизайненые так, что половина "новых" (ver. 8+) фич с ними не работает? Я давно не слежу за языком, может что-то улучшилось?

Я напоминаю, что ветка началась с этого:

То ли дело в расте Result<(), dyn Error> или Err в Go - сразу понятно какие ошибки могут быть, да?

Вы описываете частный случай (dyn Error или anyhow), который имеет очень узкое применение. А вывод делаете про весь язык.

Документация к языку, стандартная библиотека и 99,99% библиотек реализуют врапперы вручную или пользуются thiserror. Это не "пиши хорошо, а плохо не пиши", это стандарт. А специально написать плохой код не проблема в любом языке.

С исключениями точно так же библиотека может гарантировать только определенный тип исключений просто сделав catch с враппом, так в чем разница?

Я не против исключений в целом, с чего вы это взяли. Может вы вообще не мне отвечаете? Перечитайте всю ветку.

Я только считаю реализацию исключений в С++ плохой, но checked exception - хорошей.

это стандарт. А специально написать плохой код не проблема в любом языке.

Если функция написана так, что в документации один тип ошибки, а кидается другой, то это проблема С++, а не плохого кода.

Если функция написана так, что она может выкинуть любую ошику, то это проблема плохо кода, а не Rust

Теперь понятно, хорошо что мы с этим разобрались.

Если функция написана так, что в документации один тип ошибки, а кидается другой, то это проблема С++, а не плохого кода.

Не приписывайте мне то, что я не говорил.

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

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

Если функция написана так, что она может выкинуть любую ошику, то это проблема плохо кода, а не Rust

И опять вы мне чего то приписываете. Документация к языку Rust говорит, что нужно писать враппер (что конечно тоже влияет на производительность, но меньше чем с исключениями). В результате функция "самодокументирована" по всем возможным ошибкам. И забыть обработать невозможно, будет как минимум warning.

Но если в данном конкретном месте не важен тип ошибки, то можно воспользоваться anyhow или Box<dyn Error>>.

Теперь понятно, хорошо что мы с этим разобрались.

А я прихожу к выводу что вы тролль, и дискуссия по существу вам не интересна.

Даже в таком варианте есть пара моментов:

  • Тут понятно, что ошибки вообще есть. Исключения об этом не скажут. Кроме checked в java, но их дизайн настолько плохой, что ими не пользуются

  • Тут надо явно (в расте) обработать их или прокинуть. Случайно нельзя забыть никак

Тут понятно, что ошибки вообще есть. Исключения об этом не скажут.

Ну будет написано в доке, вот и все. Да, может оказаться что неожидано возникает исключение, которого не ожидали, типа Null Reference - но точно так же функция в расте внезапно может упасть в панику.

Тут надо явно (в расте) обработать их или прокинуть

Вот есть функция fn process() -> Result<(), LibErr>

Ничто не мешает писать просто process(); игнорируя(случайно забыв) любые ошибки. А в лучшем случае в 99% реального кода будет просто process()?; , вот и вся обработка(по этому anyhow так и популярен). Чем это лучше исключений?

#![deny(warnings)] и вот любой забытый Result превращается в ошибку компиляции

Но от process()?; никак не поможет.

А почему оператор "?" вообще считается проблемой?

Ну будет написано в доке

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

Но я ленивый, люблю когда за меня компилятор работает.

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

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

Грубый пример на псевдоязыке, потому что синтаксис плюсов я уже подзабываю.

struct Data {
  handle: int,
  file: File,
}

fn Data.magic() {
  self.handle = 256*256;
  self.file = File::open("file.txt");
  self.handle = 0;
}

В мире где File::open() может выбросить исключение, компилятор не имеет права удалить строку self.handle = 256*256;, потому что это повлияет на значение переменной handle в случае исключения. Поэтому огромное количество оптимизаций (даже банальные перестановки независимых строк местами) невозможно в присутствии исключений, это только самый банальный пример.

Ну да. А в мире, где вы используете коды возврата, по результатам вызова строки 8 может случиться return, и компилятор точно так же не может выбросить строку 7.

Но идея понятна – "явное лучше неявного" и всё такое.

Это почему это? Return тут не написан. Код ошибки будет записан в file, и это уже проблема того ,кто будет file использовать в данном случае. Другое дело, что в Rust нельзя будет забыть проверить file на ошибку перед использованием.

Тут – не написан. А как только вы начнёте переписывать File::open на использование кодов возврата вместо исключений – будет написан..

По-че-му? Давайте вот вам реальный код на Расте

use std::fs::File;
struct Data {
  handle: i32,
  file: Option<File>,
}

impl Data {
fn magic(&mut self) {
  self.handle = 255*255;
  self.file = File::open("example.txt").ok();
  self.handle = 0;
}

fn use_file(&mut self) {
  // Вот тут проверяем ошибку только. Причём пока не сделаем проверку
  // у нас сам объект File банально недоступен
  if let Ok(f) = self.file {
    f.read();
  }
}
} // impl Data

Вариант, если файл обязательный (в С++ нельзя вернуть код возврата из конструктора, в расте, как видно, с этим проще):

use std::fs::File;

struct Data {
    handle: i32,
    file: File,
}

impl Data {
    #[allow(unused_assignments)]
    fn try_new() -> std::io::Result<Self> {
        let mut handle = 255 * 255;
        let file = File::open("example.txt")?;
        handle = 0;
        Ok(Self {handle, file})
    }
}

В Rust просто нет конструкторов, вот и проще :)

В C++ решается приватным конструктором и фабричным методом как в Rust.

компилятор не имеет права удалить строку self.handle = 256*256;, потому что это повлияет на значение переменной handle в случае исключения

А вы уверены, что это именно необходимая оптимизация компилятора, а не ошибка в программиста в логике работы программы? Просто я долго пытался придумать, для чего это могло быть нужно, и единственное назначение подобного повторного присвоения, это как раз детектирование исключения в функции File::open. Но в этом случае, подобная оптимизация будет вообще не к месту.

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

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

Для машины, скомпилировать/выполнить язык и проанализировать его - это две разные задачи. Для меня первым звоночком было сравнение двух IDE от одной и той же компании, одна для Java другая для Python. Задолго до всяких ИИ. Для Java меню с готовыми рефакторингами настолько огромное, что его пришлось разбить на подменю. Вынести выделенные переменные в отдельных класс, написать геттеры, обложить тестами и т.д. и т.п. Для Python в этом же месте ровно ОДИН рефакторинг был - переименовать переменную. И тот у меня сбойнул в первую же неделю. Потому что Java можно уверенно анализировать, а Python нельзя.

Исключения ОЧЕНЬ мешают анализировать язык и делать автоматические трансформации.

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

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

 И в некоторых случаях исключения реально могу значительно улучшить читаемость кода и упростить логику работы приложения (о чем и пишет автор).

Мне вспомнилась реально виденная реклама: "У коммерческих приложений, в отличие от opensource, исходный код не доступен, поэтому вам не придётся его читать!"

Я не назову улучшением читаемости, если мы часть поведения делаем не отражённой в коде и даже не доступной в IDE. Я не знаю ни одной IDE, где можно было бы нажать на catch, и она подсветила бы те throw, которые им покрываются, даже в других файлах и библиотеках. Я не знаю в С++ способа детерминированно убедиться, что данное исключение будет поймано, или что в данном месте кода может/не может быть непойманных исключений.

Я честно не понял, зачем вы пишите про подсветку кода. Если у вас нет кода библиотеки, то вы точно так же не сможете перейти в IDE на все return с кодами возврата (у вас банально нет исходников). А если обработка ошибок идет с помощью исключений, то тривиальный try { func_call() } catch (...){ } ловит все исключения в func_call() детерминированно и с гарантий.

Объясняю.

то вы точно так же не сможете перейти в IDE на все return с кодами возврата

Допустим в Rust. Я вижу в документации или просто наведя мышкой на функцию, её заголовок. А он выглядит например так. fn download_url( url: &String) -> Result<Vec[u8], DownloadError>; И я сразу же, без дальнейших действий, знаю, что при вызове функции может случится одно из трёх:

  • она вернёт массив с байтами, обёрнутый в Ок,

  • она вернёт объект структуры DownloadError, обёрнутый в Err ,

  • или случится системная паника.

Всё. Я точно знаю, что никаких других вариантов быть не может. Дальше, если меня колышет, почему именно скачивание не случилось, я открываю доку на DownloadError. А если нет - то и это мне не нужно, хватит знать, что он есть.

При исключениях же я вызываю функцию и не знаю, вернётся ли она вообще, может ли она вообще выбросить исключение, и какие именно, и сколько их - это всё никак не отображено в заголовке, и мне остаётся только надеяться, что в документации это всё перечислено. (Так вот, даже в знаменитых библиотеках часто перечислено не всё и не всегда).

 то тривиальный try { func_call() } catch (...){ }

Пройдёмте в Питон. Если вы в любой opensource проект пошлёте коммит с такой строкой - вас пошлют обратно. Обычно - вежливо. Потому что вы не знаете, что именно вы в этом месте ловите, чьё оно, откуда вылетело. Ловите гораздо больше, чем думаете, что ловите.

В Питоне даже ошибка синтаксиса - это исключение. То есть если в коде func_call() вы напишете имя переменной с опечаткой - то ваш catch это поймает, и вы даже не узнаете, что там была ошибка синтаксиса. То же, если юзер нажмёт Ctrl+C.

В других языках не столь круто, но всё равно вы рискуете наловить "не своих" ошибок и обработать их не так, как надо. И соответственно рискуете не получить "ваш" эксепшн, если по пути его поймает вот так другой разработчик.

может случится одно из трёх: она вернёт массив с байтами, обёрнутый в Ок, она вернёт объект структуры DownloadError, или случится системная паника

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

Что касается моего кода с try { func_call() } catch (...){ }, это пример детерминированного способа ловли всех исключений с гарантий, а не коммит в opensource проект.

Паника в Расте концептуально не предназначена быть обработанной. Как только кто-то вызывает панику, он знает, что убивает всё. Это не аналог исключений, это аналог if ( is_err ) sys::exit(255); Кстати, и то и то можно перехватить, но ситуации, когда это оправдано - эзотерические. Соответственно, мне как разработчику почти никогда не нужно учитывать возможность паники ранее. Не больше, чем возможность того, что весь комп повиснет. Мы же это нигде обычно не учитываем, кроме логики работы с внешним ресурсом.

Эмбед не в счёт, там своя атмосфера.

это пример детерминированного способа ловли всех исключений с гарантий,

А зачем нам обсуждать способы, не применимые на практике? Тут есть хаб "Ненормальное программирование", там народ на С++ такое вытворяет, что черти в аду смущаются. Но мы не будем говорить, что в С++ есть такие возможности.

Паника в Расте концептуально не предназначена быть обработанной.

Так про это я писал в самом первом комментарии к статье, насчет обоснования почему обработку ошибок нужно делать именно так - потому что по другому не получается.

А зачем нам обсуждать способы, не применимые на практике?

Я вам ответил на ваше утверждение "Я не знаю в С++ способа детерминированно убедиться, что данное исключение будет поймано, или что в данном месте кода может/не может быть непойманных исключений."

Это ваши слова и в них речь шла именно про С++, а не про Python или абстрактную OpenSource библиотеку.

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

Да никак, только найти все вызовы функций и глазами трассировать вызовы наверх, через все ветвления, потенциально весь код читать. Если и есть какие-то анализаторы, которые это делают, то они в стандарт языка не входят и гарантий не дают.

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

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

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

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

По форумам стоит вой на тему "Если я хочу использовать библиотеку, а она async, то мне тоже по всей программе приходится вводить async." Видимо, народ это серьёзно напрягает. Меня - нет, поэтому комментировать это я не могу.

С исключениями та же кухня. Если чей-то код использует исключения, то мне рядом с ним тоже приходится вкушать исключения по полной программе. Мне нужно узнавать в хрустальном шаре, у GPT, ловить автора в чате, в документации, может ли каждая из функций бросить исключения и какие.

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

Наконец автор просто врёт. Его пример с рекурсивной фибоначей просто написан неправильно. Там нужно было использовать and_then. Ну и сразу возникают вопросы про компилятор и режимы оптимизации.

За такое в Древней Греции били керопеджией по просопу.

Хм, просто убедиться, что на верхнем уровне есть try catch. Если использовать стандартные фреймворки - то он точно есть.
А в Java можно сделать checked exception, тогда компилятор подсветит все места, где исключение не обработано.

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

ЗЫ: ИМХО, исключения на то и исключения что это исключительные ситуации, а узнать открылся файл или нет можно из стейтов-кодов.

А когда оно всё же выстреливает, случается большой переполох.

А ещё если у нас конструктор перемещения объявлен noexcept , то мы получаем дополнительное быстродействие от коллекций так как можем избавиться от копирования.

В Java вы можете описать все возможные Checked exceptions. Но это оказалось совершенно бесполезным.
Но при вызове функции вообще не нужно знать, какие именно исключения она может выкинуть, достаточно сделать try catch и вернуть сообщение про текущий контекст.
Исключения - это не про базовый поток управления, это про исключительные ситуации и нужно только знать, что они бывают. Крайне редко нужно знать, какие именно произошли ситуации - и это в документации к функции как раз и описывается.

В Java вы можете описать все возможные Checked exceptions. Но это оказалось совершенно бесполезным.

Потому что было очень неудобным. И рядом с checked были и unchecked исключения, которые вроде про одно, но по факту разные концепции

достаточно сделать try catch

Как узнать, когда это делать? если

документации к функции как раз и описывается.

то сегодня функция не кидает исключения, а в следующей версии кидает, но доку не поправили. Вот и всё, ошибка как бы говорит вам "до встречи в рантайме".

На верхнем уровне - вообще всегда нужно делать try-catch (для unchecked exception), так как всегда могут быть системные исключения.
А для checked - компилятор подскажет. Но checked - это просто более удобный способ для "кодов возврата", но в сравнении с unchecked оказался неудобным.

А для checked - компилятор подскажет

Ну только их не используют почти никогда.

На верхнем уровне - вообще всегда нужно делать try-catch

Ну ок, вот мы ловим на верхнем уровне что-то. Теперь приложение не падает, а просто работает неправильно. А всё из-за какой-то одной функции в глубине приложения, которая вдруг стала кидать исключения. Сигнатура вроде не менялась, но теперь её поведение напрямую влияет чуть ли не на всё приложение, всё потому что забыли её обернуть в try-catch. Это ли не выстрел в ногу?

А как ты думаешь, почему индустрия ушла от checked exception? Потому что неудобно и уменьшает надежность, а не увеличивает.

Теперь приложение не падает, а просто работает неправильно

А почему оно работает неправильно? У тебя в глубине произошло что-то непредвиденное (исключительная ситуация) и ты на верхнем уровне обработки сообщил о том, что что-то не то произошло. Это как раз правильное поведение, именно то, что и ожидается при вызове.
Исключения надо ловить не там, где они возникли, а там, где это важно из бизнес-логики, а там уже все равно (обычно), какое было исключение и кто его кинул. Ну, просто отдал 500 - это лучше, чем упасть или чем пропустить проблему.

Только в реальности вы откроете фукнцию и увидите fn download_url( url: &String) -> anyhow::Result<Vec[u8]>;

Минус только в том, что вы не имеете никакого представления как корректно выйти из ошибочного состояния. Ровно и то, что привело его к таковому.

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

Вообще, тут напрашивается try/finally

fn Data.magic() {
  self.handle = 256*256;
  self.file = File::open("file.txt");
  self.handle = 0;
}

В мире где File::open() может выбросить исключение, компилятор не имеет права удалить строку self.handle = 256*256;, потому что это повлияет на значение переменной handle в случае исключения.

Это потому что у вас псевдоязык бедный. Если в языке есть конструкция try ... finally то предыдущий пример можно записать так:

fn Data.magic() {
  self.handle = 256*256;
  try {
    self.file = File::open("file.txt");
  }
  finally {
    self.handle = 0;
  }
}

- и вот у компилятора уже вполне достаточно информации, чтобы понять, что self.handle = 256*256; - это мертвый код, который можно выкинуть при оптимизации.
PS Да, я в курсе, что в C++ нет finally, а если попытаться эмулировать эту логику на деструкторе локального для функции объекта (который выполняет роль finally), то компилятору может быть сложно увидеть эту логику. Но это говорит лишь про C++, а не про недостатки использования исключений вообще.

ситуации типа вместо проверки существования файла – обращаемся к нему и бросаем exception, если его нет

Проверили, что файл есть. После этого другой процесс его удалил. И мы с полной уверенностью идём открывать этот файл.

Я немного про другое. Отсутствие файла может быть абсолютно нормальной ситуацией, и в этом случае код возврата уместнее, чем эксепшн. Транзакционность – отдельная тема.

Банально: если у нас мало эксепшнов и они случаются только в важных ситуациях – код проще отлаживать. Вплоть до того, что можно ставить breakpoint on thrown exception.

А потом вы начинаете работать с какими-нибудь сетевыми ресурсами (включая файлы на какой-нибудь волшебно сделанной шаре или даже NAS на привычных SMB/CIFS) и выясняется что в половине случаев у вас нет выбора как проверить наличие ресурса кроме как потыкав в него палкой, которое обязательно стрельнет в исключение)

У исключений проблемы производительности растут в основном из большого объёма раскрутки стека. В описанном сценарии следует обрабатывать исключения близко к точке их возникновения, как только становится понятно, что делать с исключениями.

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

Потому что компилятор вообще никак не оптимизирует "выполнене пути без исключений". Компилятор (грубо говоря, на самом деле это не совсем так, но это нюансы работы оптимизаторов) генерирует код для try и всего, что внутри, как будто этого try нет. Он просто для всех try пишет в сегмент данных дополнительную метаинформацию: интервалы адресов в коде и сопоставленный им адрес, куда делается переход для обработки исключения. Потом при выбросе исключений подключается рантайм, который ходит по стеку, находит адреса возвратов в нём (для этого так же может потребоваться дополнительная разметка call site-ов, но это опять же, данные) и дальше смотрит, есть ли в таблице try информация об этих адресах. Если он таковую находит, то делает (грубо говоря) простой jump в адрес обработчика.

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

it depends. Если путь к файлу задан юзером, то очевидным решением будет явная проверка наличия этого файла. Если же это какой-то ресурс приложения, или какой-то файл (например, кэш), который ранее создало само приложение где-нибудь в $HOME/.cache и по идее никто не должен был его удалять (если только это зачем-то не сделал юзер, ну или не поломалась ФС), то вполне валидно бросить исключение. Ну и обработать ошибку уже несколькими уровнями выше.

Да, безусловно it depends. Я к тому, что на исключениях удобно писать, когда их появление – именно что исключительная ситуация, а когда "ну что, бывает, значит, надо делать так, а если и это не получилось, то эдак" – коды возврата могут быть уместнее.

Ну и, конечно, нет резона ломать уже принятый в кодовой базе стиль. Т.е. лучше иметь унифицированный подход хотя бы в рамках модуля.

С исключениями проблемы начинаются, когда на них начинают писать логику

Вспомнилось питонье исключение StopIteration, которое вроде бы проблем не вызывает.

Питон просто настолько медленный, что оверхед исключений никак не влияет на общую производительность.

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

У исключений есть и ещё один туз в рукаве: при успешном выполнении они никак не снижают производительность, ведь мы разделяем поток управления.

Это не так. Код с обработкой исключений сложно распараллелить и тем более конвейеризовать.

Код с обработкой исключений сложно распараллелить ...

Не расскажите, почему? Насколько мне известно, обработка исключений в отдельном потоке ничем не отличается от обработки исключений в основном потоке приложения. И если логика параллелиться, то она будет параллелиться не зависимо от наличия или отсутствия исключений (тем более, что генерация исключений в STL или рантайме никуда не девается).

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

А векторных операциях даже сам процессор не гарантирует синхронности исключений с вычислениями.

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

Естественно, если реализация не поддерживает обработку исключений, то и использовать их будет нельзя, а если поддерживает, то можно. И все это определяется не тем, хорошие исключения или плохие, а особенностями реализации конкретно этой асинхронной библиотеки.

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

Если вы, например, словили своё исключение по делению на ноль посреди цикла (скажем, for (...) { a[i] = 1.0 / b[i];}), то обработчик исключений (который, возможно, находится в совсем другом модуле) обязан исходить из предположения, что цикл уже выполнился для начальных ненулевых элементов. А это очень дорогое предположение при определённых обстоятельствах. В то время как проверка через if может быть асинхронна.

обработка исключений подразумевает синхронное выполнение ветки исключения по отношению к основной ветке кода

Откуда вы взяли это предположение? Обработка исключений в потоке, это самый что ни на есть обычный код и никакая его "синхронизация с основной веткой" не подразумевается. Если вам по логике работы приложения нужно прокидывать сообщение о возникновении исключения из потока в основную ветку кода, то вам это нужно это делать отдельными объектами синхронизациями.

Про цикл ничего не понял. Ну словили вы деление на ноль, но откуда берутся обязательства в обработчике, причем тут вообще массив? Вы же сами закладываете логику обработчика, но на нее нет никаких подразумеваемых обязательств (кроме требований стандарта).

В то время как проверка через if может быть асинхронна.

Можете привести пример? Мне кажется, что вы говорите про какую нибудь асинхронную библиотеку, но про это я уже отвечал выше

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

Про цикл ничего не понял. Ну словили вы деление на ноль, но откуда берутся обязательства в обработчике, причем тут вообще массив?

Притом, что массив – это главное основание для параллельных вычислений.

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

Возникновение исключения в некотором операторе подразумевает, что предыдущие операторы выполнены, а следующие не выполнены. Иначе мы вообще не могли бы определить его причину.

Можете привести пример?

for (i=0; i<N; i++) {

a[i] = 1.0 / (abs (b[i]) < eps ? 1.0 : b[i]);

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

try {

for (i=0; i<N; i++) {

a[i] = 1.0 / b[i]);

}

catch {

... ошибка в i-м элементе! ...

} // такой цикл должен выполняться строго последовательно от 0 до N-1 в одном потоке

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

Значит я не правильно понял ваш предыдущей ответ:

подразумевает синхронное выполнение ветки исключения по отношению к основной ветке кода

Ваши примеры не эквивалентны. Первый код не возвращает ошибку, если b[i] ноль, а подсовывает вместо него единицу. Наверно нужно в первом примере либо делать две ветки для проверки условия, но тогда оптимизация по перебору элементов массива у вас не получится, либо в последнем примере не требовать индекс ошибочного элемента, но тогда компилятор выполнит оптимизацию без проблем даже при возможности исключения "деление на ноль".

Разумеется, они не эквивалентны! Я об этом и пишу.

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

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

В первом коде вы не возвращаете индекс элемента при ошибке, так откуда он появляется во втором примере?

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

Так как раз именно потому, что нет возврата, всё и работает эффективно.

А обработка ошибок есть, она в условном выражении.

Во втором примере нам индекс элемента нужен, чтобы занулить ошибочное значение в обработчике.

Что касается nan, то давайте не будем менять условия задачи на ходу. В статье написано про деление на ноль – я показал, как это пишется, если нужна эффективность. Про nan тоже можно расписать, но это ничего по существу не добавит.

Так как раз именно потому, что нет возврата, всё и работает эффективно.

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

Напишите эквивалентный первому фрагмент с помощью исключений. Не я виноват, что это невозможно или неэффективно.

Давайте попробует вернуться к вашему первому утверждению

Если вы, например, словили своё исключение по делению на ноль посреди цикла (скажем, for (...) { a[i] = 1.0 / b[i];}), то обработчик исключений (который, возможно, находится в совсем другом модуле) обязан исходить из предположения, что цикл уже выполнился для начальных ненулевых элементов. А это очень дорогое предположение при определённых обстоятельствах. В то время как проверка через if может быть асинхронна.

Я вообще не понял, что вы хотите показать (доказать) с помощью приведенных примеров. Ведь изначально речь шла про возможность оптимизации, верно?

Да всё верно. Обработчик ошибок с помощью условного оператора может быть автоматически распараллелен и векторизован (например, этот цикл может быть автоматически выгружен в GPU или распараллелен на 80 ядер CPU), так как это просто N независимых вычислительных процессов. А с помощью исключений – не может, так как распараллеливание теряет контекст, а обработчик контекста один.

Отлично, теперь идем дальше.

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

А кто вам обещал возвращать код ошибки?

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

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

Мне его обработчик деления на ноль нафиг не сдался, если он простым своим присутствием замедляет программу во много раз.

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

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

  if( check ){
    throw  "Error";
  }
  if( check ){
    return ERROR_CODE;
  }

Я же от вас прошу примера, в чем тут по вашему мнению состоит неправда.

Мне его обработчик деления на ноль нафиг не сдался, если он простым своим присутствием замедляет программу во много раз.

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

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

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

В вашем же примере я не понял, причем тут исключения, если два ваши фрагмента кода делают разные вещи.

Вы понимаете, что для опровержения общего утверждения достаточно одного примера?

Что касается вашего кода, то в нём нет ничего, эффективность чего можно было бы улучшить или ухудшить.

Представьте, что у вас вместо for – некий parallel_for, который может выбросить исключение и не гарантирует последовательное выполнение.

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

Ну разве что на уровне "исполняемый модуль грохнулся при работе".

В C# такие исключения оборачиваются в единый AggregateException, а составляющие его уходят в коллекцию InnerExceptions.

Из коробки или надо руками что-то делать?

И если из коробки – то ведь после эксепшна оно всё равно будет дожидаться окончания всех потоков, т.е. так применять имеет смысл, только когда на перерасход проца при исключении плевать (или перехватывать внутри и самому останавливать через CancellationToken или как его там?)?

Из коробки.

Цикл останавливается, но текущая итерация не отменяется (хотя возможна кооперативная отмена). Отсюда и возможность нескольких исключений.

Т.е. с CancellationToken стоит заморачиваться, только когда одна итерация длинная, но её можно прервать?

Исключения допустимы тогда, когда они не превращаются во второй контур логики и перехватываются как можно ближе к точке возникновения для ретраев или прокидывания в ошибку. В остальной логике гораздо удобнее и понятнее следовать парадигме railway oriented с Result<T> и обработкой Success и Fail. Потребовались годы, на самом деле, чтобы это осознать, набив шишек.

У меня тут парсинг вашего русского языка выдал ошибку неоднозначности. Есть два варианта как это можно прочитать:

  1. не (превращаются во второй контур логики и перехватываются как можно ближе к точке возникновения) == не второй или не перехватываются

  2. не превращаются во второй контур логики и перехватываются как можно ближе к точке возникновения == не второй, но перехватываются

Так вот если первый вариант верен то я с вами полностью согласен, а если второй то категорически не согласен.

Я почти согласен с автором статьи, в плюсах эксепции (и то как он их правильно использует - кэтчи как можно дальше от траев по стэку) конечно очень удобно, а экспектед имхо не взлетит без аналога растовского ?.

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

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

Где же нужно ставить кэтчи? В цикле обработки сообщений/команд/коннекций/реквестов/итд. Главный маркер здесь какой то цикл который запускает независимые друг от друга вычисления. Тогда эксепция просто абортит какое то одиночное вычисление, а цикл продолжается.

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

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

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

или прокидывания в ошибку

Плюс исключений в том, что для прокидывания в ошибку их даже ловить не надо

Вы реализовывали разветвленную бизнес логику с ретраями, прокидывая исключения через весь стек вызова? С исключениями нет линейности, это как гото, второй контур логики. В стеке дотнета, например, есть такой товарищ Владимир Хориков, он очень подробно эту тему освещает, какие плюсы даёт использование railway oriented подхода в энтерпрайзе и где целесообразно ловить исключения. Да и на мсдн это есть, и на NDC выступлениях разных людей.

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

 в рассмотренном ниже примере реализация на C++ с использованием исключений примерно в четыре раза быстрее, чем на Rust.

причем кода на расте в итоге не рисует и сравнивает плюсы с плюсами. И это не считая, что он нагородил кучу бесполезных проверок на ровном месте, которые ожидаемо все замедляют.

Почему он перезаворачивает unexpected тоже вопрос отдельный

Исключения (и, кстати говоря, промисы тоже) плохи тем, что они подменяют пусть и сложный, но правильный поток (flow) упрощённой версией. Иногда переупрощённой. Та простота, что хуже воровства. Пример можно взять прямо отсюда, с Хабра: недавно была статья, в которой автор жаловался, что в Go он не может сделать примерно так (пишу по памяти, но вряд ли сильно ошибаюсь):

try
{
    данные = запросить_данные_из_БД();
    новые_данные = обработать_данные(данные);
    положить_данные_в_БД(новые_данные);
}
catch (...) // Паттерн "покемон": "Поймай их всех!"
{
…
}

Представим себе, что запросить_данные_из_БД() — сама по себе дорогая операция, а обработать_данные() берёт свободный ресурс из пула (специализированное ядро, например), вешает на него таску и счастливо джойнится до конца исполнения. И внезапно менеджер пула подвисает и перезапускается.

Что можно было бы сделать?

Как минимум, дать ему несколько шансов, если запрос менеджера пулов обломился. То есть, do {res = обработать_данные(данные); } while (pm_timed_out == res && i++ < 10);. Именно в эту сторону, как я понял, нас и толкает Go. (Я на нём не писал, а про его обработку ошибок узнал только из критической статьи). Автору это не понравилось, и он придумал какой-то способ это обойти и приблизиться к своему идеалу (см. выше).

И теперь данные из запросить_данные_из_БД(); пропадут. Ведь в лучшем случае из catch-секции мы придём туда, откуда заново запустим ту же функцию.

Зато всё просто и наглядно. Даже слишком.

Как по мне, исключения должны быть действительно исключительными ситуациями, а всё остальное в нашем потоке должно быть поточным, т.е. без прыжков туда-сюда. В полном соответствии с названиями.

И то же самое относится к промисам. Не надо с их помощью спрямлять flow сильнее, чем позволяет здравый смысл.

Что можно было бы сделать?

Для меня это выглядит как преждевременная оптимизация.

Если будет надо, try всегда можно разбить на два. А вот проверки ошибок, если промежуточные результаты не нужны, уже в одну не склеишь.

Если у нас в первой части результат, доставшийся сравнительно дорогой ценой, а во второй части мы можем его потерять из-за сравнительно мелкого недоразумения, это не преждевременная оптимизация, а просто качественное проектирование.

Насколько я понял из той статьи, Go как раз заставляет качественно проектировать, и именно это в нём и не нравится.

заставляет качественно проектировать

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

Хотя давеча видел точку зрения, что серьёзные люди свой серьёзный софт пишут только по "водопаду": аджайл - это для тех, кто сам не знает, чего хочет.

А это не так

Я очень извиняюсь, но это у кого как.

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

Вторую версию пришлось проектировать уже по уму. Дошло до введения атрибута [Bottleneck], которым я размечал код, и который при прогоне автотестов тщательно контролировал. И вот уже она, в отличие от первой версии, от себя тормозов к платформе не добавляла. И ей можно было реально пользоваться даже после подписания акта о приёмке.

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

А вот проверки ошибок, если промежуточные результаты не нужны, уже в одну не склеишь.

Почему? Если у нас функция возвращает условный Maybe или Either и язык поддерживает – прекрасно склеиваются.

я отдаю приоритет выполнению серверного процесса, и скорее смирюсь со сбоем одного запроса, чем с вылетом всего приложения.

Я около 10 лет назад "на коленке" сделал серверное приложение, которое именно вылетало в некоторых (редких) критических ситуациях. Ну вот нужно было "к утру" (не шутка), разбираться некогда было. Сервис Виндоус, после падения автоматом перезапускается.

Почти 10 лет прошло. Я ещё помню об этом техдолге, и я к нему обязательно вернусь. Как только все остальные дела переделаю...

Это называется Let it Crash. И, например, в Erlang-е довольно распространенная идиома.

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

Проблема с исключениями в том, что они и сейчас не быстрые. И как раз в моменты выбрасывания исключений, и за это и борьба. Цена выброса исключения в тысячи или десятки тысяч раз дольше чем тот же std::expected. Представьте если бы API ОС всегда когда не хватает переданного буффера - кидало бы исключение? Ну или похлеще случаи, типа EINTR. А самое главное, чем дольше они выбрасываются - тем меньше запросов вы можете обслужить.

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

PS: Вот еще статья на тему + взгляд с другой стороны: https://johnfarrier.com/c-error-handling-strategies-benchmarks-and-performance/

PS: Вот еще статья на тему + взгляд с другой стороны: https://johnfarrier.com/c-error-handling-strategies-benchmarks-and-performance/

Это не взгляд с другой стороны, а подтасовка фактов, притянутая за уши.
Обработка ошибок на коде возврата реализована как возврат целого числа, тогда как обработка ошибок на исключениях реализована как возврат текстовой строки. Еще бы ей не быть в 49540.11784 раз медленнее, если нужно каждый раз выделять память в куче :-)

Вы же всегда можете сделать свои замеры. Ну, наивное изменения бенча из этой статьи на throw в цикле и возврат не пустой строки ч/з std::unexpected - дает разницу в 8000+ раз. При этом я вообще не думаю что это близко к реальности, так как в таком случае у стэка нет глубины.

А касательно кодов возвратов - так на то они и коды, и представляют более распространенный случай/подход.

Так же там не был рассмотрен вариант в котором возвращается bool, а код возврата и возможно даже строка в TLS, с доступом ч/з GetLastError и тому подобное.

С исключениями C++ десять тысяч итераций с n=15 выполняются за 7,7 мс. Со значениями возврата std::expected они выполняются за 37 мс — почти пятикратный рост времени исполнения! Можете убедиться сами: Quick Bench

Ну туфта же.

Человек убил всю производительность использованием std::expected<>

Простая замена на C-style код ускоряет его в 25(!) раз относительно "с++" и 4.3 раза относительно версии с исключениями.

https://quick-bench.com/q/LFPzMwd58xhaPBasnz3oUDKhgiM

constexpr unsigned invalid = ~0;

unsigned do_fib_expected(unsigned n, unsigned max_depth) {

   if (!max_depth) return invalid;

   if (n <= 2) return 1;

   auto n2 = do_fib_expected(n - 2, max_depth - 1);

   if (n2 == invalid) return invalid;

   auto n1 = do_fib_expected(n - 1, max_depth - 1);

   if (n1 == invalid) return invalid;

   return n1 + n2;

}

Не туфта, автор сравнивает исключения не с кодами ошибок, а с тем же Result из Rust

Автор приводит ссылку на сравнение C++ реализаций exceptions vs std::expected. А затем достаточно голословно добавляет про Rust. Сравнить exceptions vs C-style - отличная мысль, а результат приведенный @beeruser, похоже, раскрывает лукавство автора и обесценивает все сказанное им о производительности.

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

Ну и отдельно хочу заметить, что автор смешивает два разных типа исключений. Те, которые выбрасывает сама программа через throw, и те, которые приходят из OS. Обработка вторых, это отдельная тема, на мой взгляд не имеющая отношения к вопросу.

Обработка ошибок в стиле Си - это настолько неудобная вещь, легко подверженная ошибкам, что пора бы уже придумать ей замену. 2024й год на дворе всё-таки!

Товарищ переводчик, не переводи глупостей

Автор, знающий даже современный c++ не понимает зачем нужно функциональное программирование. Вот как привычки определяют сознание!

Жаль, что rust иногда выкидывает панику, то есть чистоту функций не гарантирует. Например, при целочисленном делении на ноль или выходе за пределы массива или выделение памяти. И лови эту панику потом. Это следствие что сам процессор или ось выкидывает "исключения"

Концептуально исключения и возврат ошибок строятся на разных подходах.

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

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

Как говорится, выбирайте из двух зол.

Да, ошибок.

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

И как это обычно бывает в этом месте в контексте не достаточно информации для анализа проблемы :)

Поэтому мы перехватываем исключение ниже и обогащаем (оборачиваем один эксепшн другим) нужной инфой.

Но тоже самое делается и с возвращаемыми ошибками.

А точно вызывающий контекст имеет достаточно информации, чтобы добавить к ошибке что-то полезное? Так бывает далеко не всегда. Особенно если код написан по всем канонам декомпозиции.

Нет. Это я и указываю. Что нет редко когда по факту достаточно прокинуть ошибку до самого верха. Чаще нужно ее обрабатывать раньше чтобы обогатить данными для последующего анализа.

И в этом плане разница между возвратом ошибки и киданием исключения уже не такая и большая.

Просто дело вкуса и специфика языка.

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

Но ведь упадет не всё приложение, а только отдельный тред.

Здесь любят исключения, а день-два назад кто-то писал, что их не любит.

На моей памяти, если разработчик юзает исключения, он просто крайне ленив. Настолько ленив, что его гениальные исключения перехватываются только по ...

В старом коде это была не просто боль.

Второй кейс, очень актуальный в прежние времена и не настолько актуальный сейчас - это компоновка либ, собранных разными компиляторами.

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

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

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

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

Хм, и как ты пробросом исключения прострелишь ногу? Игнорированием ошибки - да, легко и это чаще встречается для Result или checked exception.

Забыл где-то поставить catch на нечасто срабатываемый тип исключения - исключение не поймается и улетит до main или того дальше, что равносильно падению программы. Хуже того, когда в вызываемом коде добавляют ещё один возможный тип исключений, без добавления соответствующего обработчика. Одно из решений проблем - ловить всех наследников базового класса исключений, но, кажется, так делают не слишком часто.

Прострел ноги тут в том, что непроверяемое на этапе компиляции наличие catch блоков ведёт к созданию программ, которые иногда падают из-за непойманного исключения. С Result классами ситуация лучше, если программист не злоупотребляет с unwrap.

Checked исключения схожи с Result классами, и они лучше обычных, но есть они не во всех языках (в C++, например, их нету). И опять же, неясно, зачем они такие нужны, если можно использовать Result класс и вообще отказаться в языке от исключений.

Прострел ноги тут в том, что непроверяемое на этапе компиляции наличие catch блоков ведёт к созданию программ, которые иногда падают из-за непойманного исключения.

Тут проблема скорее в синтаксисе try/catch конструкций для типизированных исключений. Если попробовать придумать синтаксис, обеспечивающий декларацию перехвата исключений без дополнительных конструкций try/catch, тогда и исключения можно было бы использовать более безопасно.

вообще отказаться в языке от исключений.

Как оказалось, полностью отказаться от исключений все равно не получается

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

Хм, а зачем перебирать в catch все типы исключений? Обрабатывай те, которые хочешь рассмотреть отдельно, а остальное закроется catch(). Если уж в языке нормально сделаны исключения, то и возможность поймать "все" тоже поддерживается. Ну или ловить все по базовому классу, это тоже стандартная практика (и правильная).

Ну и до main редко долетает, обычно все ловится гораздо раньше, на уровне фреймворков, но это если не хочется их ловить раньше.

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

При этом логика работы с exception (именно как исключениями) гораздо удобнее и не приходится ее копировать другими средствами, как делают в Go.

как ты пробросом исключения прострелишь ногу

Функция раньше не кидала исключений, а вдруг стала. Как такое отследить на этапе компиляции?

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

Ловить что-то на верхнем уровне - это понятно. Проблема не в этом

так как ловить исключения на уровне бизнес-логики нужно все равно 

Как узнать, какая именно функция должна быть обёрнута? Оборачивать каждую? Ну ок, допустим мы знаем, что библиотечная функция (к которой у нас нет доступа) может кинуть какое-то множество исключений, и хотим в этом случае подставить дефолтное значение. Внутри этой функции есть вызовы других функций, которые тоже могут кинуть исключение. И вот проблема - если ловить всё, то можем отловить что-то критичное для приложения, потому что мы не знаем полный список того, что могут выкинуть функции. И на верхнем уровне вместо реагирования на критическую ситуацию мы получим нерабочее приложение. Перечислить только известные исключения? Но тогда мы можем пропустить что-то незначительное, и вместо дефолтного значения у нас всё упадёт до верхнего уровня.

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

Я не понимаю проблемы. Если я вызываю какую-то функцию и она может кинуть исключение (а любой код в системе может кинуть исключение) и я знаю, как его обработать - я делаю try-catch и обрабатываю исключение. Мне не нужна никакая информация про саму функцию, я просто знаю, что в этот момент я могу разобраться с проблемой - и получить всегда корректную логику.
Так как реально бывает только две разумных реакции - "повторить" или "поднять выше", то не понятно, зачем нужно знать список возможных ошибок и для чего.
Крайне редко нужно реагировать на какие-то конкретные исключения (и, обычно, только для переупаковки их в какие-то свои сообщения, не для разной логики).

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

В Java, заметим, тоже все с исключениями типобезопасно, нет никаких проблем выделить собственные исключения и работать с ними одним способом, а с системными - другим. И компилятор все нужное проверит.
А Either стоит использовать для управления потоком исполнения, а не для обработки исключительных ситуацией. И наличие и Exception и Either/Option/Result позволяет разделить логику и ошибки.

А ты много видел проектов на Java/С#/Kotlin? Более-менее современных (не старше 10 лет)?

Так как реально бывает только две разумных реакции - "повторить" или "поднять выше", то не понятно, зачем нужно знать список возможных ошибок и для чего.

Смотря какой тип программного обеспечения. Например, в управляющих программах разумной реакцией обычно является “ингнорировать” или “занулить результат”.

Ты рассуждаешь здесь с точки зрения транзакционной логики, которая не везде применима.

Натолкнулся на канониче ский пример того, почему я не люблю исключения. STL

https://en.cppreference.com/w/cpp/filesystem/path/parent_path

May throw implementation-defined exceptions.

Чего? Я спросил родительскую дирректорию, а мне бросают исключение. А могут и не бросить, а какой тип у этого исключения? А как мне на него реагировать? Что вообще происходит?

1) это будет что-то из "путь недоступен" (ака "у вас диск вынули" или "прав нет") или подобное

2) либо вы на такое не рассчитываете, и можете там ничего не обрабатывать (и вас ОС завершит), либо ставите перехват всех исключений и идете по ветке "случилось страшное - нет такого пути" и кидаете своё (вами ожидаемое и вам известное) исключение/возвращаете код возврата

1) Этого не должно быть, ибо про path читаем:

An object of class path represents a path and contains a pathname. Such an object is concerned only with the lexical and syntactic aspects of a path. The path does not necessarily exist in external storage, and the pathname is not necessarily valid for the current operating system or for a particular file system.

Т.е. для класса содержать некорректные пути - это норма.

Для проверки доступности есть метод exist. Собственно, я им и собираюсь пользоваться. И если пути нет, то и делать ничего не собираюсь. Т.е. это штатная ситуация.

И вот теперь снова я вопрошаю: что случилось такого страшного, что вылетело исключение?

ну, мало ли - а вдруг памяти не хватило для создания нового path ?
или это был примонтированный путь, когда вы опрашивали exist - он был, а когда вызывали parent_path - то он уже отмонтировался (или драйвер FS обновили/удалили или еще что-то)

а когда вызывали parent_path - то он уже отмонтировался

Приводил же цитату стандарта (черновика): доступность\недоступность не должно влиять. Если не хватило памяти, но так должен вылететь std::bad_alloc, а неизвестно что.

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

я подозреваю что фраза

May throw implementation-defined exceptions.

написана автором странички помощи - по его личному опыту реального кода.
Обычно там все работает нормально.

Но, увы - иногда и noexcept бросает исключения 😏

Картинка по теме топика

И как, вам такая ситуация кажется нормальной?

1) нет, не кажется

2) в железе тоже есть исключения (только они называются прерывания), которые работают отдельно от кодов возврата.

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

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

ИМХО исключения были бы хороши, если бы они работали так же быстро как и прерывания (вместо разворачивания списка вызовов и т.д.)

Исключения были бы хороши, если гле-то рядом была аннотация всех возможных типов, которая выкидывает функция. Если кто-то что-то добавил выкидывающие окромя них происходила бы ошибка компиляции.

ну вот в java есть CheckedExceptions не скажу, чтобы это что-то очень сильно меняло (кроме небольшой возни с оборачиванием или наследованием).

во-вторых, если у вас есть слой ОС/ядра/библиотек - у вас должен же быть способ ловли ошибок оттуда? Понятно, что паранить плохо, но нет никакой гарантии что "там внутри ошибка"....

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

опечатка

*паранить => параноить

А зачем? Какой смысл в этой аннотации, что ты будешь с ней делать?

ну как же - красиво выглядит

потом, если в будущем надо будет поменять что-то в коде - сразу видно, что там внутри что-то поменялось, когда аннтация изменится, все кто ее использовали тут же это увидят и перепишут у себя в коде ;)

Ну, можно считать список возможных исключений частью контракта, но это не очень помогает со всякими техническими исключениями.
Собственно, checked exception и предлагали такой подход, но в результате в том же Spring JDBC чуть-ли не самое полезное - замена checked exception на аналогичные unchecked.

Скорее нужен способ прокидывать в документацию список тех исключений, которые надо в документации показать. С автоматическим "сбором" всех исключений по стеку. Но это уже про какие-то фреймворки вокруг web api, да и то не очень востребовано (хотя мы в своей библиотеке похожее делаем, но на уровне всего сервиса, не конкретного метода).

извините, я там у себя забыл тег "сарказм" указать.

по теме - я согласен что какой-то должен быть способ сбора сведений о потенциальных исключениях,

но вопрос в обновлении - как разумно сделать так, чтобы при обновлении библиотеки файловой системы (и добавлении исключений), потом не ломалась библиотека работы с картинками?

Похоже что компилятор вычислил значение на этапе компиляции.

Само собой. И смог он это сделать, потому что в коде нет исключений.

Забавно, что если в вашем примере перейти к строковой ошибке, то отставание будет всего в 2 раза.

Похоже, тут еще много зависит как от компилятора, так и от самой stl.

Нет. Это потому, что именно функция с std::expected и типы подобраны так, что компилятор всё умудрился вычислить на этапе компиляции. Использование исключения в функции автоматически включает runtime и функция будет вызываться явно (также для такой функции невозможно указать constexpr).
Правда это не отменяет того факта, что тест сделан некорректно, т.к. на практике количество ситуаций, где все-все входные данные известны на этапе компиляции, весьма ограничено.

а поставьте там (45, 200); и получится вот такое:

Скрытый текст

throws - в 2 раза быстрее чем expected

Почему я предпочитаю исключения, а не значения ошибок

Потому что я м%дак.
/ music /
Directed by Robert B. Weide.

Главная проблема обработки ошибок в том , что чем более навороченный там код, тем большая вероятность, что там ошибка.

увы - в наше время "навороченный код" есть даже в микрокоде процессора.

что уж говорить про всякие навороченные "Hello World" (которые полагаются на код ОС) ...

Много лет тому назад один мой программист писал на шарпе весь проект на одних try cath . Но в catch ничего не делал. Пришлось уволить.

Я не против исключений, но я вам добавил еще один вариант в тест (не нужно вдаваться в дискуссии правильности, это просто для теста):

unsigned do_fib_error(unsigned n, unsigned max_depth) {

   if (!max_depth) return UINT_MAX;

   if (n <= 2) return 1;

   auto n2 = do_fib_error(n - 2, max_depth - 1);

   if (n2 == UINT_MAX) return n2;

   auto n1 = do_fib_error(n - 1, max_depth - 1);

   if (n1 == UINT_MAX) return UINT_MAX;

   return n1 + n2;

}

https://quick-bench.com/q/bjx2YxH8yqFnUADebpsBCPZfuFk

Забавно это читать в контексте крестов. Обычно исключения запрещены:

  • локальным код-стайлом (долой этот жирный goto с размотками стека);

  • не-локальным, но которому приходится следовать, иначе твой продукт не купят (привет, autosar и другие производные misra, стайлы под что-то вроде ДО-174);

  • тупо не поддерживает платформа;

  • тупо не можешь их использовать, тк к примеру пишешь что-то реалтаймовое типо RU/eNB

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

Или здесь найдется кто-нибудь, кто пишет что-то на крестах и может использовать исключения?

Да.

всё абсолютно безопасно

На самом деле, нет: ещё возможно переполнение при делении минимально возможного int (для двухбайтного это -32768, для четырёхбайтного - (int)0x80000000) на -1.

А, не дочитал.

Сравним это с ошибками в функциональном стиле

Так вот давайте с ними и сравним, а не с гошным `if err != nil return nil, err;`, от которого функциональщики ловят кринж!

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