24 September 2012

Адвокат дьявола


Бешенной собаке -- и семь верст не крюк

В Сети есть масса набросов на C++. В этом нет ничего удивительно, ибо плюсы -- язык популярный, язык далеко не самый простой, и язык не лишенный ряда проблем, разной степени серьезности.
Отдельная, и, наверное, самая распространенная категория этих набросов -- набросы с позиций "а си лучше!". Набросы такого рода, все как один, совершенно бездарные, порожденные малограмотными мудаками, разбирающимися в критикуемой ими области не лучше, чем Муртазин в телефонах. Самая известная история из этой серии, это, конечно, выход с присядом Линуса Торвальдса, но сегодня, просто в качестве примера, я хочу разобрать не ее, а кое-что другое.

Картинка НЕ для привлечения внимания. 
На фото -- типичная поклонница плюсов в вакууме озере

Итак, встречайте -- "Why should I have written ZeroMQ in C, not C++", часть первая и часть вторая.

Там довольно много букв, изложу это дело тезисно и кратко.

В начале нас предупреждают -- это не очередной наброс в стиле Линуса! Тут все по научному и серьезно, никакого троллизма!
Афтор пишет, что он задумался о выборе языка для очень серьезного проекта, который должен быть чертовски надежным, работать в режиме 24x7 и ни в коем случае не падать.
А код на плюсах ужасно не надежен, потому что падает, по мнению автора, постоянно. Почему? Да потому, что там используются эти дурацкие и такие не очевидные исключения: вот кинул его тут, и никогда не можешь быть уверен, что кто-то позаботится о перехвате выбрасываемого где-то там. А в си с обработкой ошибок все просто, все на виду. А значит, никакого undefined behaviour, которого автор пытается избежать всеми силами.
А без исключений плюсы использовать категорически невозможно, пишет автор -- ошибку из конструктора никак не вернешь. А значит надо использовать двухэтапную инициализацию, в объектах появляется инвариант, и все это становится сильно хуже обычной сишной структуры с внешним методом init(), который принимает указатель на эту структуру...
Ну и вторая часть -- история о том, как плох std::list потому что он не интрузивный. А не интрузивный он лишь потому, что интрузивность сильно противоречит строгим ООП канонам, которым следует весь код на плюсах, включая STL.
Это -- все.

Что можно ответить на эти бредни?

Первое -- надежное приложение, это не столько история о приложении, написанном красиво на правильном языке программирования, это история о должном подходе к тестированию. Даже очень хорошо написанный код может содержать ошибки, которые случайно попадут в релиз. При этом, плохо написанный код можно, в результате тестирования, довести до состояния, когда в релиз не попадет ни одной критической ошибки.
Это я все к чему -- хорошее тестирование, это когда у тебя код покрыт тестами на 100%, т.е. максимально полно. А максимальное покрытие означает, что у тебя проверяется поведение не только в типичной ситуации, когда все хорошо, но и моделируются все ошибки, нештатные ситуации и т.д., когда код начинает выполняться в ветках, отрабатывающих "аварии". И, кстати, как показывает практика, именно в этих местах чаще всего живут всевозможные баги, терпеливо дожидаясь своего шанса.
Так вот, если ты обеспечил автоматическое тестирование всех закоулков кода, тебе совершенно не страшно, что у тебя вдруг где-то возникнет исключение, которые ты забыл перехватить и корректно обработать. У тебя появляется совершенно другой уровень гарантий.

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

Третье -- про двухэтапную инициализацию.
Да, двухэтапная инициализация это злое зло, и это ни для кого не новость. Да, она порождает в объекте совершенно ненужный инвариант, который все время надо проверять, если вы желаете находить во время исполнения программы в коде совершенно дурацкие ошибки, порожденные лишь тем, что вы банально забыли вызвать init().
Скажу больше: плюсы с исключениями и плюсы без них, это два совершенно разных языка.
И, тем не менее, даже при такой дурацкой схеме "конструктор + init" плюсы все равно имеют преимущество перед сишной структурой + init. Потому что работая с си, ты точно так же можешь забыть вызвать init(), и при этом поля в структуре у тебя вообще находятся в неопределенном состоянии (тот самый undefined behaviour, с которым типа борется афтор). В случае же с плюсами, ты можешь абсолютно надежно проинициализировать все поля в конструкторе (который 100% будет вызван!) и дальше полагаться как минимум на эту информацию, что, в том числе, позволяет детектировать склероз по поводу вызова init(). Не говоря уже о второй, не менее существенной выгоде -- автоматическом вызове деструктора. Автоматическом, что в случае с си, приводит к еще одному поводу нахватать ошибок из-за банального склероза, который, обычно, сопровождается утечками памяти и других ресурсов.

Четвертое -- грамотное использование исключений серьезно повышают надежность кода.
Это только в учебниках у тебя код про обработку ошибок имеет вид типа "if (!do_something()) handle_error()". В реалиях все несколько сложнее. К примеру, я, в свое время, писал код, когда одна удаленная система передает по специальному протоколу другой системе команды на выполнение. Много разных команд. Выполнение типичной команды -- это обычно вход минимум на пять-шесть уровней вложенности по коду. И вот по всему этому пути, в любом его месте, может возникнуть отказ. Можно в каждой точке отказа написать одну строчку -- выкинуть исключение с информацией о проблеме, и перехватить ее в месте, где команда начала исполняться, дабы ответить по протоколу о деталях проблемы. Или другой вариант -- в каждой такой точке написать прорву кода лишь для того, чтобы протолкнуть информацию о проблеме с восьмого уровня вложенности на первый, в точку поступления команды. Одних только разных кодов ошибок в коде, о котором я рассказываю -- больше трех десятков. Представляете, сколько надо написать дополнительного кода, чтобы отказаться от исключений в пользу очевидного сишного стиля?
Ну а дальше работает простая логика -- количество ошибок прямо пропорционально объему кода, а это значит, что код без исключений намного менее надежен...

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

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

Шестое -- интрузивность, якобы ужасно противоречащая объектно-ориентированной сущности C++.
Бред. Автор языка, с фамилией про мертвую птичку, тысячу раз черным по белому писал, что одним из серьезных преимуществ плюсов перед другими языками, является его многопарадигменность. А если у кого-то на почве инкапсуляции данных началась мания преследования, так это к доктору. Пусть поставит клизму.
Отличный пример грамотно реализованных интрузивных контейнеров -- Boost Intrusive.

Итого -- лично я бы не раздумывая отрывал руки и яйца человеку, который несет херню типа "writing the program in C yields shorter and more readable codebase". Это ложь и провокация, порожденная человеком, который не знает толком языка и пытается при этом строить какие-то догадки по его поводу.
Да, у плюсов действительно есть ряд хорошо известных проблем. Например, высокая сложность языка, порождающая сложность обучения оному и нетривиальность создания компиляторов (желательно, еще и быстро работающих). Или проблемы ABI. Дык всё это совсем не означает, что надо выбирать Си, дабы писать более короткий, красивый, быстрый и надежный код.

Любите друг друга.

зы. Кстати, наткнулся на такой вот забавный материал почти по теме.
Питонист пишет, что он не хочет переходить на Go, из-за того, что там используются коды возврата, вместо исключений.
Why I’m not leaving Python for Go


No comments:

Post a Comment