Чем плох C++ (спойлер: всем)

Mar 01, 2020 10:59


с wiki:

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

[Spoiler (click to open)]

Ada и Си

К числу обычно упоминаемых недостатков С++ можно отнести:
• Отсутствие системы модулей. C++ унаследовал от Си подключение заголовочных файлов с помощью препроцессора. Это вынуждает дублировать описания объектов, порождает неочевидные требования к коду (см. правило одного определения) и увеличивает объём компилируемого текста, а значит и время компиляции.
• Наличие более чем одного механизма для выполнения одних и тех же задач, что усложняет язык и приводит к неоптимальному и небезопасному кодированию.
• Унаследованные от Си опасные и провоцирующие ошибки возможности (макроопределения, адресная арифметика, неявное приведение типов, возможность прямого управления распределением памяти).
• Отсутствие встроенных механизмов статической валидации времени жизни объектов, приводящее к внезапному краху программ из-за обращения к уничтоженной переменной, или из-за неправильной многопоточной работы с объектами.
• Шаблоны порождают объёмный и не всегда оптимальный код. Частичное определение шаблонов усложняет как сам язык, так и программы, где оно используется.
• Множественное (в том числе виртуальное) наследование приводит к созданию громоздких иерархий классов, которые при любом изменении требований к программе могут потребовать серьёзного пересмотра.
• Сложный синтаксис и объёмная спецификация языка затрудняют его изучение.
• Язык не поощряет создание надёжного, легко читаемого и удобного в сопровождении кода, вместо этого зачастую предлагая выбор между короткими и простыми, но опасными средствами, унаследованными от Си, и новыми, объёмными и сложными, но более безопасными механизмами.
• Сложная и постоянно разрастающаяся стандартная библиотека, затрудняющая изучение языка.

Чаще всего критики не противопоставляют C++ какой-либо другой конкретный язык, а утверждают, что отказ от использования единственного языка, имеющего многочисленные недостатки, в пользу декомпозиции проекта на подзадачи, решаемые на различных, наиболее подходящих для них, языках, делает разработку существенно менее трудоёмкой при одновременном повышении показателей качества программирования[21][22]. По этой же причине критикуется сохранение совместимости с Си: если часть задачи требует низкоуровневых возможностей, разумнее выделить эту часть в отдельную подсистему и написать её на Си.
В свою очередь, сторонники C++ заявляют, что устранение технических и организационных проблем межъязыкового взаимодействия за счёт использование одного универсального языка вместо нескольких специализированных важнее, чем потери от несовершенства этого универсального языка, то есть сама широта набора возможностей C++ является оправданием недостатков каждой отдельной возможности; в том числе недостатки, унаследованные от Си, оправданы преимуществами совместимости (см. выше).
Таким образом, одни и те же свойства C++: объём, сложность, эклектичность и отсутствие конкретной целевой ниши применения рассматривается сторонниками как «главное достоинство», а критиками - как «главный недостаток».

Известно несколько исследований, в которых была сделана попытка более или менее объективно сравнить несколько языков программирования, одним из которых является C++. В частности:
• В работе «Haskell vs. Ada vs. C++ vs. Awk vs. …» Пауля Худака и Марка Джонса[23] сравнивается ряд императивных и функциональных языков в решении модельной задачи быстрого прототипирования ГИС-системы военного назначения.
• В работе Лутца Прехельта[24] рассмотрено семь языков (C, C++, Java, Perl, Python, Rexx и Tcl) в задаче написания простой программы преобразования телефонных номеров в слова по определённым правилам.
• В статье Дэвида Велера «Ada, C, C++, and Java vs. The Steelman»[25] приведено сопоставление языков Ада, C++, Си, Java с документом «Steelman» - списком требований к языку для военных разработок встроенных систем, который был выработан комитетом по языку высокого уровня Министерства обороны США в 1978 году. Хотя этот документ сильно устарел и не учитывает многих существенных свойств современных языков, сравнение демонстрирует, что C++ по набору востребованных в отрасли возможностей не так уж сильно отличается от языков, которые можно считать его реальными конкурентами.

Ада близка к C++ по набору возможностей и по сферам применения: это компилируемый структурный язык с Симула-подобным объектно-ориентированным дополнением (та же модель «Алгол с классами», что и в C++), статической типизацией, средствами обобщённого программирования, предназначенный для разработки крупных и сложных программных систем. В то же время он принципиально отличается по идеологии: в отличие от C++, Ада строилась на основе предварительно тщательно проработанных условий производителей сложного ПО с повышенными требованиями к надёжности, что наложило отпечаток на синтаксис и семантику языка.
Прямых сравнений эффективности кодирования на Аде и C++ немного. В упомянутой выше статье[23] решение модельной задачи на Аде привело к получению кода примерно на 30 % меньшего по объёму (в строках), чем на C++. Сравнение свойств самих языков приводится во многих источниках, например, в статье Джима Роджерса на AdaHome[26] содержится перечисление более 50 пунктов различий свойств этих языков, большая часть которых - в пользу Ады (больше возможностей, более гибкое поведение, меньше вероятность ошибок). Хотя многие утверждения сторонников Ады спорны, а часть из них явно устарела, в целом можно заключить:
• Синтаксис Ады гораздо строже, чем C++. Язык требует соблюдения дисциплины программирования, не поощряет «программистские трюки», стимулирует написание простого, логичного и легко понимаемого кода, удобного в сопровождении.
• В отличие от C++, Ада максимально типобезопасна. Развитая система типов позволяет, при соблюдении дисциплины их объявления и использования, максимально полно статически контролировать корректность использования данных и защищает от случайных ошибок. Автоматические преобразования типов сведены к минимуму.
• Указатели в Аде контролируются гораздо более строго, чем в C++, а адресная арифметика доступна только через отдельную системную библиотеку.
• Настраиваемые модули Ады по возможностям аналогичны шаблонам C++, но обеспечивают лучший контроль.
• Ада имеет встроенную в язык модульность и стандартизованную систему раздельной компиляции, тогда как C++ применяет включение текстовых файлов и внешние средства управления компиляцией и сборкой.
• Встроенная многозадачность Ады включает параллельные задачи и механизм их коммуникации (входы, рандеву, оператор select). В С++ всё это реализуется только на уровне библиотек.
• Ада строго стандартизована, за счёт чего обеспечивает лучшую переносимость.
В статье Стефена Цейгера из Rational Software Corporation[27], утверждается, что в целом разработка на Аде обходится на 60 % дешевле, и приводит к получению кода, имеющего в 9 раз меньше дефектов, чем на Си. Хотя эти результаты не могут быть прямо перенесены на C++, но всё же представляют интерес с учётом того, что многие недостатки C++ унаследованы от Си.

Оригинальный Си продолжает развиваться, на нём разрабатываются многие масштабные проекты: он является основным языком разработки операционных систем, на нём написаны игровые движки многих динамических игр и большое число прикладных приложений. Ряд специалистов утверждает, что замена Си на C++ не повышает эффективность разработки, но приводит к ненужному усложнению проекта, снижению надёжности и увеличению затрат на сопровождение. В частности:
• По мнению Линуса Торвальдса, «C++ провоцирует на написание … значительного объёма кода, не имеющего принципиального значения с точки зрения функциональности программы»[мнения 3].
• Поддержка ООП, шаблоны и STL не являются решающим преимуществом C++, так как всё, для чего они применяются, реализуемо и средствами Си. При этом устраняется раздувание кода, а некоторое усложнение, которое к тому же далеко не обязательно, компенсируется большей гибкостью, более простым тестированием, лучшими показателями производительности.
• Автоматизация доступа к памяти в C++ увеличивает затраты памяти и замедляет работу программ.
• Использование исключений C++ вынуждает следовать RAII, приводит к росту исполняемых файлов, замедлению программ. Дополнительные трудности возникают в параллельных и распределённых программах. Показательно, что стандарт кодирования на C++ компании Google прямо запрещает использование исключений.[29]
• Код на C++ сложнее для понимания и тестирования, его отладка затрудняется использованием сложных иерархий классов с наследованием поведения и шаблонов. К тому же в средах программирования на C++ больше ошибок, как в компиляторах, так и в библиотеках.
• Многие детали поведения кода стандартом C++ не специфицированы, что ухудшает переносимость и может являться причиной трудно обнаруживаемых ошибок.
• Квалифицированных программистов на Си существенно больше, чем на C++.
Нет убедительных данных о преимуществе C++ перед Си ни по производительности программистов, ни по свойствам программ. Хотя есть исследования[30] утверждающие, что программисты на Си тратят 30 % - 40 % общего времени разработки (не считая отладки) на управление памятью, при сопоставлении общей производительности разработчиков[23] Си и C++ оказываются близки.
В низкоуровневом программировании значительная часть новых возможностей C++ оказывается неприменимой из-за увеличения накладных расходов: виртуальные функции требуют динамического вычисления реального адреса (RVA), шаблоны приводят к раздуванию кода и ухудшению возможностей оптимизации, библиотека времени исполнения (RTL) очень велика, а отказ от неё лишает большинства возможностей C++ (хотя бы из-за недоступности операций new/delete). В результате программисту придётся ограничиться функционалом, унаследованным от Си, что делает бессмысленным применение C++:
… единственный способ иметь хороший, эффективный, низкоуровневый и портируемый C++ сводится к тому, чтобы ограничиться всеми теми вещами, которые элементарно доступны в Си. А ограничение проекта рамками Си будет означать, что люди его не выкинут, и что будет доступно множество программистов, действительно хорошо понимающих низкоуровневые особенности и не отказывающихся от них из-за идиотской ерунды про «объектные модели».
… когда эффективность является первостепенным требованием, «преимущества» C++ будут огромной ошибкой.
- Линус Торвальдс,[31]

По мнению Алана Кэя, объектная модель «Алгол с классами», использованная в C++, уступает модели «всё - объект»[32], используемой в Objective-C, по общем объёму возможностей, показателям повторного использования кода, понимаемости, модифицируемости и тестируемости.
Модель наследования C++ сложна, трудна в реализации и при этом провоцирует создание сложных иерархий с неестественными отношениями между классами (например, наследование вместо вложения). Результатом становится создание сильно зацепленных классов с нечётко разделённым функционалом. Например, в [33] приводится учебно-рекомендательный пример реализации класса «список» как подкласса от класса «элемент списка», который, в свою очередь, содержит функции доступа к другим элементам списка. Такое отношение типов является абсурдом с точки зрения математики и невоспроизводимо на более строгих языках.

Высокая вязкость решений на C++ может требовать повторной разработки значительных частей проекта при необходимости внесения минимальных изменений на поздних стадиях разработки. Яркий пример подобных проблем можно найти в[21]
Как отмечает Ян Джойнер[34], C++ ошибочно отождествляет инкапсуляцию (то есть помещение данных внутрь объектов и отделение реализации от интерфейса) и сокрытие реализации. Это усложняет доступ к данным класса и требует реализовывать его интерфейс практически исключительно через функции доступа (что, в свою очередь, увеличивает объём кода и усложняет его).
Совпадение типов в C++ определяется на уровне идентификаторов, а не сигнатур. Это затрудняет реализацию абстрактных механизмов работы с данными, так как делает невозможной подмену компонентов, основанную на совпадении их интерфейсной функциональности. В результате для включения в систему новой функциональности, реализованной на уровне библиотек, оказывается необходимо вручную модифицировать уже имеющийся код для адаптации его под новый модуль[35]. Как отмечает Линус Торвальдс[31], в C++ «код кажется абстрактным лишь до тех пор, пока не возникает необходимость его изменить».

Порождающее метапрограммирование C++ основано на шаблонах и препроцессоре, оно трудоёмко и ограничено по возможностям. Система шаблонов C++ фактически является вариантом примитивного функционального языка программирования, исполняемого на этапе компиляции. Этот язык почти не пересекается с самим C++, из-за чего потенциал роста сложности абстракций оказывается ограниченным. Программы, использующие шаблоны C++, имеют крайне низкие показатели понимаемости и тестируемости, а само разворачивание шаблонов порождает неэффективный код, так как язык шаблонов не предоставляет никаких средств для оптимизации (см. также раздел #Вычислительная эффективность). Встраиваемые предметно-специфичные языки, реализуемые таким образом, всё равно требуют знания самого C++, что не обеспечивает полноценного разделения труда. Таким образом, возможности C++ по расширению возможностей самого C++ весьма ограничены.[36][37]

Идеология языка смешивает «контроль за поведением» с «контролем за эффективностью», то есть предполагает, что обеспечение полного контроля программиста за всеми аспектами исполнения программы на довольно низком уровне является необходимым и достаточным условием достижения высокой эффективности кода. В действительности для сколько-нибудь крупных программ это неверно, так как их сложность настолько высока, что осознание её в полном объёме на низком уровне превышает возможности человека. Принцип «не платишь за то, что не используешь»[⇨], заявленный как средство обеспечения эффективности, на практике приводит к отказу от параметрического полиморфизма и необходимости явного описания различного поведения для различных ситуаций под единым идентификатором (перегрузки функций), с ручной оптимизацией кода для каждого такого варианта, что вызывает значительное увеличение объёма и сложности кода, усугубляя проблемы управления сложностью. Возложение на программиста низкоуровневой оптимизации, которую качественный компилятор предметно-ориентированного языка способен выполнить заведомо более эффективно, приводит лишь к росту трудоёмкости программирования и снижению показателей понимаемости и тестируемости кода. Таким образом, принцип «не платить за то, что не используется» в действительности не даёт желаемых выгод в эффективности, но негативно сказывается на качестве.

ML и Си

Принцип C++ «не навязывать „хороший“ стиль программирования» противоречит промышленному подходу к программированию, в котором ведущую роль играют качество программного обеспечения и возможность сопровождения кода не только автором, и для которого предпочтительны языки, сводящие к минимуму влияние человеческого фактора, то есть как раз «навязывающие „хороший“ стиль программирования», хотя такие языки и могут иметь более высокий порог вхождения.

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

Шаблоны - утиная оптимизация, тот самый параметрический полиморфизм.

Существует несколько разновидностей полиморфизма. Две принципиально различных из них были описаны Кристофером Стрэчи[en] в 1967 году: это параметрический полиморфизм[⇨] и ad-hoc-полиморфизм[⇨], причём первая является истинной формой, а вторая - мнимой[1][4]; прочие формы являются их подвидами или сочетаниями. Параметрический полиморфизм подразумевает исполнение одного и того же кода для всех допустимых типов аргументов, тогда как ad-hoc-полиморфизм подразумевает исполнение потенциально разного кода для каждого типа или подтипа аргумента. Бьёрн Страуструп определил полиморфизм как «один интерфейс - много реализаций»[5], но это определение покрывает лишь ad-hoc-полиморфизм.

Существует мнение, что предпочтение использования C++ (при возможности выбора альтернативных языков) отрицательно характеризует профессиональные качества программиста. В частности, Линус Торвальдс говорит, что использует положительное мнение кандидатов о C++ в качестве критерия отсева[мнения 3]:
C++ - кошмарный язык. Его делает ещё более кошмарным тот факт, что множество недостаточно грамотных программистов используют его… Откровенно говоря, даже если нет никаких причин для выбора Си, кроме того чтобы держать C++-программистов подальше - то одно это уже будет достаточно веским основанием для использования Си.
…Я пришёл к выводу, что действительно предпочту выгнать любого, кто предпочтёт вести разработку проекта на C++, нежели на Си, чтобы этот человек не загубил проект, в который я вовлечён.
- Линус Торвальдс,[31]

C++ определяется его апологетами как «мощнейший» именно потому, что он изобилует опасными взаимно-противоречивыми возможностями. По мнению Эрика Реймонда, это делает язык сам по себе почвой для личного самоутверждения программистов, превращения процесса разработки в самоцель:
Программисты - это зачастую яркие люди, которые гордятся … своей способностью справляться со сложностями и ловко обращаться с абстракциями. Часто они состязаются друг с другом, пытаясь выяснить, кто может создать «самые замысловатые и красивые сложности». … соперники полагают, что должны соревноваться с чужими «украшательствами» путём добавления собственных. Довольно скоро «массивная опухоль» становится индустриальным стандартом, и все используют большие, переполненные ошибками программы, которые не способны удовлетворить даже их создателей.

… такой подход может обернуться неприятностями, если программисты реализуют простые вещи сложными способами, просто потому что им известны эти способы и они умеют ими пользоваться.
- Эрик Реймонд в [47]

Перечисленные выше факторы делают сложность менеджмента проектов на C++ одной из самых высоких в индустрии разработки ПО.
Джеймс Коггинс, в течение четырёх лет ведущий колонку в The C++ Report, дает такое объяснение:
 - Проблема в том, что программисты, работающие в ООП, экспериментировали с кровосмесительными приложениями и были нацелены на низкий уровень абстракции. Например, они строили такие классы как «связанный список», вместо «интерфейс пользователя», или «луч радиации», или «модель из конечных элементов». К несчастью, строгая проверка типов, которая помогает программистам C++ избегать ошибок, одновременно затрудняет построение больших объектов из маленьких.
- Ф. Брукс, Мифический человеко-месяц

Rust и Go
(To Objective-C;)

Старейшим конкурентом C++ в задачах низкого уровня является Objective-C, также построенный по принципу объединения Си с объектной моделью, только объектная модель унаследована от Smalltalk. Objective-C, как и его потомок Swift, широко используется для разработки ПО под macOS и iOS.

Попыткой создать промышленную замену C/C++ стал разработанный в корпорации Google в 2009 году язык программирования Go. Авторы языка прямо указывают, что мотивом для его создания были недостатки процесса разработки, вызванные особенностями языков Си и C++[52]. Go - компактный, несложный по структуре императивный язык с Си-подобным синтаксисом, без препроцессора, со статической типизацией, строгим контролем типов, системой пакетов, автоматическим управлением памятью, некоторыми функциональными чертами, экономно построенной ООП-подсистемой без поддержки наследования реализации, но с интерфейсами и утиной типизацией, встроенной многопоточностью, основанной на сопрограммах и каналах (a-la Occam). Язык позиционируется как альтернатива C++, то есть, в первую очередь, средство групповой разработки высокоэффективных вычислительных систем большой сложности, в том числе распределённых, допускающее, при необходимости, низкоуровневое программирование.
В одной экологической нише с C/C++ находится разработанный в 2010 году и поддерживаемый корпорацией Mozilla язык Rust, ориентированный на безопасное управление памятью без использования сборщика мусора. В частности о планах частичной замены C/C++ на Rust объявила в 2019 компания Microsoft[53].

P.s.

Осталось вспомнить, что Apple развила Objective-C до такой степени, что после OSX 10.6 начала переписывать на нём части своей операционной системы. А применение public-интерфейсов на Си вместо всяких сипов (sip: C++ to Python macro compiler) избавляет программу на С++ от ограничений.

c++, #Вычислительная

Previous post Next post
Up