Недавно я опубликовал мнение о фундаментальной экономической модели разработки ПО, которая не способствует (и объективно не должна способствовать) массовому переходу с C/C++ на «безопасные» языки программирования Экономика безопасности кода или почему Rust не нужен.
Но чтобы оставаться честным перед читателями, решил опубликовать и статью-контраргумент с описанием обратной стороны медали, то есть почему C++ всё равно будет рано или поздно заменён, а заодно попробовать разобрать, каким будет новый язык программирования, который неминуемо придёт на смену C++.
Проблемы C++ и тупик эволюции
Невозможность безопасного управления памятью и неопределённое поведение (UB) стали частью стандарта С++. Они встроены в его основу. Макросы, заголовочные файлы, частичная специализация, SFINAE, правила инициализации - любая попытка изменения С++ часто упирается в то, что уже существует множество легального кода, который зависит от текущих правил. И нужно либо сохранять старое поведение, потому что отказ от UB или переход на строгую проверяемую модель управления памятью сразу ломает множество допущений в существующем коде или делать новую концепцию параллельно со старой, что еще больше увеличивает сложность языка.
Поэтому за успехом C++ скрывается парадокс: чем более устоявшимся становится стандарт языка, тем сложнее вносить в него изменения. Организационно это означает необходимость синхронизации большого числа участников процесса: разработчиков компиляторов, библиотек, представителей компаний и независимых экспертов, каждый из которых представляет различные интересы. Кто-то борется за безопасность памяти, кто-то - за нулевые накладные расходы, и всё это происходит с учётом обязательств перед работодателем и личных амбиций.
В результате C++ сам себя загнал в ловушку. Его проблемы известны десятилетиями, предложения обсуждаются годами, а быстро принять дополнения в стандарт можно только то, что почти ни с чем не конфликтует и, соотвественно, ни на что не влияет :-(. И для языка, который претендует на роль основного инструмента системного программирования, это уже не недостаток. Это ахиллесова пята.
Парадокс современных «C++ killers»
Поэтому неудивительно, что регулярно возникают языки-«убийцы C++». Почти каждый новый системный язык в какой-то момент позиционировался как кандидат на замену С++: безопаснее память, лучше параллелизм, понятнее ошибки, современнее модель разработки. Многие из этих языков действительно успешны в своих нишах и нередко превосходят C++ по эргономике и безопасности. Но как замена C++, в смысле массового вытеснения его из существующих больших кодовых баз, у них не получается. И причина здесь часто не в синтаксисе и даже не в качестве компилятора.
Главный барьер - это совместимость. C++ - это не только язык. Это библиотеки, ABI, соглашения о вызовах, бинарные интерфейсы, инструменты сборки, линковщики, дебаггеры, профилировщики, санитайзеры, сертификации, тысячелетние make/CMake/Bazel-скрипты, тонны заголовков и макросов, миллионы строк «встроенной культуры» кодирования. Когда новый язык приходит со своим компилятором (пусть даже он и использует LLVM как backend), он приносит с собой новый набор границ: как смешивать модули, как линковаться с существующими библиотеками, как дебажить стек вызовов, как гарантировать совместимость исключений, RTTI, манглинг имён и соглашений о вызовах на разных платформах.
В теории легко сказать «у нас есть FFI». На практике же FFI - это extern "C", что недостаточно для взаимодействия с C++ с его шаблонами, инлайнами, перегрузками, SFINAE, ADL и культурой header-only библиотек. Большая часть современной C++ -экосистемы не является удобным C API, а представляет собой набор заголовков, которые предполагают, что потребитель тоже компилируется как C++ и живёт по тем же правилам.
Почему-то все языки, убийцы C++, забыли очень полезный исторический урок. Ранний компилятор C++ (тогда ещё «C с классами») - cfront - был выполнен как транспайлер, который превращал программу C++ в обычный исходный код на C. Это означало, что любой существующий C-компилятор мгновенно становился способом «компилировать C++». Любой существующий линковщик, отладчик и профилировщик оставались на месте. Риск использования нового языка уменьшался многократно, ведь можно было начинать использовать новые возможности, не переучивая разработчиков и не перестраивая весь производственный процесс.
Однако все современные «замены C++» часто выбирают противоположный путь: создают собственный компилятор и собственную платформу исполнения (пусть даже и опирающуюся на LLVM). Технически это понятный выбор: проще обеспечить целостную систему и лучше диагностика. Но цена такого выбора - создание барьера совместимости. Там, где Cfront снимал проблему интеграции со старым исходным кодом, новый компилятор порождает её заново: нужно решать вопрос бинарных границ, отладки, смешивания с legacy-библиотеками, сборки, деплоя и политики обновлений. В результате новый язык может быть эстетически прекрасным, но он становится возможной альтернативой только для новых проектов, но никак не «заменой C++» в смысле миграции гигантских кодовых баз.
Новый язык на замену C++
Поэтому наиболее реалистичный сценарий появления настоящей замены C++ выглядит иначе. Она вырастет из транспайлера, который генерирует C или, вероятнее, C++ как целевой язык. Транспайлер даёт главное, что нужно на масштабе C++: инкрементальность. Можно начать с одного компонента, одного файла, одной подсистемы, не переписывая всё. Сборка остаётся прежней, линковка остаётся прежней, ABI остаётся прежним, инструменты остаются прежними.
«Новый язык» становится фронтендом, который постепенно отвоёвывает территорию внутри существующего мира, а не требует построить новый мир рядом. При этом можно вводить безопасность «по умолчанию», оставляя явно помеченные «unsafe»-участки там, где нужно взаимодействовать с низкоуровневой реальностью. И самое важное: можно быть совместимым не только с C как минимальным знаменателем, но и с C++ - экосистемой в целом.
Конечно, транспайлинг, это не панацея и такой подход создаёт свои сложности: качество генерируемого кода, читаемость, соответствие отладочной информации, точность отображения исходных абстракций и необходимость использования C++ компиляторов. Но эти сложности могут быть более приемлемыми, чем полномасштабная смена компилятора и всей инфраструктуры. Поэтому транспайлер - это способ свести риски перехода на новый язык к контролируемой величине и сделать миграцию на новый язык экономически возможной.
Отсюда можно сделать вывод, что скорее всего победит не обязательно самый «чистый» или «правильный» язык, а тот, у которого будет самый низкий барьер использования в уже существующей экосистеме. C++ когда-то победил именно так - не качеством языка, а простотой интеграции с уже существующими решениями. Поэтому, если когда-то и появится замена C++, которая действительно станет массовой, то она, скорее всего, начнётся с транспайлера: как надстройка над C++, которая в первую очередь совместима, и только потом удобнее и безопаснее.
