суббота, 10 июня 2017 г.

[prog.c++] В очередной раз про Rust. О том, почему до сих пор меня Rust не прельстил

На C++ я программирую уже давно. Лет 25 точно. Не могу сказать, что знаю C++ хорошо, но, наверное, хорошо знаю, как использовать C++ так, чтобы не было мучительно больно. Тем не менее, не могу не признать, что C++ уже весьма стар, сложен и противоречив. Хоть программировать на нем в последнее время становится удобнее, но есть серьезные родовые травмы, которые будут постоянно отравлять жизнь C++ разработчикам. Посему лично я был бы не прочь со временем сменить C++ на что-то более современное, удобное и безопасное.

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

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

А вот для Rust-а такого рынка, насколько я могу судить, пока еще нет. Вроде как есть какой-то совсем небольшой спрос на Rust-разработчиков, но вряд ли сейчас найдется много желающих покупать коммерческие библиотеки для Rust-а, или коммерческие лицензии на такие библиотеки. Да и вряд ли кто-то будет готов платить за консультации по Rust-у или за обучение Rust-у разработчиков, переходящих на Rust с других языков программирования. Думаю, что не ошибусь, если скажу, что не смотря на весь хайп вокруг Rust-а, реальное присутствие Rust-а в коммерческой разработке ПО в мире находится где-то на уровне статпогрешности. Посему достаточно емкого рынка инструментария для Rust-а еще просто нет. Может через 3-5 лет такой рынок сложится и достигнет достаточного объема, чтобы на нем можно было бы зарабатывать. Но сейчас это не так.

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

Но сегодня я бы хотел затронуть еще один аспект. Он касается того, насколько сильно придется перестраивать мозги старому C++нику, вроде меня, при переходе с C++ на Rust. И будет ли такая перестройка того стоить.

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

Вот, например, из того, над чем я работаю сейчас. Небольшой фрагмент:

templatetypename LOCK_TYPE, typename TRACING_BASE >
class notify_mbox_t
   : public abstract_message_box_t
   , private details::lock_holder_detector<LOCK_HOLDER>::type
   , private TRACING_BASE
   {
   public :
      templatetypename... TRACING_BASE_ARGS >
      notify_mbox_t(
         intrusive_ptr_t< time_elapsed_mbox_t > time_mbox,
         mbox_id_t id,
         TRACING_BASE_ARGS && ...tracing_args )
         :  TRACING_BASE( std::forward<TRACING_BASE_ARGS>(tracing_args)... )
         ,  m_time_mbox( std::move(time_mbox) )
         ,  m_id(id)
         {}
   ...

Это шаблонный класс почтового ящика, который параметризуется двумя параметрами. Первый параметр -- это тип объекта, который будет защищать почтовый ящик от многопоточного доступа. В качестве LOCK_TYPE может использоваться std::mutex. Или какой-то вариант spinlock-а. Или null_mutex, если защита от многопоточности не требуется.

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

Например, если в программе создается почтовый ящик, который должен быть защищен std::mutex-ом и должен логировать операции отсылки сообщений, то такой шаблон должен быть раскрыт во что-то вроде:

class notify_mbox_t<std::mutex, tracing_enabled>
   : public abstract_message_box_t
   {
      std::mutex m_lock;
      msg_tracing::tracer_t & m_tracer;

      templatetypename LAMBDA >
      auto lock_and_perform( LAMBDA && lambda )
         {
            std::lock_guard< std::mutex > lock{ m_lock };
            return lambda();
         }

   public :
      notify_mbox_t(
         intrusive_ptr_t< time_elapsed_mbox_t > time_mbox,
         mbox_id_t id,
         msg_tracing::tracer_t & tracer )
         :  m_tracer( tracer )
         ,  m_time_mbox( std::move(time_mbox) )
         ,  m_id(id)
         {}

   ...
   };

Т.е. здесь будет и std::mutex, и вспомогательный метод для блокировки объекта на время выполнения какой-то операции, и трассировщик операций над почтовым ящиком.

А вот если нужен почтовый ящик без защиты от многопоточности и без надобности в трассировке, должно получиться что-то вроде:

class notify_mbox_t<null_mutex, tracing_disabled>
   : public abstract_message_box_t
   {
      templatetypename LAMBDA >
      auto lock_and_perform( LAMBDA && lambda )
         {
            return lambda();
         }

   public :
      notify_mbox_t(
         intrusive_ptr_t< time_elapsed_mbox_t > time_mbox,
         mbox_id_t id )
         :  m_time_mbox( std::move(time_mbox) )
         ,  m_id(id)
         {}
   ...
   };

Т.е. в объекте не должно быть вообще ничего, чем объект не пользуется.

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

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

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

Я бы добавил сюда еще и то, что Rust-овый код выглядит перегруженным. Т.е. код компактен и лаконичен, но именно это и усложняет его чтение, т.к. в одной строке содержится намного больше смыслов, чем в C++. Так, встретив что-то вроде let a = f.foo()?.bar().unwrap().baz()?.unwrap_or_else(||b); нужно быть очень внимательным.

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

Так что, не смотря на то, что Rust на данный момент единственная реальная альтернатива C++, меня эта альтернатива пока что не прельщает. Мне бы хотелось чего-то вроде очищенного и более строгого C++. С уже привычным ООП, шаблонами, исключениями, перегрузкой операторов. С позаимствованными из Rust-а лайфтаймами, с нормальными модулями и пакетным менеджером. И в привычном синтаксисе, без экономии на спичках в виде fn, mut и ref.

Остается надеяться, что Страуструп был прав, говоря о том, что внутри C++ есть небольшой, красивый и безопасный язык, который стремиться вырваться наружу. Уже как бы пора :)

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