понедельник, 19 декабря 2016 г.

[prog.c++] Разбор asfikon's "C vs C++". Часть 5.

Завершаем ранее начатую тему. Сегодня разберем оставшиеся цитаты, благо их немного. А в завершении я перечислю свой перечень условий, при которых в современных реалиях выбор языка C предпочтительнее выбора C++.

Приступим.

Феерический фрагмент, который сложно прокомментировать:

ООП. Кажется, сегодня уже вся индустрия осознала, что объединение кода и данных — идея так себе, не говоря уже про повсеместное использование наследования. В теории это, конечно, здорово, когда есть класс животное и от него наследуется кошка и лошадь. Но на практике реальная необходимость (ООП головного мозга — тоже тяжелый недуг!) строить такие иерархии возникает очень редко, и если возникает, то дело обычно ограничивается одним интерфейсом и многими классами, реализующими этот интерфейс. Последнее, если что, очень просто делается на С. И возникает вопрос, а какой вообще смысл использовать язык, одна из главных фишек которого — возможность легко писать код так, как это делать не надо?

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

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

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

Идем дальше:

STL часто преподносится так, словно в мире С нет библиотек с готовыми алгоритмами и контейнерами, что, разумеется, не так. При этом STL предлагает только одну из многих возможных реализаций (даже для простого vector их можно придумать десятки) конкретного алгоритма или контейнера. Почему кто-то за меня решил, что замедление скорости компиляции и разбухание секции кода лучше, чем, например, хранение всего по ссылке и, соответственно, быстрая компиляция и разбухание кучи? Или, например, хранение всего по значению, но с небольшим замедлением скорости выполнения кода? Следует также отметить, что контейнер, написанный с нуля и заточенный под данный конкретный случай, позволит вам повысить производительность на те самые «жалкие 0.5%», которые так важны в задачах, на решение которых претендует С++. Например, если вы знаете что-то о природе данных, которые хранятся в контейнере, то можете опустить некоторые проверки. Или, возможно, вам известно, что данные удаляются из хэш-таблицы только в порядке обратном тому, в котором они были добавлены — это тоже можно использовать.

Просто ярчайший пример если не маразма, то старательного наброса на вентилятор.

Во-первых, где это "STL часто преподносится так, словно в мире С нет библиотек с готовыми алгоритмами и контейнерами"? Мне вот, например, подобных утверждений слышать не приходилось. Но мне повезло, мне доводилось работать с хорошими программистами. Ну и читать материалы, написанные вменяемыми и грамотными разработчиками. Поэтому с такой точкой зрения не сталкивался. Может быть, речь о том, что в C++ благодаря STL контейнеры и алгоритмы доступны программисту "из коробки", а в C нужно подтягивать сторонние библиотеки? Ну так оно так и есть.

Во-вторых, по поводу "Почему кто-то за меня решил, что замедление скорости компиляции и разбухание секции кода лучше, чем, например, хранение всего по ссылке и, соответственно, быстрая компиляция и разбухание кучи?" и других подобных стенаний хочется сказать: "За вас никто ничего не решал". Вам просто дали набор готовых инструментов. Очень качественно сделанных, между прочим. Хотите -- используйте. Не хотите -- не используйте. Задача стандартной библиотеки C++ вовсе не в том, чтобы там были готовые алгоритмы и контейнеры на все случаи жизни. Об этом подробнее речь шла в третьей части разбора.

Так что, если для очень специфических задач штатные средства STL не подходят, то что в C++ мешает сделать кастомное, заточенное под именно эту задачу решение? Ну кроме откровенного нежелания программировать на C++?

Идем дальше:

Из-за повсеместного использования шаблонов скорость компиляции кода на С++ просто ни на что не годится. Не говоря уже о том, что при компиляции крупных проектов нередко может потребоваться, скажем, гигов 10 оперативной памяти. Для сравнения, язык C позволяет мне компилировать по много раз на дню миллионы строк кода, используя только Raspberry Pi. Ну и если я вдруг решу, что в моем проекте имеет смысл использовать кодогенерацию, ничто не мешает ее использовать. Притом, с нормальным кэшированием результата. Понятное дело, так как шаблоны объявляются в .hpp файлах, их изменение приводит к перекомпиляции половины проекта. См также Десять причин избегать метапрограммирования.

Здесь претензия по делу. Скорость компиляции в C++ -- это больная тема. Однако, наибольший вклад в эту проблему вносят как раз таки не шаблоны, а совместимость с C. В частности, значительное время уходят на обработку заголовочных файлов. Просто в C++ стандартные заголовочные файлы гораздо объемнее, чем в C, и взаимосвязи между ними теснее. Поэтому, скажем, #include <iostream> обходится гораздо дороже, чем #include <stdio>.

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

Но вообще, да. Скорость компиляции в C++ -- это проблема. И еще долго будет проблемой.

Идем дальше:

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

Тут просто не хочется повторять заново все то, что уже было мной (и не только мной) сказано про исключения в C++. Для желающих просто оставлю несколько ссылок на тексты, в которых эта тема мной разбиралась, как мне кажется, очень подробно: #1, #2, #3.

Ну а по поводу "исключений не предусмотрено" касательно Go и Rust, автора разбираемых мифов можно в очередной раз поздравить соврамши: там есть паники. Которые как раз предусмотрены для случаев, когда информирование об ошибках посредством кодов возврата не работает. И из-за которых в этих языках используются свои аналоги RAII-инструментов. В частности, в Go -- это инструкция defer, а в Rust-е -- автоматический вызов drop-а.

Следующая цитата:

По своему опыту могу сказать, что отлаживать код C++ — мягко говоря, удовольствие ниже среднего. Продраться через тонны смартпоинтеров и виртуальных методов, или, например, посмотреть, что же происходит внутри STL, в gdb подчас сложно настолько, что проще прибегнуть к обычному отладочному выводу. В языке C все просто и понятно. Даже весьма непростые баги можно легко поймать за пару минут. Я вам даже больше скажу, код на C можно довольно комфортно отлаживать вообще без отладочных символов. Когда-то очень давно я так и делал, просто брал OllyDbg и дебажил. Попробуйте, это правда не сложно.

Тут я не в теме. Отладчиками пользуюсь крайне редко и эпизодически (почему так в подробностях описано здесь). Каких-либо проблем при этом не испытываю.

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

Идем дальше:

Код на C прекрасно пишется без каких-либо тяжеловесных IDE, в обычном Sublime Text или Vim с ctags. Для сколь-либо серьезного кода на C++ без нормальной IDE жизнь быстро становится очень грустной, потому что автоматический вывод типов, шаблоны, и вот это все, и потому что ctags начинает забрасывать не туда, куда нужно. К счастью, IDE для C++ существуют. CLion, например, довольно неплох. Но не все программисты согласны платить за него деньги и попрощаться с 2 Гб оперативной памяти. К тому же, CLion не все и не всегда подсвечивает правильно, и если открыть в нем сразу два проекта, то даже довольно мощный компьютер начнет тормозить. Есть и другие IDE, но у них свои проблемы, например, привязка к Windows или отсутствие важных возможностей, таких, как вывод типов.

Мне сложно это комментировать, т.к. в последний раз доводилось писать C++ный код в IDE где-то в 2001-ом году. И то, только потому, что нужно было делать проект на MFC, а колупаться с MFC вручную, без помощи Visual Studio, было практически невозможно (в отличии от проектов на Qt или FOX Toolkit, которые можно было разрабатывать чуть ли не в блокноте).

Я сам уже 12 лет работаю с C++ в Vim-е. Без ctags или каких-то дополнительных плагинов.

По свидетельствам очевидцев, IDE для C++ сильно отстают от таковых для Java или C#. Правда, в последние годы, с развитием проекта clang, ситуация вроде как стала улучшаться. Но т.к. я не в теме, то сложно судить, насколько данная претензия к C++ оправдана. И насколько дело лучше для чистого C.

Следующая цитата:

Нельзя упускать из виду и кадровый вопрос. Язык С сравнительно прост. По крайней мере, его реально уместить целиком в голову среднего программиста. Стандарт С11 [PDF] занимает 700 страниц со всеми приложениями и предметным указателем, а полноценный компилятор C умещается в 15-20 тысяч строк кода. Многие (не все, но многие) студенты уже на первом курсе в состоянии писать вполне сносный боевой код на C. Язык C++ в десятки раз сложнее C. Не удивительно, что его толком не знает никто. В лучшем случае, есть люди, которые знают небольшую его часть. Что намного хуже, с выходом каждого нового стандарта С++ становится еще более сложным и запутанным. Туда тянут еще какие-то концепты, корутины и прочие модные игрушки, как будто без них язык не был уже достаточно распухшим. Но хуже всего то, что правила языка часто далеко не очевидны (например) и имеют кучу исключений. Чтобы писать что-то серьезное на языке, про который неизвестно точно, как работают его компоненты и как они друг с другом взаимодействуют, нужно быть либо очень смелым, либо очень глупым.

Кадровый вопрос -- больной. Впрочем, для C ситуация вряд ли лучше. Ибо C/C++ очень четко делят программистов на две категории: на тех, кто понимает указатели и адресную арифметику, и на всех остальных. Первая категория в последние 15 лет постоянно уменьшается. Поэтому, если вам нужен программист, который сможет писать на C с указателями, то вы не сможете найти его просто так. А если нашли такового, то для него и C++ не будет проблемой. Если же разработчик способен писать не падающий код только на Java или на Python, то его нельзя пускать ни в С++, ни в C.

Язык C++, действительно, в разы сложнее C. Что является платой за мощность. Хотите тратить меньше усилий на решение своих прикладных задач -- придется заплатить за обучение. Впрочем, можно и по другому: можно героически бороться со сложностью задачи посредством низкоуровневого C и мегатон копипасты (что мы и видим в примерах кода ув.тов.asfikon-а). Ну если работодатель платит за C и не подозревает, что можно быстрее и качественнее, то все нормально. Это же проблемы работодателя, а не разработчика.

Кстати говоря, если студентам на первом курсе давать не C, а Pascal, то процент тех, кто будет в состоянии писать вполне сносный боевой код на Pascal, вообще будет приближаться к 100%. Но ведь мало кому приходит в голову вести разработку на Pascal в современных условиях, не так ли?

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

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


Ну а теперь мой список условий, при которых сейчас можно выбрать C, а не C++:

  1. Вам просто не нравится C++ и вы просто не хотите иметь с ним дело. Ну бывает, что тут скажешь. Мне, например, категорически не нравится Java. И Python-у я предпочитаю Ruby. Соответственно, я не использовать эти языки если у меня есть выбор. Так что, если вы не любите C++, то не используйте его. Но отдайте себе объективный отчет: вы не используете C++ именно из-за субъективных причин, а не потому, что у него стандарт 1500 страниц.
  2. У вас нет возможности/времени на изучение C++, а C вы уже знаете. Вполне себе объективная причина. Многие железячники, которым самим приходится писать софт для своих же железок, более-менее знают C, но не имеют возможности освоить фишки C++ (вроде шаблонов), которые могли бы им помочь.
  3. Вы работаете в условиях, где C, только C, и ничего, кроме C. Например, вам платят за разработку ядра Linux-а. Или драйверов для Linux-а. Или вы сопровождаете какой-то старый и большой OpenSource-проект за деньги. Ну тут реальность, данная вам в ощущениях, и вряд ли вы сможете что-то поменять. Хотя вот GCC двинулся в сторону C++, так что все не так однозначно :)
  4. Вам нужно решить небольшую и несложную задачу, для которой вполне достаточно C. Например, написать на C NIF-модуль для Erlang-а. Действительно, если речь идет про пару тысяч строк, то от C++ может и не быть никакого проку, так зачем же усложнять жизнь тем, кто будет подключать ваш NIF-модуль к Erlang-у?
  5. Вы хотите, чтобы ваш код можно было собрать действительно для всего на свете. Компиляторы C++ пока что есть не для всего (особенно если брать какое-то специфическое железо, для которого нет GCC, а есть какие-то наколеночные C-компиляторы).

Если же перед вами стоит выбор: изучать ли C или C++, то я бы посоветовал бы изучать C++. Просто потому, что после C++ вы сможете как опуститься на уровень чистого C, так и без проблем перейти на любой другой императивный язык высокого уровня. Будь то Java (Ceylon, Kotlin), C#, D или Rust. Однако, если возьметесь за C++, то сейчас вам придется столкнуться с такими вещами, как:

  • большой объем и сложность языка. Поймите, C++ -- это язык для серьезной разработки. Он никогда не создавался для того, чтобы в язык было легко погрузиться не умеющему программировать новичку. Поэтому нужно набраться терпения и спокойно относиться к тому, что в работе вы будете использовать только то подмножество языка, которое успели освоить. Это нормально и в этом нет ничего страшного;
  • при изучении С++, особенно если у вас не было достаточного опыта работы с другими объектно-ориентированными и/или функциональными языками, вам придется несколько раз свернуть мозги набекрень. Как минимум, один раз для того, чтобы освоить ООП в C++. Второй раз -- для того, чтобы освоить обобщенное программирование. Плюс к тому, в обобщенном программировании вам придется столкнуться с функциональным подходом;
  • в реальных проектах, с которыми вам доведется столкнуться, C++ будет очень разным. Где-то будет C++ с исключениями, шаблонами, ООП и всем остальным. Где-то будет "С с классами" в худшем своем представлении, без исключений, без шаблонов, и даже на каждую виртуальную функцию вам придется просить разрешение у архитектора проекта. Причем нужно быть готовым к тому, что четких объективных критериев разрешения/запрещения конкретной языковой возможности вы можете и не найти. А если даже и найдете, то можете быть удивлены как "четкостью", так и "объективностью";
  • в реальных проектах код будет сильно отличаться от того, что вы видите в учебниках или в популярных блогах. Причем в обе стороны: код будет как сильно хуже, так и сильно лучше. Тут нужно отдавать себе отчет в том, что язык сам по себе, а программисты сами по себе. Использование С++а неким Васей Пупкиным вовсе не означает, что код Васи Пупкина автоматически станет образцом стиля и воплощением здравого смысла. Помните, что код на Фортране можно написать на любом языке программирования. И если Вася Пупкин любит писать в стиле goto cleanup, то он будет продолжать так делать в C++, даже не смотря на наличие более удобных механизмов. И, вообще говоря, это проблема всех языков программирования, не только C++а;
  • в C++ нет ни общепринятой системы управления зависимостями, ни нормальной общераспространенной системы сборки. Ну вот так сложилось. Неприятно, но жить можно. Особенно, если руки растут не из жопы;
  • время компиляции ваших проектов, особенно очень больших, будет значительным. Если вы пришли в C++ из Go или D, то разница будет просто на порядки;
  • если вам нужно будет разрабатывать кроссплатформенный код, то придется приготовиться к тому, что на разных платформах вы будете использовать разные C++ компиляторы, с разным уровнем поддержки современных стандартов C++. Да и сами компиляторы могут относиться к C++ по-разному (например, VC++ позволяет обходится без typename в ряде случаев, а GCC и clang -- нет). Поэтому для того, чтобы убедиться, что код компилируется и работает, нужно будет прогнать его на всех платформах;
  • язык C++ позволяет строить нужные пользователю абстракции, но т.к. абстракции протекают, нужно быть готовым разобраться с тем, как нужная вам абстракция устроена. Грубо говоря, даже выбирая тип стандартного контейнера для какой-то простенькой задачки, вам нужно будет понимать, чем vector отличается от deque, а deque от list-а.

Очевидно, может возникнуть вопрос, а зачем вообще с C++ связываться? Вопрос очень правильный.

Вполне возможно, что незачем вообще. Сейчас в мире есть огромное количество разработчиков, которые вполне хорошо себя чувствуют, не зная никаких других языков, кроме JavaScript. Или Python-а. Или Java. Или VisualBasic-а. И это совершенно нормально. Как и в любой другой отрасли, по мере развития этой самой отрасли, происходит сильная сегментация. И человек, который плотно занимается front-end-ом для Web-приложений, вовсе не обязательно должен быть в состоянии принять участие в разработке ядра реляционной СУБД.

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

Кроме того, программирование -- это область, которая постоянно движется вперед. Десять лет назад казалось, что C++ не выжить, что он останется только для сопровождения древнего legacy. Прошло десять лет, и оказалось, что C++, во-первых, очень сильно изменился сам и, во-вторых, набрал очень приличный темп своего развития. После долгостроя C++11 последовал C++14, поддержка которого в clang и gcc на очень приличном уровне появилась еще до официального принятия стандарта. Теперь тоже самое происходит и с C++17, только теперь компанию clang и gcc должен составить еще и vc++. Такого, вообще-то говоря, еще не было за всю историю C++. И тенденция может продолжится и далее (C++20, C++23). Поэтому, если смотреть в будущее, выбор динамично развивающегося C++, вместо консервативного C, выглядит более осмысленным.

Добавим сюда и то, что на эффективность работы программ снова стали обращать внимание. И это не спроста. Это обычное развитие для бизнеса: когда захватывается какая-то новая ниша и поставщиков решений не много, то себестоимость не играет большой роли. Однако, по мере освоения ниши и появления большего количества игроков, тема снижения себестоимости и урезания издержек выходит на первый план. Так что если ваше решение хотя бы на 10% эффективнее, чем решение конкурента, то ваши издержки ниже, а шансы на выживание -- выше.

Добавим сюда и то, что сейчас огромное количество разработчиков не знают, что такое эффективность, доступная для языков, вроде C, C++, Ada или Rust. Когда толпы народа пишут даже не на Java, а на Ruby, Python-е, JavaScript-е или Erlang-е, то очень сложно конкурировать с ними используя тот же самый арсенал. На Ruby вы будете вести разработку с той же скоростью и потреблять ваше приложение будет столько же, сколько и у ваших конкурентов. Получить преимущество можно за счет чего-то, что позволяет вести разработку так же быстро, но генерировать более быстрый код, жрущий меньше памяти. Яркий пример -- это язык Go от Google.

Ошибочно считать, что Go -- это замена C и C++. Go -- это замена Python-у и Java. Программы на Go пишутся так же легко и просто, как на Python, но работают гораздо быстрее.

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

Язык C++, начиная с C++11, уверенно движется в этом же направлении: упрощение разработки, но, при этом, получение максимально эффективного кода. Причем, чем дальше, тем увереннее это движение происходит. И, если мы вспомним, что программирование -- это динамично развивающаяся область, то я не удивлюсь, если в скором времени C++ изменится еще сильнее. В частности, в язык могут быть добавлены модули (увеличение скорости компиляции), концепты (упрощение обобщенного программирования), рекомендации C++ Core Guidelines могут стать частью языка/стандартной библиотеки, а компиляторы смогут проводить статический анализ кода (в том числе и на соответствие Core Guidelines)... Десять лет назад я бы, например, не поверил бы, насколько C++ может стать лучше. Так что не удивлюсь, если еще через 10 лет C++ будет еще более другим.

При этом C++, в отличии от того же Swift-а, есть на всех основных платформах. И C++ не требует, чтобы вы переписывали свой старый код под новый вариант языка. То, что в C++ проектах можно найти древний, почти-что-С-шный код без шаблонов, исключений и с goto cleanup -- это как раз одна из сильных сторон C++. Старый, давным-давно отлаженный и работающий код продолжает оставаться таковым и не требует переделки. Это может не нравится, но если у вас миллионы строк такого кода, то ваш взгляд на плавную эволюцию C++ и бережное отношение к обратной совместимости обязательно поменяется :)

Так что, если вам действительно нужен хороший инструмент для решения сложных задач, от которых требуется и эффективность, и качество, то C++ обязательно окажется в short-list-е подходящих инструментов. И отказываться от C++ в пользу C в свете всего вышесказанного, на мой взгляд, не сильно разумно.

Вот, пожалуй, и все. Спасибо всем, кто прочел все части этого разбора.

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