Pull to refresh

Comments 54

Что за кликбейт? Так и пишите в заголовке — "коды ошибок замедлили код на 5%, а исключения только на 1%"

Так в пять раз сильнее замедлили!

А в функции, которые не выбрасывают исключений, был добавлен noexcept?

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

Все-таки, имхо, raii немного не о том
Хотелось бы посмотреть на производительность варианта с одним маалюсеньким изменением:
чтобы в PROPAGATE_ERROR стоял атрибут [[unlikely]].
Вполне возможно, что результаты будут совершенно другие.
www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0323r7.html
Я использую expected, правда самописный с небольшим улучшением void специализации. Он с одной стороны не создаёт такого шума в обработке, как коды ошибок, с другой гораздо строже исключений и не даёт игнорировать ошибки. И что самое главное, решает пользователь — может ничего не делать и получит исключения, либо проверять коды и не иметь исключений.
Измените код таким образом:

#define UNLIKELY(x) __builtin_expect(!!(x), 0)

#define PROPAGATE_ERROR(CODE) { auto problem = CODE; if (UNLIKELY(problem)) return problem; }


См.
GSL assert/ensures/expects

Здесь надо быть осторожным — msvc не имеет встроенных expect функций как gcc и clang.

А разве предсказатель переходов не должен сам "догадаться", какая ветка выполняется чаще?

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

На основании expect/likely/unlikely можно делать не только переходы, но и группировку кода (unlikely дальше, или вообще в отдельной секции), какие ветки лучше оптимизировать при конфликте за ресурс типа регистрового пула, и так далее.

Если предсказатель переходов встретит ветку впервые, то он использует статическое предсказание: backward branches are taken. Другими словами, предсказатель переходов недолюбливает условный переход вперед на старшие адреса, в итоге это медленно. Компилятор знает это и организует циклы таким образом, чтобы тело цикла было likely быстрым, а выход из цикла — unlikely медленным.

Это вы предполагаете или точно знаете?

Предсказатель переходов увеличивает производительность. Производители процессоров поняли это и столкнулись с тем, какое поведение выбрать при статическом предсказании. Каждый выбрал своё. Затем новые процессорные архитектуры стали требовать определенное поведение. И теперь целые числа кодируют лишь дополнительным кодом, теперь лишь 'backward branches are taken'.

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

Повторюсь — это вы предполагаете или точно знаете?

Руководство по оптимизации x86 от Intel (документ номер 248966), пункт 3.4.1.2:


Branches that do not have a history in the BTB (see Section 3.4.1, “Branch Prediction Optimization”) are predicted using a static prediction algorithm:
Predict forward conditional branches to be NOT taken.
Predict backward conditional branches to be taken.
Predict indirect branches to be NOT taken.

Где-то рядом (быстрым поиском не нашёл) ещё сказано, что префиксы подсказок likely/unlikely игнорируются после P4.


По-моему, всё чётко однозначно. Или вы не верите тут даже Intel?

Спасибо!


По-моему, всё чётко однозначно. Или вы не верите тут даже Intel?

Intel я, конечно, верю, хотя тут сказано конкретно о предсказании, когда этот переход выполняется впервые (если я правильно понимаю — do not have a history подразумевает это).


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


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


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


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

хотя тут сказано конкретно о предсказании, когда этот переход выполняется впервые (если я правильно понимаю — do not have a history подразумевает это).

Да.


Соответственно, неясно, как это повлияет на код из поста.

Или компилятору не будет подсказок про ожидаемые вероятности, и он сам что-то предположит.
Или ему подскажут в виде явных likely/unlikely/__builtin_expect/etc., и он поставит переходы соответствующим образом.


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

Почему происходит что?


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


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


  2. Экономия в случае сработавшего предсказателя на ветку с ошибкой — раз ветки нет, то и пробовать её не будет :)


  3. Экономия за счёт того, что не надо кэшировать строки кода с обработкой ошибки.


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



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


Ну на то и обсуждение, чтобы подсказать идею. Может, автор сам это сделает, а может, кто-то ещё...

Почему происходит что?

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


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


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


Ну на то и обсуждение, чтобы подсказать идею. Может, автор сам это сделает, а может, кто-то ещё...

Жаль, что это перевод. Так что вся надежда на кого-то еще.

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


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

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

Для некоторый языков (например Rust) это выглядит как довольно очевидная оптимизация на уровне самого языка. Там есть оператор ? который как раз пробрасывает вверх ошибочное значение и ничего не мешает на уровне реализации этого оператора правильно поставить likely/unlikely в нужную ветку. И даже если он не стоит то думаю предсказатель ветвлений и сам разберется и будет в большинстве случаев правильно определять ветку выполнения кода. Но основная моя идея это как раз то что 1-5% процентов скорости выполнения не стоят значительного усложнения процесса написания и поддержки кода и лучше использовать единый подход везде.

Это значит только, что в Rust оператор? всегда получает указание unlikely — как в случае с выбросом исключений в c++.
А насчёт усложнения…
Комбинированный подход C++ как раз упрощает и облегчает поддержку и написание, так как коды ошибок означают, что обрабатываться ошибка будет рядом с местом её возникновения — т.е. компетентным участком кода — а не будет возникать в качестве неприятного сюрприза для ничего не подозревающего клиентского кода:
если функция OpenFile возвращает код ошибки, да ещё и посредством Enum class — то язык обяжет этот код ошибки обработать (или будет навязчиво об этом напоминать). Если же она выкидывает исключение — то я об этом могу узнать только из документации, которую читают не все и не всегда… При этом если у вас добавится новое исключение — то опять никто об этом не предупредит.
Исключения же остаются на внештатные ситуации, которые невозможно предусмотреть, вроде ошибок выделения памяти, а не для вполне себе штатных проблем.

Даже при 100% правильном угадывании перехода, быстрее будет код, в котором условный переход будет происходить при обнаружении ошибки. Так как при условном переходе нам нужно считать адрес перехода, инструкции к которым мы переходим могут ещё не быть погруженными в кэш и т.д.

А какая модель обработки исключений применялась? Их же вроде несколько — SEH, DWARF, SJLJ…
А какой подход наносит меньший вред читаемости и поддерживаемости кода? Мне кажется этот вопрос зачастую важнее сравнения 1% и 5% производительности.
UFO just landed and posted this here
Есть много областей применения где 2мкс — это целая вечность

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

Важность достижимости скорости исполнения, близкой к аппаратной, сильно переоценена.

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

Разница между overhead'ом в 1% и 5% вообще малосущественна, потому что это разница между скоростью 99% и 95% — под микроскопом не различишь.

С другой стороны, исключения, как способ работать с ошибками, имеет существенные недостатки, именно с точки зрения организации исходного кода. Один из них — разрыв «сюжетной линии» программы между кодом, который что-то делает, и кодом, который разгребает последствия. Получается, что при ошибке программа вылетает неведомо куда, и это место находится по тексту далеко от того места, где возникла ошибка. Другая проблема — исключения с легкостью пролетают между уровнями абстракции, и что, простите, высокоуровневый код, работающий с базой данных, должен делать с исключениями, прилетевшими из низкоуровнего кода, работающего с сетевым стеком, если он ничего про сеть не знает (ну, по крайней мере, не должен знать)? Третья проблема — обработка «внешних» ошибок (файл не открылся, данные испорчены, блок на диск не записался) должна кардинально отличаться от обработки ошибок во внутренней логике программы (это ошибки программиста, а не условий, в которых оказался исполняемый код), систематическое использование исключений помогает с легкостью смешивать эти совершенно разные вещи между собой.

Обработка ошибок — вообще очень сложная тема, и многие программы с ней не справляются даже на уровне «написать осмысленное сообщение в лог и завершиться, по возможности ничего не сломав». Поэтому говорить о механизме обработки ошибок гораздо интереснее с точки зрения, как этот механизм помогает сделать обработку корректной, не переусложная чрезмерно логику программы, а не с точки зрения, как он позволяет ускорить ее исполнение на 4%.

Это тот самый случай, про который сказано, premature optimization is the root of all evil.
Другая проблема — исключения с легкостью пролетают между уровнями абстракции, и что, простите, высокоуровневый код, работающий с базой данных, должен делать с исключениями, прилетевшими из низкоуровнего кода, работающего с сетевым стеком, если он ничего про сеть не знает

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

Если где-то на низком уровне возникла ошибка, которая там же может быть и исправлена — то исключение не летит. Исключение летит только в случае, если низкоуровневый код не может сам справиться «без помощи вышестоящего начальства».

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

Что должен делать высокоуровневый код, которому пришло исключение? Понятно, что он не знает и не может знать о деталях, что именно пошло не так на низком уровне. Что можно сделать на высоком уровне — это:
1) Отказаться от выполнения операции, которая привела к ошибке;
2) Сообщить пользователю об отказе и передать диагностическую информацию;
3) Освободить ресурсы, связанные с ошибочной операцией

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

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

4) Попытаться вызвать операцию ещё сколько-то раз, возможно, через время;
5) Отказаться от выбранной стратегии и попробовать другую;
6) Продолжать выполнять другие необходимые действия, отказавшись от исходного.

Если на каком-то уровне абстракции программа может выполнить что-то из перечисленного в п.4-6 — тогда имеет смысл ловить исключения на этом уровне.
Какая польза верхнему уровню, вплоть до живого человека, знать, что где-то там глубоко внизу случился DNS lookup failure по причине того, что timeout? Что этот самый верхний уровень должен с этой информацией делать?

Верхнему уровню, по большому счету, надо знать:
1) можно ли сделать что-то, в терминах API, который вернул ошибку, чтобы ошибка рассосалась? Ну, например, подождать (сколько?) и попробовать еще раз, предоставить дополнительную информацию (какую?), и т.п.
2) не вызвана ли ошибка неверными параметрами?
3) что сказать человеку, если нужно его вмешательство?
4) что записать в лог для разработчика?
Что этот самый верхний уровень должен с этой информацией делать?

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

Какая польза верхнему уровню, вплоть до живого человека, знать, что где-то там глубоко внизу случился DNS lookup failure по причине того, что timeout? Что этот самый верхний уровень должен с этой информацией делать?

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

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

Программа для того и разделяется на уровни абстракции, чтобы на нижних уровнях можно было выполнять элементарные операции, а на высших — более сложные.
2) не вызвана ли ошибка неверными параметрами?
3) что сказать человеку, если нужно его вмешательство?
4) что записать в лог для разработчика?

Информация для пунктов 2-4 предоставляется верхнему уровню при использовании исключений.

Что из неё писать в лог, а что — на экран, это уже решает верхний уровень.
Угу. И в результате получаем сообщения вида «Ошибка 100500 при исполнении операции XYZKLMN в модуле QWERTYUIOP.DLL».

Предлагаю таких ошибок пользователю не вываливать. Ошибка должна выглядеть, как «проверь сетевое соединение», «кончилось место на диске», «не хватает прав доступа», «сначала надо авторизоваться» и т.п., независимо от того, какой уровень абстракции наткнулся на эту ситуацию.
Хорошо. Но что мешает это реализовать при использовании исключений?
Вообще, это требует инфраструктуры. В меру гибкой, в меру жесткой, чтобы было удобно делать хорошо и неудобно — плохо :-)

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

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

Как маленькая часть этой инфраструктуры, в языке должно быть запрещено передавать немодифицированную ошибку вверх по стеку, без явного указания, что именно это и имеется ввиду.
Исключение летит только в случае, если низкоуровневый код не может сам справиться «без помощи вышестоящего начальства».
Ах, если бы это всегда было так!
В реалии же бывает так, что исключения о «невозможности загрузить значок», выкинутые драйвером Windows, пролетают мимо них, а также мимо вызвавших эту функцию создателей QT — и валят программу ничего не подозревающего разработчика, удосужившегося вызвать банальнейший диалог OpenFile на «неправильной» папке в Viste.
Причём если б там использовались коды ошибок — то до такого никогда бы не дошло, а максимум показывалась бы иконка по умолчанию…
Ну, это вы привели пример неправильной разработки с использованием исключений. Глупо винить молоток в отбитых пальцах, если пользоваться им неправильно.

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

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

Коды ошибок же видны сразу и всегда, и если не использовать result то компилятор будет ругаться warning'ами — т.е. программы с кодами ошибок всегда будут обрабатывать как минимум два варианта — «удача» и «неудача».
Т.е. цена ошибки программиста «не учёл такую ситуацию» — это одинаковая ветка для всех вариантов «облома» вместо специализированной обработки именно данного конкретного кода ошибки.

Исключения — это goto по неизвестному адресу, а коды ошибок — это switch, сваливающийся в default.
Т.е. цена ошибки программиста «не учёл такую ситуацию» — это одинаковая ветка для всех вариантов «облома» вместо специализированной обработки именно данного конкретного кода ошибки.

Но ведь в случае исключений имеем то же самое. Поведение по умолчанию — это передача исключения наверх (т.е. «одинаковая ветка для всех вариантов облома»).
Исключения — это goto по неизвестному адресу, а коды ошибок — это switch, сваливающийся в default

Исключения — это не goto. Это как если в вашем switch в качестве default прописано «return error_code;», и так же в вызывающей функции. Но только этот код не надо писать вручную, и не надо составлять switch после каждого вызова какой-нибудь функции.
Но ведь в случае исключений имеем то же самое. Поведение по умолчанию — это передача исключения наверх
Совершенно не то же самое. Передача исключения наверх — это бросание гранаты в толпу в надежде, что там найдётся специалист, смогущий её поймать и разобрать, причём все правильные специалисты умеют обезвреживать только свои любимые, уникальные типы гранат; наличие же подходящего специалиста никто не гарантирует.

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

Исключения — это не goto. Это как если в вашем switch в качестве default прописано «return error_code;», и так же в вызывающей функции.
Исключение аварийно завершает функцию, если не поймано обработчиком, оно не требует от функции корректно обрабатывать эту ситуацию, более того — оно скрывает свою способность возникнуть из ниоткуда.

Return error code же возвращает управление точно в точку вызова функции, причём эта функция всегда точно знает как поступать, когда возвращено не OK.
Они требуют, чтобы функция всегда корректно обрабатывала это.
Совершенно не то же самое. Передача исключения наверх — это бросание гранаты в толпу…
В случае же с кодами ты лично передаёшь её специалисту-подрывнику

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

А откуда уверенность, что тот, кому вернули код ошибки — «специалист-подрывник»? И что он сделает с этим кодом, если он не специалист? Правильно, вернёт ещё выше, т.е. сделает точно то же, что делают исключения.

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

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

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

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

В случае возврата кода ошибок программист обычно ленится проверять код возврата и освобождать ресурсы при возврате на уровень выше. Последствия: программа пытается работать «сквозь ошибки», в приложении отсутствует половина элементов управления, в файлы и сетевые соединения записывается неверная информация. Утечка ресурсов. Диагностика отсутствует.

В случае исключений типичная ошибка программирования — отсутствие обработчиков исключений. Последствия — закрытие программы с диагностикой.

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

Простейшая стратегия использования исключений позволяет иметь программы, в 99% случаев ведущих себя корректно в случае ошибок. А именно — встроиться в иерархию std::exception. Наличие одного catch(std::exception& e) в функции main() гарантирует, что:
1) ни одна ошибка вызовов API не будет пропущена;
2) все исключения C++ будут пойманы;
3) операция, вызвавшая ошибку, будет отменена, а ресурсы — освобождены;
4) Какая-никакая диагностика через e.what(), а не «ошибка -154».

Заметьте: затраты программиста на реализацию этой стратегии являются малыми, постоянными, и не зависят от размеров программы.
То есть в направлении наличия возможных специалистов. А возврат кода ошибки, простите, в каком направлении производится? Не в том же ли?
Отличие в том, что при использовании кода ошибок *все* использующие функции обязаны быть специалистами, в то время как при использовании исключений специалистами могут быть только те, для кого это специально назначили.
И что он сделает с этим кодом, если он не специалист?
А должен он в обоих случаях иметь 100500 стратегий на обработку каждого вида ошибок
Код ошибки говорит две вещи: что функция не выполнилась, и причину этого невыполнения. Любой обработчик всегда должен быть специалистом в обработке только части «функция не выполнилась» — и этого в подавляющем большинстве случаев достаточно, чтобы результат ошибки не привёл к критическим последствиям вроде падения программы.
100500 стратегий нужно только исключениям, потому что они требуют сразу и того, и другого, и если этого нет — то посылаются наверх, где вероятность встретить специалиста уменьшается практически до нуля, а последствия ошибки раздуваются до критических.

Однако в случае исключений упрощается диагностика и наладка.
И что мне с того, что я узнал что в драйверах Windows есть ошибка? Как это поможет мне исправить падение моей программы без штудирования и копирования в неё массивных кусков QT (со всем вытекающим техническим долгом) и написания моей версии этой неправильной функции?
Неправильное же отображение значка я и пользователи моей программы как-нибудь переживут.
Простейшая стратегия использования исключений позволяет иметь программы, в 99% случаев ведущих себя корректно в случае ошибок. А именно — встроиться в иерархию std::exception. Наличие одного catch(std::exception& e) в функции main() гарантирует, что:
1) ни одна ошибка вызовов API не будет пропущена;
2) все исключения C++ будут пойманы;
3) операция, вызвавшая ошибку, будет отменена, а ресурсы — освобождены;
4) Какая-никакая диагностика через e.what(), а не «ошибка -154».
Забыли главное: 5) что программа аварийно завершиться, т.е. не будет работать даже если исключение было в совершенно неважной части кода. Что абсолютный ужас и самое неоправданное поведение для любой сколько-нибудь серъёзной программы.
Отличие в том, что при использовании кода ошибок все использующие функции обязаны быть специалистами

Это в теории. На практике же "специалист" почти всегда превращается в


и отличие от исключений только в том, что это наплевательство из тайного стало явным. Может, в этом и есть плюс ;)


image


5) что программа аварийно завершиться

При catch в main()? Конечно, нет.


т.е. не будет работать даже если исключение было в совершенно неважной части кода

Откуда вывод про "неважную часть"? Неважные как раз обычно прикрывают.


Что абсолютный ужас и самое неоправданное поведение для любой сколько-нибудь серъёзной программы.

И это лучше, чем молчаливая маскировка ошибки.

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

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

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

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


Я как-нибудь напишу опровержение, когда будет время.

Хм, но ведь "исключения возбуждаются постоянно" – это классический случай неправильного использования исключений. По мне, упомянутый в статье критерий "исключений должно быть столько, чтобы можно было запускать программу под отладчиком с остановкой на них" хорош и правилен.
(Как следствие, от Error codes, а лучше чего-то типа Either или хотя бы Maybe, не уйти).

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

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


Статья рассматривает исключения vs коды ошибок слишком однобоко.

Во-первых, чтобы выбросить изначальное исключение, кто-то где-то в глубине все равно выполнит этот if и проверит ошибку. Во-вторых, по части исключений — есть вопросы по такому быстрому unwind'у. Если есть типизация и необходим выбор конкретного обработчика — не замедлится ли раскручивание? Zero-cost, насколько я понимаю — оно таково только на уровне ОС, самого механизма исключения?

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

Sign up to leave a comment.