среда, 16 мая 2018 г.

[prog.c++.flame] Неоднозначные впечатления от нового предложения Саттера "Zero-overhead deterministic exceptions: Throwing values"

На Reddit-е недавно дали ссылку на документ "Zero-overhead deterministic exceptions: Throwing values" за авторством Герба Саттера. Когда выдалось время пробежался по содержанию. Затем пробежался еще раз... Как-то все это неоднозначно.

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

С другой стороны, есть ряд моментов, которые сильно смущают.

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

Во-вторых, смущает какие-то отсылки к предложению по добавлению контрактов в C++ (которого я толком не видел) и соображения следующего рода: "A precondition (e.g., [[expects...]]) violation is always a bug in the caller (it shouldn’t make the call). Corollary: std::logic_error and its derivatives should never be thrown (§4.2); use contracts instead."

У меня был опыт использования контрактов в Eiffel и D. Контракты -- это красивая штука, но с ней есть вот какая заковыка: полная проверка контрактов (т.е. и пред-, и постусловий, и инвариантов) -- это очень дорого. В том же Eiffel при компиляции проекта с полностью включенными контрактами (в том числе и в stdlib) результирующий exe-шник раздувался в разы и, соответственно, в разы падала производительность. Да, при этом ты перестаешь боятся того, что в программе что-то пойдет не так, но потеря скорости работы, да еще столь существенное... Поэтому в Eiffel-е, ЕМНИП, была рекомендация включать полную поддержку контрактов только в debug-билдах, а в release-билдах оставлять только проверку предусловий. А если вы хотите выжать из кода максимум, то и проверку предусловий приходится выключать.

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

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

Ну и в-третьих, очень мне не понравились пространные, но акцентированные рассуждения о том, что нужно пересмотреть поведение C++ного рантайма при исчерпании памяти. Мол, вместо выбрасывания bad_alloc-а нужно грохать все приложение. А кому такое поведение не нравится, тот пусть использует специальные варианты new(nothrow) или новые функции try_reserve с ручными проверками возвращаемого значения.

Вот это, как по мне, так уже полный трындец, дорогие товарищи. Ну нельзя же так :(

Я понимаю, что Саттер ссылается на большой опыт и приводит примеры того, что сделано в Microsoft и как в продуктах Microsoft подобные подходы давно используются. Но ведь эти примеры -- это же не весь мир. И далеко не все приложения, которые были и, что важно, еще будут на C++ написаны. Поэтому неприятно, когда такие судьбоносные решения принимаются на основании ограниченной выборки.

Причем отдельно непонятен такой аргумент как:

Most code that thinks it handles heap exhaustion is probably incorrect (see also previous point; there is some truth to the mantra “if you haven’t tested it, it doesn’t work”). Recovery from heap exhaustion is strictly more difficult than from other kinds of errors, because code that is correctly resilient to heap exhaustion can only use a re-stricted set of functions and language features. In particular, recovery code typically cannot ask for more memory, and so must avoid calling functions that could allocate memory, including functions that could fail by throwing another of today’s dynamic exceptions which would require increased allocation.

Если обработка исчерпания памяти реализована неправильно, то с вероятностью, близкой к 100%, программа все равно упадет. Хотя бы из-за того, что будут возникать повторные bad_alloc-и, в том числе и из деструкторов объектов, вызванных при раскрутке стека из-за первого bad_alloc-а. Так что неправильно написанное приложение все равно завершит свою работу преждевременно. Зачем же при этом мешать тем приложениям, которые смогут пережить исчерпание памяти?

В общем, раздел 4.3 в данном предложении расстроил меня очень сильно. Если бы его не было, я бы воспринял это предложение Саттера с гораздо большим воодушевлением.

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

Комментариев нет: