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

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

Давайте рассмотрим пример, в котором применено защищённое от ошибок целочисленное деление:

int safeDiv(int a, int b) {
   if (b == 0)
      throw Div0(); // Исключения передаются особым образом
   return a / b; // Теперь-то всё абсолютно безопасно, ведь так?
}

Новые языки программирования склонны применять сообщения об ошибках в функциональном стиле и кодировать ошибки в возвращаемый тип. Например, Go кодирует ошибку в возвращаемый тип при помощи кортежа (res, err), а Rust возвращает Result<T, E> — тип-сумму результата и ошибки.

Даже в более старых языках наподобие C++ значения ошибок сегодня включают в стандартную библиотеку при помощи std::expected:

// Ошибка стала частью сигнатуры функции 
std::expected<int, Div0> safeDiv(int a, int b) {
   if (b == 0)
      return std::unexpected(Div0());
   return a / b;
}

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

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

На самом деле, чаще всего всё происходит наоборот.

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

▍ Так было ли использование типов результатов ошибкой?


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

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

▍ С исключениями гораздо проще работать


Стоит учитывать, что я пишу эту статью с точки зрения человека, работающего с серверным кодом, значительный объём которого хранится в памяти. Это значит, что я отдаю приоритет выполнению серверного процесса, и скорее смирюсь со сбоем одного запроса, чем с вылетом всего приложения. Чтобы соблюдать SLA аптайма, мне нужно правильно обрабатывать любые сбои. Да, даже те, при которых не справляется абстрактная машина языков программирования и начинают проявляться ограничения реальной системы. Память небесконечна, CPU не могут обрабатывать все целые числа, а Деда Мороза не существует. Хоть моя точка зрения определённо не полностью объективна, я считаю, что от подобного подхода могут выиграть и другие системы. Даже в случае фронтенд-приложений я бы предпочёл увидеть диалоговое окно ошибки, чем вылет и потерю состояния.

В случае серверных программ у нас есть хороший способ обработки ошибок: отправка сообщения об ошибке клиенту, а если это не срабатывает, то закрытие соединения. Да, даже в ситуациях out-of-memory и SIGFPE, которое возникает при делении на ноль, можно отправлять сообщения об ошибках, а не создавать панику для всей системы. Да, закрытие всей программы — это «безопасно», но не особо продуктивно и к тому же может представлять собой вектор для DoS-атак.

Исключения чрезвычайно всё упрощают. Где бы мы ни находились, пусть даже на глубине в сотню стеков вызовов в далёком уголке программы, при обнаружении ошибки мы выбрасываем исключение. После этого всю сложную работу берёт на себя раскрутка стека. В Linux это обычно происходит в libgcc, определяющей способ раскрутки при помощи двоичного файла с DWARF eh_frame. Красота раскрутки заключается в том, что вызываются все деструкторы, удаляются все дескрипторы ресурсов, а все удерживаемые блокировки снимаются. А лучше всего то, что вся магия происходит за кулисами, ведь компилятор и так генерирует правильную информацию о раскрутке. [Если, конечно, вы не занимаетесь реализацией JIT и написанием компилятора, но это уже тема для отдельного поста.]

Так что простого блока try-catch в начале нашей обработки соединения будет достаточно для правильной обработки ошибки и передачи сообщения клиенту.

▍ Бойлерплейт


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

Давайте вкратце исследуем метрики кода двух примерно одинаковых проектов баз данных. CockroachDB — проект на Go с явно обрабатываемыми ошибками:

$ rg 'if err != nil' | wc -l
24570

Почти 25 тысяч путей обработки ошибок!

Большинство этих ошибок — это артефакты повторяющегося стиля отчётности с пересылкой, но есть и достаточно много случаев, просто используемых для log.Fatal(). На мой взгляд, это не особо удобно.

CedarDB — наш проект, в котором используются исключения C++:

$ rg 'catch \(' | wc -l
140

В сто с лишним раз меньше кода!

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

Разумеется, существуют решения, позволяющие снизить объём бойлерплейта. В Rust можно подсластить некрасивый синтаксис макросом ?. Он скроет все повторяющиеся проверки, но всю работу вам всё равно придётся делать. Кроме того, теперь ваша функция возвращает коды ошибок, то есть вам придётся изменить публичный интерфейс и рекурсивно менять все функции, вызывающие эту функцию. Вместо того, чтобы работать в своём уголке, ваша дополнительная проверка теперь распространяется на половину кодовой базы. С другой стороны, всегда есть искушение добавить немного unwrap(). Вас эта змея точно не укусит.

▍ Системные ошибки


Что, если я скажу вам, что большинство программ уже обрабатывает гораздо меньше ошибок, чем следует? Вернитесь к примеру с safeDiv из начала статьи и попробуйте поделить -232 на -1. Получится 232, но это число не умещается в диапазоне int. Ой-ёй.

Как же гарантировать, что ваши программы не вылетят? Добавлять возврат ошибки каждый раз, когда функция делает assert? Но и это ещё не всё: распределение памяти может сбоить, стек и арифметические могут переполняться. Без выбрасывания исключений такие состояния отказа часто просто оказываются незамеченными. Вызываете, казалось бы, невинную функцию? Переполнение стека! Ой, ваш сервер свалился. Складываете два значения? Паника в Rust! Присваиваете значение временному объекту? Поздоровайтесь с OOM killer! В любой части потока кода, которая управляется пользователем (а чем он не управляет?), могут возникнуть подобные ошибки.

Что касается распределения памяти, если вы работаете с Go, то вам не повезло, ведь у него есть сборщик мусора. На Rust у меня больше надежды, ведь работа по интеграции с ядром постепенно заставляет его расти и создавать более надёжные интерфейсы, не приводящие к панике, например, Box::try_new(). Но это потребует рефакторинга всего написанного кода.

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

▍ Исключения позволяют создавать более качественные сообщения об ошибках


Можно подумать, что при более явной обработке ошибок ближе к источнику ошибок мы наверняка будем получать более качественные сообщения об ошибках. Но мой опыт снова говорит об обратном: значения возврата при ошибках часто содержат мало информации и приводят к созданию плохих сообщений об ошибках. Классический пример — это системные вызовы, обычно отвечающие стандартам C. Возвращаемое значение часто оказывается просто кодом ошибки -1, а настоящая причина по каким-то причинам передаётся как побочный канал через errno. Разобраться в истинной причине сбоев часто невозможно по одному лишь коду ошибки, для этого требуется существенный объём контекста вокруг вызывающего кода. Вы получили EINVAL. Что делать дальше? strerror сообщает: «Invalid argument». Замечательно! Вероятно, система точно знает, что пошло не так, но отказывается сообщать подробности.

То есть errno сам по себе не особо полезен. Для получения нужных сообщений об ошибках вам нужен системный вызов в качестве контекста, но даже в этом случае ошибки остаются непонятными. Возьмём для примера write(2), который может возвращать EINVAL в случае, когда «object which is unsuitable for writing» или в случае невыровненных буферов. Но как определить, какой из этих двух вариантов верен?

Новые языки справляются с этим немного лучше, но ошибки Rust тоже попадают в ловушку error kind. Мы парсим где-нибудь int, и пользователю выводится IntErrorKind::InvalidDigit. Я тут выполняю парсинг многомегабайтных CSV, а вы говорите мне, что «в строке найдена недопустимая цифра». Меня это должно устроить? Разумеется, есть крейты вне std наподобие anyhow, которые делают это лучше. Но по моему опыту, значения возврата из-за ошибок только мотивируют к появлению плохих ошибок. Для создания правильного сообщения об ошибке необходимо иметь существенный объём контекста на пути возникновения ошибки, который не поместится в простой код ошибки. Вместо этого вам, вероятно, придётся распределять динамическую ошибку, как и в случае с исключениями. Для отладки может также понадобиться добавление обратной трассировки. И так вы получите исключения, только с лишними шагами!

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


У исключений есть и ещё один туз в рукаве: при успешном выполнении они никак не снижают производительность, ведь мы разделяем поток управления. Значения возврата из-за ошибок перемешивают ошибку и путь успешного выполнения и всегда требуют проверок и ветвлений для обработки ошибок. За это приходится платить снижением производительности для каждого результата. Давайте рассмотрим пример с рекурсивным вычислением чисел Фибоначчи. Чтобы избежать переполнений, мы будем сообщать об ошибке при большой глубине вычислений.

В примерах ниже используется код на C++, что позволяет точнее сравнить две версии:

struct invalid_value { std::string reason; };

unsigned do_fib_throws(unsigned n, unsigned max_depth) {
   if (!max_depth) throw invalid_value(std::to_string(n) + " exceeds max_depth");
   if (n <= 2) return 1;
   return do_fib_throws(n - 2, max_depth - 1) + do_fib_throws(n - 1, max_depth - 1);
}

std::expected<unsigned, invalid_value> do_fib_expected(unsigned n, unsigned max_depth) {
   if (!max_depth) return std::unexpected<invalid_value>(std::to_string(n) + " exceeds max_depth");
   if (n <= 2) return 1;
   auto n2 = do_fib_expected(n - 2, max_depth - 1);
   if (!n2) return std::unexpected(n2.error());
   auto n1 = do_fib_expected(n - 1, max_depth - 1);
   if (!n1) return std::unexpected(n1.error());
   return *n1 + *n2;
}

Версия с expected чуть длиннее, но, по сути, это ведь один и тот же код, правда? Значит, и выполняться он должен одинаково!

Сравнение производительности исключений и значений ошибок.

С исключениями C++ десять тысяч итераций с n=15 выполняются за 7,7 мс. Со значениями возврата std::expected они выполняются за 37 мс — почти пятикратный рост времени исполнения! Можете убедиться сами: Quick Bench. Версия на Rust чуть быстрее, чем версия на C++ с expected, но всё равно в четыре раза медленнее, чем версия, выбрасывающая исключения! Даже если мы пожертвуем ради производительности строкой сообщения об ошибке и будем возвращать в expected только код ошибки, исключения всё равно вдвое быстрее. Да, разумеется, функции обычно бывают чуть больше (если вы не строго следуете стилю чистого кода; тогда вас, вероятно, не заботит производительность). Но в реальном коде я тоже наблюдал существенное замедление.

▍ Почему возвращаемые значения ошибок настолько медленнее?


Очевидная причина, то есть проверки на ошибки и ветвление, ничего не объясняет: следует ожидать, что CPU быстро научится прогнозировать ветвления, то есть, по сути, они будут «бесплатными», ведь так? Это прячет от нас тот факт, что эти проверки втихомолку потребляют обычно невидимые ресурсы CPU: кэш команд, кэш микроопераций, буфер истории ветвлений, буфер переупорядочивания и так далее. Так как выполнение почти любой функции может закончиться сбоем (помните, что распределения и математика могут и будут завершаться сбоем), проверка всевозможных ошибок занимает приличное количество ресурсов, которые нельзя использовать под полезный код. Исключения обрабатываются по совершенно иному пути исполнения кода, обычно выделенному в «холодных» частях, что уже даёт им большое преимущество.

Ещё одно отличие заключается в том, что вместо возврата одного int мы возвращаем «толстый» объект результата. Это делает вызов гораздо более затратным, потому что теперь нам нужно перемещать значения в стеке, что требует дополнительной подготовки. В случае успеха нам также нужно преобразовывать «толстый» результат в реальное значение. Это может помешать компиляторным оптимизациям, в частности, оптимизации хвостовых вызовов. В общем случае, пересылка ошибок в функциональном стиле требует больше регистров и места в стеке, что в свою очередь приводит к снижению степени встраивания кода. Разумеется, можно обойти эти проблемы, распределяя ошибки где-нибудь на странице, например, в локальной памяти потока. И именно так поступают исключения!

▍ Выбрасывание исключений когда-то было ужасно медленным


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

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

Однако раскрутка стека раньше обладала серьёзной проблемой: она синхронизировалась глобально. Чтобы найти местоположения кадра стека в двоичном файле, нам нужно смотреть в глобальную таблицу символов, которая затем используется для поиска информации о раскрутке для текущей функции. Как уже говорилось, эти символы находятся в разделе .eh_frame формата DWARF. Эта информация обычно статична, генерируется во время компиляции и обычно не требует синхронизации. Однако программы также могут загружать код динамически при помощи dlopen, например, для загрузки плагинов или для JIT-компиляции.

Проблема здесь заключается в том, что нам нужна изменяемая структура данных, в которой можно динамически обновлять информацию раскрутки для нового кода. Для защиты такой структуры libgcc просто создаёт мьютекс вокруг этого кода. Даже на машине с сотней ядер ни одно исключение не может быть выброшено одновременно. При высокой concurrency и множестве одновременных клиентов это может стать узким местом. К счастью, теперь в libgcc есть функциональность, позволяющая выполнять весь процесс раскрутки без блокировок. В простых случаях, например, когда не загружаются динамические кадры стека, блокировки использовать не нужно. В случае с динамическими кадрами всё чуть сложнее, но Томас Нойманн закоммитил реализацию B-дерева с оптимистичным связыванием блокировок для таких динамических блоков кода.

Однако даже при всём при этом раскрутка всё равно затратнее, чем должна быть. Для раскрутки стека таблицы eh_frame DWARF, по сути, интерпретируются, то есть здесь можно ещё многое улучшить. Уже существуют исследовательские предложения, например, Reliable and Fast DWARF-Based Stack Unwinding для компиляции таблиц раскрутки в нативный код, что должно быть на порядок величин быстрее, чем современные реализации исключений.

▍ В заключение


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

  • Исключения обеспечивают разделение задач, отделяя ошибочный путь.
  • Результаты при использовании кодов ошибок могут скрывать ошибки системного уровня, например, out-of-memory или переполнения.
  • По умолчанию благодаря исключениям можно легко предоставить контекст первопричины.
  • Код, использующий исключения, может работать быстрее, чем код со встроенными возвратами.

Я имею большой опыт работы с языком C++, позволяющим практически что угодно, поэтому мне непонятно, почему более новые языки наподобие Rust или Go не позволяют применять исключения. Хотя исключения можно использовать неверно, значения ошибок в функциональном стиле определённо не являются универсальным решением. Если вам необходимо определить контекст сообщения об ошибке, вам нужны исключения, так что у вас должна быть возможность их применять.

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (139)


  1. rsashka
    16.09.2024 13:20
    +2

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

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

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


    1. Medeyko
      16.09.2024 13:20
      +2

      Возможно, я что-то не знаю, но я не вижу, какие фичи 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 в два раза медленнее, чем код с исключениями, у меня вызывает сомнения - он ни кода не привёл, ни методологии сравнения. Хотя гипотетически, конечно, случаи, когда исключения критически быстрее, представить можно, на практике это маловероятно.)


      1. rutenis
        16.09.2024 13:20

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

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

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

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


        1. Medeyko
          16.09.2024 13:20
          +3

          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'а "превращались бы в тыкву", мне кажется несостоятельным.


          1. rsashka
            16.09.2024 13:20

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

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


            1. Medeyko
              16.09.2024 13:20
              +1

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

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


              1. rsashka
                16.09.2024 13:20

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

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

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

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

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


                1. Medeyko
                  16.09.2024 13:20
                  +1

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

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

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

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

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

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


                  1. rsashka
                    16.09.2024 13:20

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

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


              1. PrinceKorwin
                16.09.2024 13:20

                Мне кажется вот поэтому:
                https://konnorandrews.gitlab.io/descent-into-madness/post/thought-2-panic-bomb/

                thread panicked while panicking. aborting

                Правда этот баг уже пофикшен :)


                1. Medeyko
                  16.09.2024 13:20
                  +1

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


  1. AndreyDmitriev
    16.09.2024 13:20
    +6

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

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


  1. MountainGoat
    16.09.2024 13:20
    +30

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

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

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

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

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

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


    1. degistration
      16.09.2024 13:20

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

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

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


      1. MountainGoat
        16.09.2024 13:20
        +6

        Но всегда есть нюанс...


    1. sumin_av
      16.09.2024 13:20
      +8

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

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

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


      1. MountainGoat
        16.09.2024 13:20
        +5

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

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


      1. xflower
        16.09.2024 13:20
        +3

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

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


        1. BigBeaver
          16.09.2024 13:20

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


    1. a-tk
      16.09.2024 13:20
      +3

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


    1. bromzh
      16.09.2024 13:20
      +6

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

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

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


    1. xflower
      16.09.2024 13:20
      +4

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

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

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

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


  1. aamonster
    16.09.2024 13:20
    +2

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

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


    1. MountainGoat
      16.09.2024 13:20
      +5

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

      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 в случае исключения. Поэтому огромное количество оптимизаций (даже банальные перестановки независимых строк местами) невозможно в присутствии исключений, это только самый банальный пример.


      1. aamonster
        16.09.2024 13:20
        +9

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

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


        1. MountainGoat
          16.09.2024 13:20
          +1

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


          1. aamonster
            16.09.2024 13:20
            +6

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


            1. MountainGoat
              16.09.2024 13:20

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

              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


              1. redfox0
                16.09.2024 13:20

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

                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})
                    }
                }
                


                1. NN1
                  16.09.2024 13:20
                  +4

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

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


      1. rsashka
        16.09.2024 13:20
        +2

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

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


        1. MountainGoat
          16.09.2024 13:20
          +3

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

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

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

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


          1. rsashka
            16.09.2024 13:20
            +1

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

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


            1. MountainGoat
              16.09.2024 13:20
              +3

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

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

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


              1. rsashka
                16.09.2024 13:20
                +2

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


                1. MountainGoat
                  16.09.2024 13:20
                  +5

                  Объясняю.

                  то вы точно так же не сможете перейти в 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.

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


                  1. rsashka
                    16.09.2024 13:20
                    +3

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

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

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


                    1. MountainGoat
                      16.09.2024 13:20
                      +4

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

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

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

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


                      1. rsashka
                        16.09.2024 13:20

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

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

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

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

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


                      1. MountainGoat
                        16.09.2024 13:20
                        +1

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

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

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


                      1. rsashka
                        16.09.2024 13:20

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

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

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


                      1. MountainGoat
                        16.09.2024 13:20
                        +2

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

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

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

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

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


                  1. yatanai
                    16.09.2024 13:20
                    +1

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

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


                    1. a-tk
                      16.09.2024 13:20
                      +4

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


                    1. NN1
                      16.09.2024 13:20

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


                1. DancingOnWater
                  16.09.2024 13:20

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


      1. xflower
        16.09.2024 13:20

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


      1. mvv-rus
        16.09.2024 13:20
        +2

        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++, а не про недостатки использования исключений вообще.


    1. fshp
      16.09.2024 13:20
      +1

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

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


      1. aamonster
        16.09.2024 13:20
        +2

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

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


    1. ritorichesky_echpochmak
      16.09.2024 13:20

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


      1. a-tk
        16.09.2024 13:20
        +2

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


    1. konsoletyper
      16.09.2024 13:20
      +3

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

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

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

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


      1. aamonster
        16.09.2024 13:20

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

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


    1. RedEyedAnonymous
      16.09.2024 13:20
      +1

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

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


      1. a-tk
        16.09.2024 13:20

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


        1. RedEyedAnonymous
          16.09.2024 13:20
          +2

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


  1. vadimr
    16.09.2024 13:20
    +5

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

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


    1. rsashka
      16.09.2024 13:20

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

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


      1. vadimr
        16.09.2024 13:20
        +1

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

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


        1. rsashka
          16.09.2024 13:20

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

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


          1. vadimr
            16.09.2024 13:20

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

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


            1. rsashka
              16.09.2024 13:20
              +1

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

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

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

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

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


              1. vadimr
                16.09.2024 13:20

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

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

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

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

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

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

                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 в одном потоке


                1. rsashka
                  16.09.2024 13:20

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

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

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

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


                  1. vadimr
                    16.09.2024 13:20

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

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

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


                    1. rsashka
                      16.09.2024 13:20

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

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


                      1. vadimr
                        16.09.2024 13:20

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

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

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

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


                      1. rsashka
                        16.09.2024 13:20

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

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


                      1. vadimr
                        16.09.2024 13:20

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


                      1. rsashka
                        16.09.2024 13:20

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

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

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


                      1. vadimr
                        16.09.2024 13:20

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


                      1. rsashka
                        16.09.2024 13:20

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

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


                      1. vadimr
                        16.09.2024 13:20

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

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

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

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


                      1. rsashka
                        16.09.2024 13:20

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

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

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

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

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

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


                      1. vadimr
                        16.09.2024 13:20

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


                      1. rsashka
                        16.09.2024 13:20

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

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


                      1. vadimr
                        16.09.2024 13:20

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

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


            1. aamonster
              16.09.2024 13:20

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


              1. vadimr
                16.09.2024 13:20

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

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


                1. a-tk
                  16.09.2024 13:20
                  +3

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


                  1. aamonster
                    16.09.2024 13:20

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

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


                    1. mayorovp
                      16.09.2024 13:20
                      +1

                      Из коробки.

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


                      1. aamonster
                        16.09.2024 13:20

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


  1. NeoNN
    16.09.2024 13:20
    +2

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


    1. boldape
      16.09.2024 13:20
      +2

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

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

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

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

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

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

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

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

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

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

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


    1. xflower
      16.09.2024 13:20
      +1

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

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


      1. NeoNN
        16.09.2024 13:20

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


  1. domix32
    16.09.2024 13:20
    +1

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

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

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


  1. ImagineTables
    16.09.2024 13:20
    +1

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

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

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

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

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

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

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

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

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


    1. xflower
      16.09.2024 13:20
      +3

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

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

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


      1. ImagineTables
        16.09.2024 13:20

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

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


        1. xflower
          16.09.2024 13:20
          +1

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

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

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


          1. ImagineTables
            16.09.2024 13:20

            А это не так

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

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

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

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


      1. aamonster
        16.09.2024 13:20

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

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


  1. IsKaropk
    16.09.2024 13:20
    +1

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

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

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


    1. zzzzzzerg
      16.09.2024 13:20
      +2

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


  1. VoodooCat
    16.09.2024 13:20
    +4

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

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

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

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


    1. rsashka
      16.09.2024 13:20

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

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


      1. VoodooCat
        16.09.2024 13:20

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

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

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


  1. beeruser
    16.09.2024 13:20
    +3

    С исключениями 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;
    
    }


    1. nikitakim
      16.09.2024 13:20
      +2

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


      1. santanico
        16.09.2024 13:20
        +1

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

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

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


    1. mayorovp
      16.09.2024 13:20
      +2

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


  1. Dhwtj
    16.09.2024 13:20

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

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

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


  1. a-tk
    16.09.2024 13:20
    +4

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

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

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

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


    1. lrdprdx
      16.09.2024 13:20

      Возврат исключений предполагает

      Опечатка ?


      1. a-tk
        16.09.2024 13:20

        Да, ошибок.


    1. PrinceKorwin
      16.09.2024 13:20

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

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

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

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


      1. a-tk
        16.09.2024 13:20
        +1

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


        1. PrinceKorwin
          16.09.2024 13:20
          +1

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

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

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


  1. PrinceKorwin
    16.09.2024 13:20
    +1

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

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


  1. Vest
    16.09.2024 13:20
    +1

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


  1. 9241304
    16.09.2024 13:20

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

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

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

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


    1. a-tk
      16.09.2024 13:20
      +3

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


      1. 9241304
        16.09.2024 13:20

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


  1. Panzerschrek
    16.09.2024 13:20
    +2

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


  1. DancingOnWater
    16.09.2024 13:20

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

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

    May throw implementation-defined exceptions.

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


    1. vassabi
      16.09.2024 13:20
      +3

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

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


      1. DancingOnWater
        16.09.2024 13:20

        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. Собственно, я им и собираюсь пользоваться. И если пути нет, то и делать ничего не собираюсь. Т.е. это штатная ситуация.

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


        1. vassabi
          16.09.2024 13:20
          +2

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


          1. DancingOnWater
            16.09.2024 13:20

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

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

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