Lex Kravetski (lex_kravetski) wrote,
Lex Kravetski
lex_kravetski

Category:

Священная война

 

Про Haskell мы с товарищами уже несколько раз реально перетёрли. И про удобства, и про сравнение с объектными языками. Сейчас назрела внутренняя необходимость (то бишь, злость взяла) перетереть на баянистую тему: C++ vs Java.

Для справки: на C++ я программировал лет десять. После этого я благополучно децл сменил область деятельности и перешёл на Java. Из этого всем должно быть очевидно, что оба языка я хорошо знаю и понимаю (кому не очевидно, тот сам виноват). Однако же пару дней назад пришлось-таки снова написать всякого на C++. Что вызвало нехилое раздражение.

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

И так во всём. Чуть где нужно что-то очевидно стандартное – его нет. Зато есть что-то очень-очень гибкое, с помощью чего это стандартное следует собирать. Причём, началось это очень давно. В тот самый момент, когда разработчики С решили вдруг, что строки вполне можно встроить в виде char*. Да чего там – в тот момент, когда они поняли, что с помощью массивов вообще всё что угодно можно сгенерить. Они были правы, но их правота вылилась в мега-кучи ошибок с битой памятью. По опыту, половина ошибок при написании чего-то на C проистекает из-за бития памяти с помощью char*.

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

По счастью, при разработке С++ часть этих недочётов была учтена и в STL всё-таки сделали и вменяемые строки и вменяемые контейнеры. Но С, друзья мои, не умер. Он живёт в обрывках кода, которые до сих пор приходится использовать. Так, например, в WinAPI не стали использовать STL, вместо этого ввели свои типы и очень весёлую форму записи. С указателями, а иногда даже с двойными указателями. Если надо получить набор значений, то изволь заводить буфер под них и сообщать его размер соответствующей функции. Так мега-экономия памяти обернулась её разбазариванием в буферах.

Стоит ли говорить, что для превращения std::string в спецтип WinAPI и обратно, надо писать дополнительный код. Ну а если нужен не WinAPI, а MFC, то там тоже свои классы и тоже нужна конверсия. Казалось бы мелочь – не сделали встроенный тип «строка», однако вон оно как вышло.

Кстати, строки иногда надо превращать в числа, а числа – в строки. Что в этих случаях делается в Java? Да ничего практически. Число в строку превращается само (или явным вызовом встроенного toString()), а обратно парсится встроенными же Integer.parseInt и им подобными. Операция-то очень распространённая, отчего бы её не встроить в С++? Говорят, нельзя. Говорят, каждый программист сам для себя напишет. Вот и пишут.

Самое интересное: кто сходу скажет, что надо написать, чтобы вывести на экран double с той точностью, которая в данном случае есть у числа? Ну, то есть, если double x = 123123.5657356252, то на экран вывелось бы 123123.5657356252?

cout << x; ?

printf("%f", x); ?

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

Зато в printf можно очень гибко задавать формат вывода. Любой формат. Кроме самого простого и очевидного. Точнее, его тоже можно задать, но выглядит это так, что никто не догадается. В общем, чем так согрешила функция «перевести в строку»? Неужели же своей простотой? Я не знаю. Знаю только, что в Java нет никаких проблем с переводом чисел во все стороны. И, кстати, printf там ровно такой же, как в С++. Но им пользуются редко.

Ах да, есть же функции парсинга в С! Как же они называются? fromString? Нет. StringToDouble? Для С++ слишком очевидные названия. Слишком длинные. Наверно str_to_dbl? Тоже нет. Функция называется atof. Такое вот простое и очевидное название. Любой сразу догадается, что функция должна называться именно так. Что интересно, если конверсия не удаётся (вместо «1.2» в строке содержится, например, «Fuck off»), то что будет? Выкинется исключение? Но, увы, это – функция из С, а в С нет исключений. Поэтому функция просто и незамысловато вернёт ноль. Что делает проверку валидности значений весьма нетривиальным занятием – по действиям функции ведь совершенно непонятно, действительно ли ноль был в строке или какое-то левое значение. И как, блин, проверить? Сравнить строку с "0"? А если там было "0.0" или "000.00000"? Фигня получается.

Справедливости ради, надо отметить, что есть функция с более очевидным названием strtod. Но ни та, ни другая не работают с std::string, то есть, опять же нужна конверсия через c_str().

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

Проблема в том, что в С++ отсутствует последовательность. Он сделан на основе С, обладает с ним почти полной совместимостью, отсутствующее в С прикручено к нему изолентой, и, – что самое плохое, – он исповедует свободу во вред – не задаёт стандартов. Отсутствие стандартного подхода ко всему, полагание на «программист сам додумается» приводит к разнобою даже внутри самого языка, не говоря уже про сторонние библиотеки. С++ не задаёт даже стандартных способов именования. Поэтому в разных программах встречаются совершенно разные способы. У кого-то венгерская нотация, у кого-то всё идёт через подчёркивание, у кого-то вообще никакой. Свобода, блин. Программиста не притесняют. Но такая свобода хороша, когда программист находится в вакууме и всё его общение с внешним миром сводится к вбрасыванию туда экзешников. Увы, обычно это не так. Программист обычно пользуется чужими библиотеками и чужим же кодом. В результате Java-код и Java-библиотеки в 99% случаев имеют один и тот же стиль именования и даже фигурные скобки там проставлены одинаково. Чужой Java-код не портит своего. А вот в случае С++ всё сразу идёт вразнос. В одной библиотеке функции называют в виде MyFunc, в другой – My_Super_Func, в третей – mymegafunc, ваш коллега любит венгерскую нотацию, а вы сами совершенно справедливо уважаете нэйминг из Java. При этом одни требуют указатели, другие – ссылки, а третьи – ссылки на указатели. В вашем же коде всё это превращается в

 

int myValue = mymegafunc(**My_Super_Func(&MyFunc(*m_piValue));

 

Смотришь на всё это и думаешь «да где тут что вообще?!!»

Вот такая она – свобода. Людям её только дай... Стандарты, блин, руля́т.

Вообще в С++ крайне тяжело назвать то, что там сделано лучше, чем в Java. До версии Java 1.4 в С++ лучше было наличие шаблонов. Однако в Java 1.5 появились дженерики, которые шаблоны даже превосходят. Последний плюс уверенно ушёл в прошлое. Ах да, в С++ ещё можно объявить указатель на функцию. Что временами удобно, но имеет такой синтаксис, что за ним надо каждый раз лезть в справочник. Не верите? Ну напишите тогда не подглядывая указатель на константную функцию класса A с параметром int* и её вызов у указателя на объект класса. Я лично такие вещи писал раз триста, но так и не запомнил.

Кстати, в С++ есть и ещё одна полезная штука – возможность передать константный объект и объявить константную функцию. Вот этого в Java пока не сделали. Жаль. Итого один плюс против разливанных морей минусов.

Что меня реально вспарило при временном возвращении к С++, так это долбанные хэдеры. Тут я реально не понимаю, нафига их вообще ввели. Может, предполагалось, что сорсы к хэдерам можно будет подменять? Но, мой бог, зачем? Никто же этим никогда не пользуется. С чего бы тогда код не писать прямо там, где объявляются функции? Загадка, блин, природы. Точнее, загадка человеческой мысли.

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

Хэдеры же, кроме необходимости объявлять в одном месте, а писать в другом, влекут за собой ещё одну болезнь С++ – кучи мусора. Выражающиеся в проверках того, что хэдер подключен ровно один раз. Ну да, те самые #ifndef и так далее. Стандартные шаманские строки, которые всех так сильно задолбали. Которые содержатся в каждом хэдере, поскольку далеко не все знают, что можно написать не менее шаманское #pragma once. Но даже с прагмой вопрос остаётся открытым: а какого хрена это одноразовое включение не встроено сразу в язык? Чего бы такого мы потеряли, если бы хэдер включался ровно один раз без нашего вмешательства? Ответа нет. Поскольку ничего бы мы не потеряли. Просто в С++ модно писать мусорный код. В С++ – это элемент стиля. Который ничего не даёт, но очень круто выглядит.

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

Ещё неприятно поразила необходимость писать инклюды вручную. Это, конечно, особенность не языка, а оболочки, но ведь неспроста наверно в оболочках Java импорты включаются автоматически, а в оболочках С++ их надо вписывать руками, хотя и с подсказками компьютера. Печаль сего не в том, что тяжело эту строчку набрать, а в том, что тяжело вспомнить, в какой именно библиотеке лежит нужная функция. В результате приходится постоянно лезть в help.

Кстати, в Java проблема хэлпа тоже была решена универсальным способом – хэлп содержится ровно там, где код. То есть, комментарии к коду являются и хелпом к нему. Некоторые оболочки С++ тоже показывают комментарии во всплывающих подсказках, но без стандарта это – полумера. Никто ведь не знает, в какой форме будет написан комментарий тем или иным программистом. Да и ключевых слов для комментариев не предусмотрено.

В Java разработчики уверенно забороли практически все болезни вдохновлявшего их языка. Во-первых, отказались от статичности и сделали язык дорабатываемым. Во-вторых, как уже было сказано, документацию скрестили с кодом и слили объявления с реализациями. В-третьих, убрали долбанные указатели, заменив их ссылками (ссылкам разрешили принимать нулевое значение – в С++ отсутствие этого очень сильно напрягает). В-четвёртых, – хотя тут спорно, – встроили сборщик мусора. С ним немного медленнее, зато 90% проблем и ошибок исчезли сами собой. В-пятых, ввели стандарты именования. Пусть негласно, но ввели. В-шестых, встроили объект «строка» в язык и научили все объекты преобразовываться в него. В-седьмых, трэды опять же встроили в язык и встроили в него же все механизмы работы с трэдами. Всё перечисленное офигенно удобно. Хотя при слове Java у большинства людей всплывает в мыслях исключительно сборщик мусора, это далеко не единственное преимущество.

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

В Java встроен GUI. Причём, не только в виде одной из его возможных реализаций, но и в виде стандартных интерфейсов, которые поддерживают все остальные реализации. GUI не опирается на ресурсные файлы и всё управление им содержится в коде программы. Это – мега-плюс. А второй мега-плюс – GUI в большинстве случаев сам занимается расстановкой и выравниванием элементов. Всё это утомительное жонглирование координатами, которым страдают Builder и MFC, в Java неактуально.

Встроенные библиотеки реализуют практически все стандартные задачи. Причём, таким образом, что гибкость не теряется. То есть, в наличии и максимально гибкие объекты для достижения чего угодно и обёртки над ними для решения стандартных и наиболее часто встречающихся задач. Когда я говорю «практически все», я имею в виду «практически все». Был поражён, например, когда обнаружил в библиотеках работу с midi.

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

Компилятор в Java не даёт забыть про обработку исключений – это тоже большой плюс, хотя с непривычки слегка раздражает. В С++ некоторые функции в редких случаях неожиданно бросали исключение, а программист про это даже не знал. Да, там тоже можно было приписать к функции throws, но можно было и не приписывать, поэтому приписывали далеко не все. В результате программа иногда вылетала в труднообнаружимых местах.

В Java встроена сериализация объектов. Встроены регулярные выражения. Понятно, что всё это можно найти и для С++, но тут даже искать не надо. Более того, контейнеры уже сериализумы – на уровне языка, без дополнительных ухищрений. Во встроенных строках уже все функции работают с регулярными выражениями. Удобство всего этого тем, кто пишет на С++, неочевидно. Зато при возвращении с Java на С++ без этого как без рук. Там, где в Java всё решалось одной строкой, в С++ надо городить нехилый огород или искать в интернете готовые решения. Подчеркну, решения вовсе не узкоспециальных случаев, а стандартных и очень распространённых задач. Давно уже решённых задач, что характерно.

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

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

В общем, чтобы я ещё раз сел на этот велосипед, чтобы хоть ещё раз...

Tags: программирование
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 605 comments
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →