Код
#статьи

Чем Rust отличается от «плюсов»: откровение ветерана С++

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

Изображение: Polina Vari для Skillbox Media

Дмитрий Свиридкин

эксперт

об эксперте

Программист. Разрабатывает на C++ и Rust решения для платформы компьютерного зрения в Arrival. Автор сборника материалов по C++.


Ссылки


Я решил попробовать Rust, потому что устал отлавливать на код-ревью (и не только) одни и те же ошибки в C++. Обязательно кто-нибудь объявит статик-лямбду и захватит в неё по ссылке нестатический временный объект. А когда код с такими ошибками коммитят, он проходит тесты, предполагающие однократный запуск. Программа попадает в продакшен, где запускается пару раз и падает. На поиск и отладку багов уходит много сил и времени.

В Rust нет бардака с библиотеками

У С++ всегда было две проблемы: недостаточная квалификация разработчиков и отсутствие нормальных пакетных менеджеров.

Раньше приходилось гуглить, копировать и компилировать исходники библиотек — та ещё головная боль. Бывало, найдёшь исходники, а компилятор не подходит, потому что он слишком старый, хедеры протухшие или версии зависимостей не совпадают. Тогда вместо готового решения придумывали своё — и это своё всегда заканчивалось кучей багов.

Изображение: Public Domain

Например, я видел реализации std::optional, которые не вызывают деструктор, даже если тип нетривиально деструктурируемый. Тогда как стандартная реализация — это куча boilerplate-кода, который даже командой из трёх-четырёх человек невозможно отладить.

Получается полный бардак. Часть кода покрывают тестами, она кое-как работает, а когда начинаешь детально тестировать — тут дедлок, там use-after-free и так далее. В Rust эти заботы можно частично переложить на плечи компилятора, но с ним иногда приходится бороться: богатая система типов требует более педантичной работы.

Чтобы писать на Rust, мне не пришлось менять IDE. Просто подключил к VS Code code-assistant rust-analyzer (это что-то вроде майкрософтовского IntelliSense). На прошлой работе писали в CLion от JetBrains. У неё есть неплохой плагин для Rust, но при рефакторинге он может наделать делов и оказать медвежью услугу. Так что IDE от JetBrains научили меня не доверять авторефакторингу — обязательно что-нибудь да сломается. Поэтому стараюсь аккуратно рефакторить сам.

Система типов в Rust защищает от ошибок

Бизнес-логика — именно то, что нужно писать на Rust, потому что с ним тяжело ошибиться. Ещё на прошлой работе мы запилили плагин — в качестве proof of concept того, что на Rust вообще можно создавать плагины к большому SDK. Логика была примитивная: принять список слов и проверить, совпадает ли с ним input.

Почему такой простой плагин? Потому что больше никто в команде не знал Rust. Язык молодой, и пока на нём мало кто пишет. Создавать проекты, которые может поддерживать только один разработчик, невыгодно. Проще найти «плюсовиков», поэтому C++ никуда не исчезнет.

Исследование команды Rust «Почему программисты не пишут на Rust». Главная причина — компания не использует Rust
Изображение: Rust Blog

На новой работе я перевожу часть проекта с C++ на Rust. Язык подкупил меня мощной системой типов, которая позволяет выразить зависимости между временами жизни объектов. В языках с ещё более мощными системами типов, например с зависимыми типами, можно проверять статически рантаймовые ограничения. Например, запретить функции принимать пустые строки — компилятор проверит.

Однако у таких мощных языков есть общая проблема: вы замучаетесь доказывать компилятору, что код работает правильно и ничего в нём не нужно исправлять. К счастью, Rust не совсем такой, это золотая середина: с одной стороны, его система типов достаточно строгая, чтобы защитить вас от распространённых ошибок. С другой — не настолько строгая, чтобы приходилось мучиться с ней лишнего.

Программы на Rust без стороннего кода сравнимы по скорости с «плюсовыми»

На прошлой работе я переписывал большой графовый алгоритм — без unsafe-кода, с контейнерами из стандартной библиотеки.

По производительности программа была всего на 10% медленнее «плюсовой». При этом обошлись без стороннего кода. Считаю, что результат хороший. Под C++ пришлось три месяца искать hashmap и перебирать варианты: в одной выравнивание как-то хитро сконфигурировано и приводит к segfault, в другом exception вылетает, если хеш плохой, третий вообще уже четыре года не поддерживается.

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

Code-assistant rust-analyzer отлично работает с шаблонами

В последнее время я оборачиваю небезопасные библиотеки языка С, чтобы подцепиться к каноническому Rust API. Если бы сразу начал писать на C++, уже давно бы закончил и общался с железом, к которому эта библиотека поставляется. А так как пишу на Rust, то пришлось целую неделю аккуратно оборачивать код в канонические Rust-структуры. Столкнулся с тонкостями системы типов: вариантностью ссылок, контравариантностью типов. Если не обращать на них внимания, то safe-обёртка над C API будет некорректной.

Оборачивать низкоуровневый unsafe-код в safe на Rust довольно долго, но оно того стоит. «Плюсовой» IntelliSense вряд ли сравнится с мощным rust-analyzer и справится далеко не со всем кодом, особенно с шаблонами.

Возможно, с появлением стандарта С++20 появятся хинты и IntelliSense научится подсказывать внутри шаблонного кода, если в параметрах указать концепт. Думаю, раньше всех эту фичу внедрит в свои IDE JetBrains — если уже не начала втихаря над ней работать. Шаблоны без концептов в C++ всегда работали плохо: стоит поставить неподходящий аргумент — и компилятор выдаёт огромные сообщения об ошибках. Пока у анализаторов Rust гораздо больше возможностей, да и писать шаблонный однотипный код на нём получается гораздо быстрее.

У Rust настоящая zero-cost abstraction

Помимо Rust, я присматривался и к другим языкам. Три года назад, когда впервые сменил работу, думал погрузиться в светлый мир JVM и написать что-нибудь на Kotlin. Но языки вроде Scala, Java и Kotlin можно применять далеко не везде. Виртуальные машины создают дополнительную нагрузку и для встраиваемого ПО в микрокомпьютерах не подходят. В таких системах пишут на чистом С, С++ или совсем страшных штуках вроде MISRA C.

У Rust, скомпилированного в native, нет дополнительного рантайма. RAII, деструкторы, конструкторы как в C++. Только у Rust линейные типы и zero-cost с ними настоящий, а у C++ — нетривиальный деструктор у типа, и хоть убейтесь, но не получится передать его значение через регистры.

Ещё есть Zig — он очень похож на Rust. Там, например, тоже есть проверка lifetime, но организована она иначе, и то, как это сделано в Rust, мне нравится больше. Других языков с проверкой lifetime я не знаю, а в языках со сборщиками мусора она не нужна: если есть ссылка на объект, значит, он точно живой.

В Go механизм похожий, но там есть сборщик мусора. Мне предлагали перейти на него четыре года назад. Я попробовал, и синтаксис меня рассмешил. Стоит автоформатеру неправильно перенести строки, и программа не скомпилируется. А всё из-за неявной расстановки точек с запятой.

С похожей проблемой я сталкивался, когда мы в первый раз подключали сторонний форматер для C++ — кажется, это был Uncrustify. Он убрал лишние фигурные скобки, и размер критических секций у меня резко вырос. Да уж, отличный форматер — поменял поведение программы. Мог бы просто весь код снести.

В Rust более лаконичный синтаксис, но к нему нужно привыкнуть

Вообще, синтаксис Rust меня вначале сильно раздражал, но я уже почти смирился с ним.

Раздражает символ ; в конце expression, который меняет возвращаемый тип на аналог сишного void. Поставил точку с запятой — программа перестаёт компилироваться. А компилятор молотит type-чекером, который занимает целое ядро, чтобы rust-analyzer и IDE написали красным: «Смотри, у тебя тут типы не сошлись».

Хорошо хоть в экосистеме Rust пофиксили много ошибок и в поставке уже есть официальный форматер, который всё делает правильно. Конечно, тоже есть проблемы. Например, если вы хотите сделать что-то серьёзное с пакетными менеджерами, например сложить собранные артефакты в каталог, то придётся вручную писать поверх скрипты, например на Bash. Штатными средствами это сделать либо нельзя, либо они unstable.

В целом я свыкся с упоротым синтаксисом и краткими ключевыми словами, но всё равно считаю, что они должны состоять хотя бы из трёх символов. Я люблю называть свои переменные fn, а мне его предлагают в качестве ключевого слова. С другой стороны, минималистичный синтаксис — это хорошо.

Лямбды можно писать кратко и без ключевого слова return — это экономит кучу времени. Зато когда после этого переключаешься на С++, то всё время забываешь писать return и, указав тип возврата, получаешь функции с неопределённым поведением. В С++ синтаксис лямбд вообще напоминает синтаксис обычных функций, только trailing return type сделали — ну, и на том спасибо, что уж там. А скобки и return нужно писать обязательно, иначе будете ждать от функции int, а она ничего не вернёт.

При этом Rust не панацея

Тех, кто только планирует погрузиться в Rust, предупреждаю: это не панацея от всех болячек C++. Он защищает вас от гонки данных через проверку borrow checker, но пропускает дедлоки. Защищает от use-after-free, но только в safe-подмножестве. Если же работаете с unsafe — у вас, по сути, будет тот же С++, только с более продвинутой стандартной библиотекой.

Изображение: Альберто Блинчиков для Skillbox Media

Хотя и здесь не всё так однозначно. Многие важные фичи, например для разработки драйверов или встроенного ПО, остаются нестабильными, а значит, писать на Rust серьёзные проекты пока рискованно. По этой причине от Rust часто отказываются в пользу C++, где всё давно stable и unsafe.



Изучайте IT на практике — бесплатно

Курсы за 2990 0 р.

Я не знаю, с чего начать
Освойте топовые нейросети за три дня. Бесплатно
Знакомимся с ChatGPT-4, DALLE-3, Midjourney, Stable Diffusion, Gen-2 и нейросетями для создания музыки. Практика в реальном времени. Подробности — по клику.
Узнать больше
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована