вторник, 5 ноября 2024 г.

[prog.c++] Послесловие к релизу SO-5.8.3: будет ли SO-5.9 и если будет, то когда?

Осенью 2014-го года мы выпустили первую версию в ветке 5.5. Эта ветка затем развивалась пять лет без серьезных ломающих изменений. Я бы был не против и дальше обходится без заметных переделок, но, к сожалению, версия 5.5 набрала такой груз разнообразных фич, который стало уже тяжело нести. Накопился опыт, взгляды на какие-то вещи принципиально поменялись и было решено разгрести накопившийся наслоения не всегда хорошо сочетающейся функциональности. Так в 2019-ом появилась ветка 5.6.

Cчитаю, что в течении последних пяти лет именно эта ветка и развивается. Хотя, формально, мы сделали переход от 5.6 к 5.7, а затем и к 5.8, поскольку были пусть и небольшие, но ломающие совместимость изменения. Тем не менее, различия между 5.6 и 5.8 гораздо меньше, чем между 5.5 и 5.6. Так что для меня лично 5.6/5.7/5.8 -- это развитие одной и той же линии романа и изменение номера версии всего лишь дань формализму.

Сейчас заканчивается 2024-й год и получается, что семейство 5.6/5.7/5.8 поступательно развивается уже пять лет. Вроде бы повторяется история с пятилетним циклом жизни 5.5 и пора задумываться о том, что дальше.

На данный момент, в отличии от ситуации с 5.5, я не вижу каких-то фатальных недостатков в семействе 5.6/5.7/5.8. Необходимости разгрести авгиевы конюшни пока нет. ИМХО, у 5.8 еще есть запас прочности для продолжения в том же духе.

Поэтому какой-то насущной необходимости начинать ветку 5.9 нет.

Насущной нет, но есть вопрос с освоением новых стандартов C++. И в 2025-ом нужно будет всерьез задумываться над этим вопросом.

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

В 2019-ом было решено, что невыгодно дальше держаться за C++11.

Полагаю, в ближайшие полтора-два года так же невыгодно станет держаться и за C++17. Вот тогда на горизонте и появится ветка 5.9.

Это один возможный сценарий развития событий.

Но может быть и другой, хотя пока что он совсем уж фантастический. Есть у меня давнишняя мысль о том, чтобы двигать SObjectizer в направлении, когда в нем можно будет держать работу с динамической памятью под полным контролем.

Ведь сейчас в SObjectizer аллокации/деаллокации буквально на каждом шагу: за send-ом скрывается new, постановка заявки в очередь к агенту может вести к аллокациям, подписка или установка delivery filter требует аллокаций, даже смена состояния агента, если для состояния используется time_limit, требует аллокаций.

Подобное поведение автоматически ставит крест на применении SObjectizer в системах реального времени. Что иронично, т.к. SObjectizer вырос из SCADA Objectizer, создававшегося именно под реальное время. Но в этом я не вижу ничего страшного, ну нет и нет.

Меня больше беспокоит другой аспект, а именно применение SObjectizer-а в условиях, где мы хотим стабильности и надежности. Пусть это десктопное приложение, в котором пользователь обрабатывает видео-потоки с сотни камер или же прокси-сервер одновременно обслуживающий несколько сотен тысяч подключений. Приложение может упереться в имеющуюся память, где-то возникнет bad_alloc и нужно как-то из возникшей ситуации выбираться без краха и последующего рестарта.

Да, это не просто. Да, придется прилагать серьезные усилия как на этапе проектирования софта, так и на этапах его разработки и тестирования. Из-за этого как раз зачастую на выживание при bad_alloc-е и не обращают внимания, у многих разработчиков есть целая мантра, что-то вроде "если нет памяти, то нет и жизни".

Прекрасно понимаю почему так, да и сам далеко не всегда готов платить за разработку дороже, если крах при bad_alloc-е вполне себе допустим.

Но что делать когда крах при bad_alloc-е ну такое себе? Вот тот же прокси-сервер с 200k одновременных подключений... Он рестартует, это OK. Вполне вероятно, что он рестартует за считанные секунды, а то и быстрее. Тогда как восстановление этих 200k соединений -- это же не быстрый процесс. Это неизбежно скажется на впечатлении пользователей от качества сервиса. Но, что хуже, у нас нет устойчивости против подобных падений в будущем. Допустим, мы принимаем 200k подключений и работаем нормально, но затем приходит одно специфическое подключение, при работе с которым мы вынуждены активно потреблять память и это ведет нас напрямую к очередному bad_alloc-у. И мы опять падаем роняя 200k соединений. А через какое-то время все повторяется вновь.

Если же поставить себе целью написать код, который может пережить bad_alloc, то придется придумать какую-то стратегию выживания. У нас должен быть набор действий по восстановлению (например, закрытие проблемного соединения и очистка связанных с ним ресурсов). И, что важно, мы должны быть уверены, что при выполнении данного набора действий у нас нет никаких динамических аллокаций (ведь каждая новая аллокация в таких условиях может вести к повторному bad_alloc-у).

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

А теперь представим, что подобное приложение пишется на SObjectizer-е и этот самый набор действий по восстановлению включает отсылку и обработку SObjectizer-овских сообщений.

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

Вот под добавление в SObjectizer чего-то подобного я бы начал ветку 5.9 не раздумывая прям завтра (но не сегодня, сегодня уже поздновато). Но...

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

суббота, 2 ноября 2024 г.

[prog.c++] Новые версии SObjectizer и so5extra: 5.8.3 и so5extra. Мои личные впечатления

Мы зафиксировали новые версии SObjectizer и so5extra: 5.8.3 и 1.6.2. На Хабре опубликована статья с описанием нововведений, так что не буду здесь повторяться. Озвучу здесь свои личные впечатления от работы над этими релизами.


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

Получилось не сказать, чтобы красиво и эргономично, поэтому не удивлюсь, если msg_hierarchy в итоге останется невостребованным. Возможностей C++17 (и моих знаний этих самых возможностей) хватило только на этот вариант. Была бы в C++ рефлексия, можно было бы сделать и покрасивши. Так что будем ждать принятия рефлексии в C++26, затем дождемся года 2029-го или даже 2030-го, чтобы безопасно перейти на компиляторы с нормальной поддержкой C++26... 🙂

Но применительно к msg_hierarchy я бы хотел вот о чем сказать: это пример той фичи, которую не получалось придумать "на заказ". Тот самый случай, когда пришлось ждать чего-то вроде "озарения".

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

А вот этим летом что-то бум! И перемкнуло в голове. Возникла непонятно откуда взявшаяся мысль о том, а что, если на каждый тип подписки выделять отдельный mbox? Т.е. хочешь подписаться на базовый тип сообщения -- бери mbox именно для этого типа. Хочешь подписаться на конкретный производный тип -- бери другой mbox, именно для этого конкретного производного типа.

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

Принципиальным моментом была именно идея о разных receiving_mbox-ах. Которую пришлось ждать больше трех лет и которая не желала появляться на свет "на заказ".

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


Когда писал статью для Хабра об этом релизе, то поймал себя на неожиданном ощущении.

В 2016-ом, когда я только начал публиковать на Хабре статьи о SObjectizer-е, маркетинговая составляющая была одной из главных. Все-таки мы хотели организовать бизнес вокруг своего OpenSource и нужно было показать "товар лицом". Конечно же, это не были чисто продажные статьи из категории "покупайте наших слонов", но начинающий маркетолог внутри нашептывал, что мол нужно показать насколько у нас все зашибись и какие мы сами крутые, все из себя такие опытные и умелые.

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

А вот сейчас работая над текстом, даже мысли такой не возникало.

Как бы не первый год пилим и пилим. Кому могли "продать", тем уже "продали". Миллионов нам SObjectizer не принес. Ну так чего выделываться?

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

Надеюсь, что подобное же настроение сохранится и дальше. Если в SObjectizer-е будет что-то переделываться или же что-то будет туда добавляться, то можно будет спокойно рассказывать о технической части, совершенно не думая о том, насколько это поможет или повредит "продажам".

Так что данная версия себя оправдала хотя бы тем, что мой внутренний маркетолог-неудачник окончательно плюнул на все и с какими-то неразборчивыми словами по типу "А ну вас! Любитесь как хотите!" ушел в закат.


На этом, пожалуй, все. Спасибо всем, кто дочитал. Если вы еще и прочитали и статью на Хабре, то вообще замечательно.

В завершении вынужден повторить банальность: если вдруг вам что-то потребовалось от SObjectizer-а, а вы этого там не нашли, то дайте знать. Мы не сможем сделать то, о чем даже и не подозреваем. А вот если нам сказать, то кто знает. Может года через три поймаем очередное "озарение" ;)

пятница, 1 ноября 2024 г.

[life.cinema] Очередной кинообзор (2024/10)

Подошло время очередного кинообзора. Традиционно в начале каждого из списков идет то, что понравилось больше, а в конце -- то, что понравилось меньше или же совсем не понравилось.

Фильмы

Дэдпул и Росомаха (Deadpool & Wolverine, 2024). Если понравились первые две части Дэдпула, то нужно обязательно смотреть и третью. Правда, в какой-то момент плотность юмора превысила мои способности к восприятию происходящего, так что, боюсь, многое прошло мимо меня. Если же вы хорошо помните персонажей из вселенных "Людей X" и "Мстителей", то наверняка заметите гораздо больше отсылок и подколок, чем получилось у меня.

Ограбление (Napad, 2024). В общем-то неплохо, мне зашло. Напомнило фильмы в духе соцреализма. Хотя к сюжету в нескольких местах есть вопросы, но если не придираться, то в общем-то неплохо.

Ячейка 234 (Unit 234, 2024). Бюджетненько, но вполне себе смотрибельно.

Игра киллера (The Killer's Game, 2024). Отличный аттракцион чтобы отключить мозги. Но если вам не нравится жанр абсолютно несерьезных комедийных боевиков, то лучше воздержаться.

Спящая (La mujer dormida, 2024). Смотреть было интересно, но развязка фильма откровенно разочаровала.

Револьвер (Ribolbeo, 2024). Откровенно слабо. В принципе, можно и пройти мимо.

Не говори никому (Speak No Evil, 2024). Мне не зашло совершенно, не возникло ощущение реальности происходящего. Хотя отметить отличную игру Джеймса Макэвоя нужно, такое впечатление, что он там единственный органично смотрелся.

Субстанция (The Substance, 2024). Не понравилось. Потенциально хорошую идею превратили в какую-то муть, а финал фильма -- откровенный треш. В общем, смотреть противно, а потраченного на просмотр времени жаль.

Сериалы

13 клиническая (первый сезон, 2022). Мне вот прям зашло. Посмотрел все восемь серий первого сезона за два дня на одном дыхании.

Медленные лошади (четвертый сезон, 2024). Самый слабый из всех сезонов. Если первые три вам понравились, то можно глянуть просто для того, чтобы посмотреть что происходит с героями. Если же первые сезоны не впечатлили, то смело можно не смотреть. Как по мне, так если бы не Гари Олдман, то этот сезон был бы откровенным говном.

Идеальная пара (The Perfect Couple, первый сезон, 2024). Красиво снятая неинтересная история с никакой детективной составляющей. Такое впечатление, что основной задачей было показать, что за внешним лоском скрывается куча скелетов в шкафах. Это-то показали, но сделали это так, что жалко потраченного на просмотр сериала времени.

Кино вне категории

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

четверг, 24 октября 2024 г.

[prog.c++] Впечатления от Google-овского sparsetable

Давеча довелось применить sparsetable из Google-овского sparsehash для представления разреженного вектора значений. Разреженный вектор в моем случае -- это вектор с обычной индексацией от 0 до N, где части элементов нет физически. Обращения к элементам идут именно по целочисленным индексам (к тому же индексы в моём случае были представлены std::uint32_t).

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

Тогда как sparsetable показал себя здесь очень и очень хорошо.

Причем речь идет именно о классе sparsetable, который хоть и публичный в библиотеке sparsehash, но все-таки выглядит как вспомогательный внутренний класс, использующийся в реализации sparse_hash_map и dense_hash_map. Т.е. это именно что специальная структура данных для представления разреженного вектора, а не какой-то из вариантов хэш-таблицы.

При том, что класс sparsetable отлично зарекомендовал себя именно как специализированная структура данных, не могу не отметить несколько неприятных особенностей реализации sparsetable, обусловленных как старым C++ стандартом, в рамках которого написан sparsetable (а это C++03), так и подходом к разработке софта в Google.

Зафиксирую здесь те из них, которые я нахожу принципиально важными. Очень надеюсь, что найду время и силы сделать форк реализации sparsetable, в котором постараюсь от этих проблем избавится. Но, к сожалению, не уверен получится ли.

Итак, поехали.

Поскольку sparsetable написан для C++03, то он не поддерживает move semantic. Вообще. Причем как для самого sparsetable -- там нет ни конструктора, ни оператора перемещения, хотя swap для sparsetable есть. Но, самое плохое, sparsetable не поддерживает movable-only типы, вроде std::unique_ptr. Т.е. из коробки не получится хранить в sparsetable экземпляры std::unique_ptr 🙁

Еще sparsetable не поддерживает strong exception safety. Там прямо в комментариях в коде написано, что следовало бы реализовать обработку ситуации, когда конструктор копирования для помещаемого в контейнер объекта бросает исключение. Если такое произойдет, то контейнер останется в хз каком состоянии.

И с exception safety там есть еще один прикольный момент: дело в том, что sparsetable выполняет переаллокацию памяти даже при удалении элемента. Т.е. да, вы вызываете erase, внутри erase sparsetable решает заменить блок из которого элемент изъят блоком меньшего размера, для чего обращается к аллокатору... А аллокатор, в принципе, может бросить исключение. Т.е. из erase может вылететь bad_alloc 🙁 Что неприятно, т.к. время от времени я пишу код, в котором для выполнения отката ранее внесенных изменений при исключении делаются erase для возврата модифицированных контейнеров к исходному виду. Представьте себе что в блоке catch или даже в деструкторе объекта вы вызываете erase для sparsetable, а он бросает bad_alloc 🥴

Ну и работа с памятью там имеет одну важную особенность. По умолчанию sparsetable использует собственный аллокатор под названием libc_allocator_with_realloc. Этот аллокатор, как следует из его названия, полагается на Си-шный realloc при переаллокации блоков данных. Но вот если std::realloc возвращает NULL, то sparsetable просто... прерывает работу программы через вызов std::exit. Понятно, что это особенности Google-овского софта, под нужды которого sparsetable и писался, но блин, представьте себе, что у вас сервер, который держит в ОП информацию о десятках тысяч текущий сессий. И вдруг весь этот сервер одномоментно падает из-за того, что при обработке запроса в рамках одной из сессий не удалось переаллоцировать блок в одном sparsetable. Как-то печально, как по мне 🙁

Плюс к тому исходный sparsetable содержит методы для какой-то странной сериализации/десериализации. Полагаю, что в Google от sparsetable это требовалось. Но зачем это всем остальным -- хз.

И совсем отдельная история -- это отношение sparsetable к несуществующим элементам. Так, константная версия operator[] возвращает константную ссылку. В том числе и на несуществующие элементы! Если значения по индексу i нет, то operator[] вернет константную ссылку на статический объект типа T. Что, помимо прочего, означает, что тип T должен быть default-constructible.

Из-за того, что sparsetable выдает несуществующие элементы за существующие, но с общим дефолтным значением, проистекает и то, что у sparsetable есть два типа итераторов. Обычный итератор, который возвращается посредством begin/end, и который итерируется, в том числе, и по несуществующим элементам. И специальный nonempty-итератор + специальные методы nonempty_begin/nonempty_end, которые следует использовать, если хочется пробежаться только по существующим элементам.

Могу предположить, что подобные заморочки с имитацией несуществующих элементов через общее дефолтное значение были полезны для реализации хэш-таблиц на базе sparsetable. Но если использовать sparsetable отдельно, то выглядит это несколько странно (мягко говоря 🤨)


В общем, под нужды заказчика sparsetable из sparsehash был вытащен, избавлен от части ненужного хлама, слегка доработан напильником для устранения проблем с компиляцией под C++20 и использования std::allocator. Но до глубокой модернизации sparsetable руки так и не дошли. Однако желание сделать это осталось. Буду надеятся, что это получится сделать.


Если кто-то из читателей никогда не слышал про sparsetable, то впечатление можно составить вот из этого описания: Implementation of sparse_hash_map, dense_hash_map, and sparsetable. Там первая часть как раз sparsetable и посвящена. Описание лаконичное, но толковое.

пятница, 18 октября 2024 г.

[prog.flame] Адекватен ли выбор языка программирования для такой задачи (Rust для Radicle)?

Часто говорят, что язык программирования нужно подбирать под задачу. Мол, не нужно брать C++ для того, что легко делается на Python. И не нужно брать Python там, где потребуется что-то более быстрое и менее ресурсоемкое.

Недавно узнал о Radicle -- это что-то вроде GitLab и Gitea, т.е. инструмент для совместной работы над программным кодом.

Radicle написан на Rust-е.

Не на Python-е. Не на Ruby. Не на Node.js. Не на Java или C#. Не на Go. А на Rust-е.

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

Посему лично я в недоумении и сам бы для проекта, подобного Radicle, точно бы не стал брать C++ или Rust. Как по мне, так здесь достаточно языка со сборкой мусора: Java, Kotlin, C#, возможно даже Go (простите, динамика в лице Python/Ruby/JavaScript идет лесом, т.к. не из тех мазохистов, которые делают что-то большое на динамически-типизированных языках).

А вот брать нативный язык без GC с претензией на околосистемность... Зачем?

Собственно, непонимание и привело к появлению этого поста. Надеюсь, в комментариях мне напихают в панамку и объяснят насколько я дебил и отстал от современных трендов. Ибо я реально не понимаю, почему в 2020-х нужно отказываться от всего, что накопилось в мире безопасных языков с GC и трахать себе мозг Rust-ом в задаче, где не нужна ни супер-пупер производительность, ни супер-пупер экономия ресурсов, ни тотальный контроль за происходящим.


Если не лень, то напишите в комментариях, плиз, какой бы вы язык программирования предпочли для реализации проекта по типу Radicle/GitLab/Gitea. Я бы лично выбирал бы между C#, Kotlin и Go (хотя не на одном из них не программировал), но точно бы не C++ и не Rust.

вторник, 15 октября 2024 г.

[prog.c++] Никогда не задумывался о таком поведении С++ в случае адресов вложенных объектов, а ведь оно логично

Давайте посмотрим на структуру данных B:

struct A {};

struct B : public A {
    A _first;
};

Какой у нее размер? С учетом того, что даже у пустой структуры размер будет, как минимум, 1 байт. И что в C++ есть empty base class optimization.

То, что B наследуется от пустого A благодаря empty base class optimization не увеличивает размер B. Т.е. размер B будет определяться размером поля B::_first, а это один байт. Следовательно, sizeof(B) должен быть равен единице.

На самом деле нет.

Дело в адресах для объекта B и для его поля B::_first:

B b;
A * p1 = &b; // Это легально т.к. B есть A благодаря наследованию.
A * p2 = &b._first;

assert(p1 != p2);

Объект типа B является объектом типа A из-за наследования. Соответственно, адрес объекта типа B будет и адресом объекта типа A.

Внутри типа B есть еще один объект типа A. И у него так же есть свой адрес.

При этом объект B и его поле B::_first -- это два разных объекта типа A.

А в C++ у двух разных объектов одного типа не может быть одинаковых адресов.

Поэтому в показанном примере компилятор не может применить для типа B оптимизацию пустой базы, что и добавляет специальное выравнивание для поля B::_first, чтобы эти адреса оказались разными. Тем самым увеличивая размер B.

Убедиться в этом можно на wandbox - цынк

Информация об этой особенности C++ найдена здесь.

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

пятница, 11 октября 2024 г.

[life.work] 30 лет профессионального программизма

Как же все-таки летит время. Вроде бы недавно публиковал аналогичный пост, но про 25 лет, а тут хоба! И уже 30.

Ну а так-то да, где-то в октябре 1994-го мне сделали трудовую книжку и я стал считаться профессиональным программистом. Писать программки начал пораньше, года с 1990-го, но именно за зарплату только с 1994-го.

Многое из того, что можно было бы сказать по этому поводу, уже было написано пять лет назад. Попробую добавить еще (не)много.

среда, 9 октября 2024 г.

[prog.data.structures] В склерозник: ссылки на тему prefix/radix tree

Довелось недавно столкнуться с темой prefix/radix деревьев. По ходу изучения нашел несколько ссылок, которые для меня оказались полезными, поэтому зафиксирую их в склерозник, чтобы было проще найти в будущем.

Даю только ссылки, без пространных пояснений, уж извините меня за халтуру, но так проще.

Adaptive Radix Tree. Собственно, основная PDF-ка, с которой и следует начинать.

HAT-trie, a cache-conscious trie. Обзор сразу нескольких подходов к реализации префиксных деревьев и хеш-таблицы с переходом к рассказу об их комбинации HAT-trie. Плюс реализация этой идеи на C++.

Crit-bit trees и qp tries and crit-bit tries. Еще несколько вариаций на тему префиксных деревьев.

Akamai Radix Tree. Реализация на C++ radix tree от Akamai. Очень интересно если хочется посмотреть, как это дело можно реализовать. Хотя оформлена библиотека (в плане описания, документации, примеров и комментариев в коде), как по мне, бестолково. Поэтому разбираться не просто + реализация на многоэтажных шаблонах, так что требуется хорошее знание C++.

Compressing dictionaries with a DAWG и Directed Acyclic Word Graphs. Пара статей вокруг идей о том, как можно сократить объем данных в префиксном дереве.

Ну и вот эта ссылка регулярно всплывала, хотя напрямую к теме, вроде бы, не относится: Implementation of sparse_hash_map, dense_hash_map, and sparsetable.

От себя добавлю, что для сценария, в котором мне потребовалось что-то вроде prefix/radix дерево, "классический" вариант префиксного дерева вел к слишком большому расходу памяти. И, как я понял, это характерно для подобного рода деревьев, поэтому как раз и есть статьи о том, как уменьшить объем данных.

пятница, 4 октября 2024 г.

[prog.c++] Использование одного аргумента шаблона (aka Traits) вместо нескольких

В догонку ко вчерашнему посту про недостающую функциональность в std::vector.

Понятное дело, что std::vector уже не сможет получить каких-то дополнительных шаблонных аргументов, потому что это поломает кучу существующего кода.

Кстати говоря, есть у меня ощущение, что в любом C++ном проекте, который писали обычные люди, вроде меня, а не монстры, вроде Девида Вандервуда или Барри Ревзина, куча кода поломается, если в std::vector начнут использовать собственные аллокаторы. Поломается потому, что код написан в стиле:

void do_something(const std::vector<int> & data) {...}

И даже шаблонный код написан вот так:

template<typename T>
void do_something(const std::vector<T> & data) {...}

а не вот так (хотя бы вот так):

template<typename T, template Allocator>
void do_something(const std::vector<T, Allocator> & data) {...}

Впрочем, это уже совсем другая история...

Но если пофантазировать?

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

Это подход на базе Traits. Уже не помню, откуда про него узнал, не удивлюсь, если из книг Александреску. Но подход уже старый и мы, например, применяем его в RESTinio.

Суть в том, что шаблон параметризуется отдельным типом Traits, внутри которого уже сосредоточена все остальная нужная шаблону информация.

Например, для std::vector это могло бы выглядеть так:

template<typename T>
struct default_vector_traits {
  using allocator = std::allocator<T>;
};

template<typename T, typename Traits = default_vector_traits<T> >
class vector {...};

И если бы со временем нам бы потребовалось добавить в шаблон std::vector еще один параметр (тот же нужный мне growth_policy), то это можно было бы сделать не меняя списка параметров для std::vector:

struct default_vector_growth_policy {
  std::size_t operator()(std::size_t current_capacity) const {
    // Код примерный, прошу помидорами не бросаться ;)
    return (current_capacity > 1 ? static_cast<std::size_t>(capacity * 1.5) : 2);
  }
};

template<typename T>
struct default_vector_traits {
  using allocator = std::allocator<T>;
  growth_policy = default_vector_growth_policy;
};

template<typename T, typename Traits = default_vector_traits<T> >
class vector {...};

И если бы мне захотелось использовать с векторами собственную политику роста емкости, то мне бы потребовалось всего лишь:

struct my_growth_policy {
  std::size_t operator()(std::size_t current_capacity) const {...}
};

template<typename T>
struct my_vector_traits : public std::default_vector_traits<T> {
  using growth_policy = my_growth_policy;
};

using my_int_vector = std::vector<int, my_vector_traits<T>>;

Понятное дело, что шаблоны классов контейнеров из стандартной библиотеки на Traits уже не перевести. Но если вы пишите свои библиотеки шаблонных классов (особенно собственных типов контейнеров, неприведихоспади!), то имеет смысл подумать о применении подхода с Traits.

четверг, 3 октября 2024 г.

[prog.c++] В std::vector не хватает вот какой штуки...

Интересно, много ли C++ программистов задумывается о том, а как растет std::vector, когда мы в него добавляем элементы через push_back и не имеем возможности сделать предварительно reserve?

А ведь рост размера std::vector может существенным образом повлиять на расход памяти.

Предположим, что реализация std::vector в вашей стандартной библиотеке использует коэффициент 1.5 для увеличения размера вектора. Т.е. вы делаете push_back в полный вектор и опа! Ваш вектор увеличился в полтора раза. Была емкость на 1000 элементов, а стала на 1500. А использоваться оттуда будет 1001.

А если реализация стандартной библиотеки использует коэффициент 2, то дела еще хуже.

Сильно негативно это начинает проявляться на векторах размером в миллион-два-три и т.д. Если у вас в программе таких векторов не один и не два, то вы с удивлением для себя сможете обнаружить, что в этих векторах где-то 1/5 или 1/4, а то и 1/3 объема не занято. Что в сумме может дать десятки мегабайт впустую потраченной памяти.

Чтобы этого не происходило приходится вручную контролировать размер и емкость вектора и, опять же, вручную дергать reserve перед push_back-ами.

А то, что делается вручную, легко забыть или же сделать неправильно 😣

Поэтому мне лично не хватает возможности задать для std::vector какую-то собственную политику роста. Что-то типа:

struct my_growth_policy {
  [[nodiscard]] std::size_t operator()(std::size_t current_capacity) {
    return ... // здесь какие-то вычисления.
  }
};

using my_vector = std::vector<my_data, std::allocator<my_data>, my_growth_policy>;

Тогда не пришлось бы вручную контролировать емкость перед каждым push_back-ом.

Но, боюсь, в стандартном std::vector такого не будет никогда.

среда, 2 октября 2024 г.

[life.cinema] О фильмах про "Чужого"

Можно сказать, что пост в догонку к недавнему кинообзору, в котором был упомянут очередной фильм из вселенной "Чужих".

Мне очень нравятся первые три фильма, каждый из них шедеврален. Хотя первый "Чужой" на мой взгляд, самый слабый из них. Но тут обязательно нужно делать поправку на время, в которое он был снят. Полагаю, в конце 1970-х сложно было сделать лучше. А вот "Чужих" и "Чужой-3" пересматриваю регулярно. Причем чем старше становлюсь, тем больше мне нравится именно третья часть.

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

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

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

Недавний "Ромул" вроде как возвращается к истокам. Но мне он кажется вторичным, поскольку подобное мы уже видели в четвертом фильме. Даже антропоморфный мутант в конце, с которым пришлось бороться в последние мгновения перед возможной катастрофой.

Не знаю, будут ли еще продолжения, полагаю, что будут. Вопрос только когда.

И, предположу, что там обязательно главным борцом с очередным чужим окажется девушка или молодая женщина. Ну такой сильный женский персонаж, который все превозмогёт и всех победит. Как же может быть иначе? ;) Что делает потенциально продолжение немного предсказуемым.

Еще, как бы это странно не звучало, мне зашел "Чужой против Хищника". Конечно же кино специфическое и по своему уровню ему далеко как до первых трех "Чужих", так и до первого "Хищника". Но зато качественно сделанное. Как мне показалось с любовью и уважением к двум исходным франшизам. Жаль что вторая часть этого противостояния оказалась отстоем, хотя этого и можно было ожидать.

вторник, 1 октября 2024 г.

[life.cinema] Очередной кинообзор (2024/09)

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

Фильмы

Чужой: Ромул (Alien: Romulus, 2024). Ну хоть отказались от прямого продолжения маразматической линии "Прометея" и "Завета", хотя и есть отсылки к этим двум говношедеврам. В целом мне показался похожим на четвертую часть. И не понравился он мне тем же, чем и четвертая часть -- очередной попыткой скрестить чужого и человека и получить неведомого мутанта. В целом же любители вселенной "Чужого" вполне могут посмотреть, это явно лучше "Прометея" и "Завета".

Восхитительно! (Délicieux, 2021). Этот фильм можно посмотреть просто ради красивой картинки. Временами работа оператора выше всяких похвал, хочется поставить воспроизведение на паузу и рассматривать кадр как картину. Сама же история простенькая и, как мне кажется, откровенно сказочная.

Ускорение (Slingshot, 2024). Хорошо сделанное кино. Но не могу сказать, что мне понравилось, т.к. с некоторых пор не люблю открытые финалы и недосказанность.

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

Ловушка (Trap, 2024). Отличная игра Джоша Хартнетта в фильме, где все происходящее выглядит каким-то откровенным маразмом.

Мистер Блейк к вашим услугам (Complètement cramé, 2023). Не понял что это было. Возможно, я еще слишком молодой, а кино рассчитано на 65-75 летних зрителей. Но выглядит это все как ужасно рафинированная сказочка-сказочка, в которой нет ни одного нормального персонажа, не говоря уже о хоть каком-либо отношении к реальности.

Ворон (The Crow, 2024). Затянуто и скучно.

Оставь мир позади (Leave the World Behind, 2023). Редкая муть. Посмотреть можно разве что если вам почему-то захотелось выкинуть два часа из своей жизни.

Воровка (2023). Халтурная халтура. Смело можно проходить мимо.

Сериалы

Охотничьи псы (Sayanggaedeul, первый сезон, 2023). Я бы сказал, что это сериал для подростков, настолько глуповаты там главные герои. Но для подросткового кино оно излишне жесткое местами. Так что даже не знаю как классифицировать: для взрослых слишком наивное, для детей слишком кровавое.

Точка ноль (первый сезон, 2024). Сплошное разочарование. Ожидал напряженного фильма про борьбу с неизвестным вирусом, но как раз ни напряжения, ни борьбы, ни ожидания глобального коллапса из-за вырвавшейся на свободу болезни в сериале не обнаружилось. Зато рассказали про сына-алкоголика зав.лаборатории, про бывшего любовника главной героини, про отголоски каких-то разборок из 90-х годов и пр. муть. В общем, категорически не рекомендую.

Не смог досмотреть

Властелины воздуха (Masters of the Air, первый сезон, 2024). Снято, конечно же, красиво. Вероятно сцены воздушных боев сделаны мастерски с максимальным сохранением исторических деталей и стремлением к достоверности. Но вот не торкает и все. Скучно и ничего с этим не смог поделать. С трудом дождался завершения второй серии и решил не продолжать.

План Б (Plan B, первый сезон, 2024). Когда во второй серии окончательно прояснилось вокруг какой идеи крутится все происходящее на экране, то стало неинтересно. А когда в третьей серии начались какие-то странные действия со стороны персонажей и приплюсовалась современная толерантная повесточка, то вообще желание продолжать просмотр пропало окончательно.

Криминальное прошлое (Criminal Record, первый сезон, 2024). Сперва раздражал откровенные непрофессионализм главной героини, а потом еще и возникло ощущение, что конфликты между некоторыми героями создают искусственно и на ровном месте, без должной мотивации. Плюс традиционное для сериалов неспешное развитие событий. В общем, в какой-то момент возникло желание прекратить просмотр и забыть про это "кино".

понедельник, 23 сентября 2024 г.

[prog.c++] Десять лет библиотеке timertt

Десять лет назад в свет вышла небольшая библиотека timer thread template, она же timertt. Делалась она чтобы иметь возможность создавать в приложении большое количество одноразовых и/или периодических таймеров. Когда я говорю про "большое", то речь идет о сотнях тысяч, как минимум.

Сделать собственные средства для поддержки таймеров заставила жизнь. Долгое время в SObjectizer-е применялись таймеры из замечательной библиотеки ACE. И не только таймеры. Много лет ACE для нас служила базовым слоем, мы брали оттуда и мутексы, и сокеты, и даже хэш-таблицы, поскольку ничего из этого в C++98 не было.

Однако, в SObjectizer-5 мы заложились сразу на C++11, в котором многое нашлось в стандартной библиотеке. Кроме того, после выхода SObjectizer-а в "свободное плавание" нам пришлось отказаться от развития ряда построенных над SObjectizer-ом библиотек, так что нам больше не нужны были сокеты и инструменты для ручной загрузки-выгрузки динамических библиотек.

В итоге, к середине 2014-го года единственное, что нас привязывало к ACE, -- это таймеры. Которые, как по мне, в ACE были сделаны очень здорово. И мы оказались в ситуации, когда небольшой SObjectizer, архив которого "весили" ~600Kb, требовал внешней зависимости размером порядка 6Mb в архиве. И нужна нам ACE была только для таймеров 😣

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

Если мне не отшибает склероз, на все про все ушло порядка месяца. В конце августа 2014-го работа началась, 4-го сентября 2014-го был сделан первый коммит, 18-го сентября 2014-го был зафиксирован первый стабильный тег.

timertt изначально создавалась под SObjectizer, хотя к самому SObjectizer-у она и не привязана, тут полностью обратная ситуация, можно сказать, что без timertt не было бы того SObjectizer-5, каким мы его знаем сегодня. Но т.к. timertt писалась под SObjectizer, то отдельно мы ее мало пиарили. Было несколько анонсов то здесь, то там, но не более того. Самый эпичный анонс случился на LOR-е, сейчас перечитываешь и остатки волос непроизвольно шевелятся... 😂

Что меня до сих пор удивляет в timertt, так это тот факт, что она очень медленно обрастает какой-то новой функциональностью. А с 2019-го туда вообще ничего не добавляется. Работает себе и работает. Редкий в моей практике случай, обычно если что-то пошло в дело, то новые хотелки возникают регулярно, да и какие-то прятавшиеся баги время от времени вылезают наружу. Тот же SObjectizer постоянно расширяется и для RESTinio есть ряд нереализованных хотелок (но тупо не хватает времени на эти работы). А вот timertt просто работает и каких-то новых пожеланий к ней нет. Это-то как раз и удивляет больше всего. Никогда прежде такого не было, а вот поди ж ты.

В завершение хочется пожелать и самому себе, и библиотеке timertt, и читателям блога вернуться к продолжению этой истории в этом же блоге в сентябре 2034-го 😁


Что-то круглые даты как-то кучно пошли. И это еще не последняя, в начале октября ожидается еще одна ;)

суббота, 21 сентября 2024 г.

[prog.memories] Vim, Ruby, Mxx_ru -- двадцать лет в пути...

Когда-то давно, в сентябре 2009-го года здесь появилась первая заметка про мое знакомство с ViM, Ruby и рождение Mxx_ru: ViM, Ruby, Mxx_ru – пять лет в пути! Часть первая: Mxx_ru (вот вторая часть). Спустя пять лет вышло продолжение истории: Vim, Ruby, Mxx_ru -- десять лет в пути... Затем прошло еще пять лет и была написана заметка Vim, Ruby, Mxx_ru -- пятнадцать лет в пути... И вот, спустя еще пять лет, можно опубликовать очередную часть этой истории 🤓


Как бы это смешно не звучало, ViM продолжает оставаться моим основным инструментом.

При этом не могу сказать, что я чему-то еще научился за прошедшие пять лет (при этом пять лет назад знал и умел совсем немного). Продолжаю довольствоваться малым и использую ViM в минималистичном, можно сказать, базовом варианте. Под катом мой .vimrc, желающие могут посмотреть если это вдруг кому-то интересно.

В таком минимализме для меня, в общем-то, и кроется весь смысл ViM-а: я оказываюсь практически в привычном окружении на любой новой системе с минимальными телодвижениями. Это как раз то, что мне и нужно.

Про ViM могу рассказать показательную историю. В конце прошлого года подключился к проекту, в котором пока основная часть разработки ведется под Windows (с эпизодическими профилактическими сборками под Linux). Основная среда -- VisualStudio, даже сам проект был оформлен в виде .sln-файла. И, что самое важное, разработка ведется на сервере заказчика, доступ к которому дается через RDC.

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

Пару месяцев терпел, но в конце-концов не выдержал. Установил ViM и начал писать код в привычном для себя окружении, а компилировался в командной строке вызывая devenv с передачей ему .sln-файла и нужных параметров. Можно сказать, вернулся в родной мир.

Так что ViM-ом продолжаю с удовольствием пользоваться. А посему могу смело сказать: 20 years with Vim and still learning :) And happy Vimmming!


Ruby практически не использую в последнее время. Нет необходимости. Уж не знаю хорошо ли это или нет, но вот не нужен он пока. Из-за чего уже основательно подзабыл этот отличный динамически-типизированный язык.

Пару лет назад в одном из проектов довелось столкнуться с Python-ом. Мне не нужно было на Python-е программировать самому, требовалось встроить его в C++ приложение, но все равно пришлось почитать что это такое и погрузится в некоторые его особенности. Порадовался тому, что в свое время выбрал Ruby, а не Python. Как по мне, так Ruby делали как инструмент для программистов, тогда как Python -- для <censored> не умеющих программировать, мягко говоря. И, судя по тому, какое распространение Python получил за прошедшие годы, умеющих программировать больше не стало 😏


Mxx_ru продолжаю использовать при разработке SObjectizer и json_dto. И кайфую от этого. Но это прекрасное время уже скоро закончится... 🥹

В прошлом году в RESTinio-0.7 мы уже полностью перешли с Mxx_ru на CMake.

Полагаю, что в 2025-ом или в 2026-ом, при переводе SObjectizer-а на C++20 (а может быть и сразу на C++23) мы также откажемся от Mxx_ru в пользу CMake. А там и очередь json_dto подойдет.

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

Тем более, что комитет по стандартизации C++ в очередной раз поднасрал C++никам внедрив в язык настолько говенную мудрёную систему модулей, что разработчики компиляторов все никак не могут допилить ее полноценную поддержку. Но допилят рано или поздно. А когда допилят, то окажется, что все наколеночные системы сборки (вроде нашей Mxx_ru) превратятся в тыкву: либо обновляйся и добавляй поддержку того, что придумали сумрачные гении из комитета, либо переходи на что-то другое.

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


Наверное, как-то грустно получилось. Но, с другой стороны, отрадно, что сама по себе история продлилась целых двадцать лет. Так что все хорошо, нет повода расстраиваться. Даже интересно, смоглу ли написать что-то на эту тему спустя еще пять лет. Будем посмотреть 😉

понедельник, 16 сентября 2024 г.

[prog.c++] Еще несколько ссылок вдогонку к теме std::launder/std::start_lifetime_as

В качестве продолжения темы std::launder/std::start_lifetime_as зафиксирую в склерознике еще несколько полезных ссылок.

Первая -- это статья "Reinterpet_cast, UB and a Pointer Type Casting in C++17" в которой еще раз разбирается тема безопасного преобразования указателей и приводится пример кода, который типа делает это правильно и без UB для C++17.

Вторая -- это доклад Джонатана Мюллера на C++Now 2024: A Deep Dive Into C++ Object Lifetimes. Сам я не осилил слушать полтора часа на английском, поэтому для меня очень полезными оказались слайды к этому докладу: PDF. И вот эти вот слайды я настоятельно рекомендую всем, кто интересуется данной темой. Хотя бы просто для того, чтобы убедится насколько глубока кроличья нора.

понедельник, 9 сентября 2024 г.

[prog.c++] Сюрприз с размером производного класса в разных компиляторах

С размером вот этой простой структуры на x64 при выравнивании по умолчанию все понятно:

struct base {
    void * m_ptr;
    short m_sh;
    char m_ch;
};

Вполне ожидаемо имеем 16 байт. Из которых реально используются 11, а остальное -- это выравнивание на границу 8 байт (т.к. указатель на 64-х битовых платформах занимает 8 байт).

А вот что с размером вот этой не менее простой структуры?

struct derived : public base {
    char m_mask;
};

А вот тут сюрприз!

Результат зависит от компилятора: для VC++ получается 24 байта (и в этом есть некоторая логика), тогда как для GCC и clang -- 16 байт (и в этом также есть логика).

Но лично мне представляется, что логика GCC/clang как-то логичнее. Тогда как поведение VC++ стало неприятным открытием. Можно даже сказать как серпом...

Посмотреть самому можно на godbolt: https://godbolt.org/z/sbo8jPcxE

пятница, 6 сентября 2024 г.

[prog.c++] В склерозник: красивый кусок кода для подсчета смещения полей объекта и их выравнивания

В LinkedIn встретил ссылку на реализацию тупла без использования рекурсии: тыц. Реализация активно эксплуатирует C++ный fold expression и std::integer_sequence. Вот прям отличная демонстрация того, как эти фичи могут (и должны?) применяться.

Еще очень удачно совпало, что ссылка эта попала мне на глаза вскоре после того, как я проделал в чем-то похожую работу. Правда, у меня ситуация была чуть сложнее, ведь в тупле все N полей присутствуют всегда, поэтому размер всех туплов одного типа одинаков и фиксирован. Тогда как в моем случае объекты могут состоять из разного набора полей, поэтому нужно размер каждого объекта определять индивидуально, да и расположение полей в каждом объекте может быть уникальным. Так что в своей реализации без метапрограммирования на базе рекурсии я не смог обойтись. Возможно, еще и потому, что у меня мало опыта с fold expression и std::integer_sequence.

Поэтому решил утащить к себе в склерозник кусочек чужого кода на память. Чтобы легче было найти когда потребуется.

Этот кусочек кода отвечает за подсчет расположения полей внутри объекта с учетом их правильного выравнивания. В результате своей работы функция calculate_positions возвращает std::array размером (N+1), где элементы 0..(N-1) содержат смещение i-го поля, а элемент N -- общий размер объекта.

Оригинал кода можно увидеть здесь, а вот чуть-чуть модифицированная мной версия:

template <class T>
struct PositionWrapper {};

template <std::size_t _last, std::size_t... _is>
struct Positions
{
  static consteval auto to_array() {
    return std::array<std::size_tsizeof...(_is) + 1>{_is..., _last};      
  }
};

template <class T, std::size_t _last, std::size_t... _is>
consteval auto operator+(
  const Positions<_last, _is...>& /*_sizes*/,
  const PositionWrapper<T>& /*_w*/)
{
  if constexpr (_last % alignof(T) == 0) {
    constexpr auto last_new = _last + sizeof(T);
    return Positions<last_new, _is..., _last>{};
  } else {
    constexpr auto last_corrected = (_last / alignof(T) + 1) * alignof(T);
    constexpr auto last_new = last_corrected + sizeof(T);
    return Positions<last_new, _is..., last_corrected>{};
  }
}

template <class... Types>
consteval auto calculate_positions() {
  return (Positions<0>{} + ... + PositionWrapper<Types>{}).to_array();
}

понедельник, 2 сентября 2024 г.

[prog.c++] Конструирование объекта в котором физически могут отсутствовать некоторые поля

На прошлой неделе делал интересную штуку: С++ный объект, в котором могут физически отсутствовать некоторые части.

Пришлось заняться этим потому что в текущем проекте было большое количество объектов вида:

struct data {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  std::vector<first_opt_attribute_type> m_first_type_attrs;
  std::vector<second_opt_attribute_type> m_second_type_attrs;
  std::vector<third_opt_attribute_type> m_third_type_attrs;
};

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

Объектов типа `data` было много, в некоторых случаях десятки миллионов. И на таком количестве хранение пустых std::vector внутри миллионов объектов типа `data` оказывается очень расточительным (ведь каждый пустой std::vector -- это, как минимум, 24 байта -- size, capacity + указатель на блок с данными).

Объекты же `data` создавались в динамической памяти и ссылки на них хранились как std::unique_ptr.

Причем модель данных не позволяла легко вынести значения опциональных атрибутов из `data` куда-то еще. Например, можно было бы попробовать завести какое-то общее хранилище атрибутов, а в самом `data` тогда хранилась бы ссылка (или индекс) в этом хранилище. Тогда если у объекта атрибутов нет, то в хранилище для объекта ничего нет, а в самом объекте лежит нулевая ссылка. Ну т.е. попробовать бы можно было, но без особых шансов на успех, но зато с большим геморроем 🙁

В общем, хотелось бы, чтобы поля m_first_type_attrs, m_second_type_attrs и m_third_type_attrs таки оставались внутри `data`, но ничего бы не потребляли, если были пустыми.

Здесь бы очень ко двору пришлись бы массивы вроде чего-то такого:

struct data {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  std::size_t m_first_type_attrs_count;
  first_opt_attribute_type m_first_type_attrs[m_first_type_attrs_count];

  std::size_t m_second_type_attrs_count;
  second_opt_attribute_type m_second_type_attrs[m_second_type_attrs_count];

  std::size_t m_third_type_attrs_count;
  third_opt_attribute_type m_third_type_attrs[m_third_type_attrs_count];
};

Но в C++ таких массивов нет. Это во-первых. А во-вторых, даже такой способ хранения, будь он возможен, все равно был бы расточительным. Ведь если для объекта нет атрибутов, то поля *_attrs_count с нулевыми значениями в нем все равно есть. Три поля std::size_t -- это 24 байта, умножаем на десяток миллионов объектов `data` и теряем пару десятков мегабайт на ровном месте 🙁

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

Получилось что-то вроде такого:

// Описание части, которая в объекте присутствует всегда.
struct data_header {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  // Вспомогательные типы для идентификации опциональных полей.
  struct first_attr_tag {};
  struct second_attr_tag {};
  struct third_attr_tag {};

  // Типы опциональных полей.
  using first_attr_vec = compound::vec<first_attr_tag, first_opt_attribute_type>;
  using second_attr_vec = compound::vec<second_attr_tag, second_opt_attribute_type>;
  using third_attr_vec = compound::vec<third_attr_tag, third_opt_attribute_type>;
};

Создается такой объект специальной функцией-фабрикой. Ей на вход подаются начальные значения опциональных полей. Если для какого-то опционального поля значения не заданы, то этого опционального поля и нет.

std::vector<second_opt_attribute_type> attrs{...};

auto my_data = data::build(
  // Данные для инициализации фиксированной части должны быть всегда.
  data_header{...},
  // А вот опциональные данные нужны только те, которые в объекте присутствуют.
  data::second_attr_vec::move_from(attrs));

// Доступ к полям фиксированной части возможен напрямую.
std::cout << my_data->m_one << std::endl;
std::cout << my_data->m_two << std::endl;

// Доступ к опциональным полям нужно проверять.
if(my_data->has_field<data::second_attr_tag>()) {
  // Поле есть, можно с ним работать.
  for(const auto & a : m_data->get_field_ref<data::second_attr_tag>()) {
    ...
  }
}

Не буду вдаваться в подробности, т.к. все это делалось в закрытом проекте. Но скажу, что весь фокус тут в функции-фабрике build, которая вычисляет сколько же места потребуется (с учетом необходимых выравниваний) и создает обычный динамический массив std::byte. А уже дальше внутри этого массива посредством placement new размещается все то, что в объекте должно присутствовать.

Для меня реализация этой кухни оказалась на грани моих знаний C++ и возможностей как программиста, где-то, наверное, даже за гранью. Но как-то все это заработало 🤓

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

Что напрягало, как это функциональный стиль C++ного метапрограммирования (работа велась в рамках C++20). Вся эта рекурсия по спискам типов... 😓 Если сталкиваешься с этим самым метапрограммированием раз в пару лет, то непросто на эти рекурсии перестроится.

Еще, конечно же, доставляла тема с std::launder. Но к ней мне придется вернуться еще раз, как минимум.

В целом же каких-то непреодолимых препятствий не встретилось, со всем удалось справиться. Так что, приходится признать, что C++ развивается в правильном направлении. Не смотря на то, что какие-то вещи в современном C++ мне сильно не нравятся.

ЗЫ. Кстати говоря, пригодился трюк вот из этой статьи: "Pulling a single item from a C++ parameter pack by its index".

воскресенье, 1 сентября 2024 г.

[life.cinema] Очередной кинообзор (2024/08)

Традиционный отчет о просмотренных за минувший месяц фильмах. Традиционно в начале каждого из списков идет то, что понравилось больше, а в конце то, что не понравилось вовсе.

Фильмы

Ненасытные люди (Greedy People, 2024). Как по мне, так это что-то вроде "Фарго", но на минималках. Совершенно не шедевр, но смотреть интересно, а на фоне остального современного шлака, как вообще выглядит нормально сделанным кино.

Зачинщики (The Instigators, 2024). Не очень понял что это было. Для комедии мне не хватило юмора, для серьезной криминальной драмы не хватило серьезности и драмы. Такое впечатление, что материала было на минисериал, но почему-то решили сделать один полнометражный фильм, в котором не удалось раскрыть и десятой доли всех промелькнувших в кадре персонажей.

Нефариус (Nefarious, 2023). Мне почти что зашло. На мой личный вкус первые 3/4 фильма просто шикарные, все держится на диалогах и актерской игре. А вот развязку, имхо, можно было сделать и покруче. Так что финал слегка разочаровал, но вот то, что ему предшествовало мне понравилось.

Материнский инстинкт (Mothers' Instinct, 2024). Качественно снято. Но! Во-первых, никто из героев не вызвал сопереживания. Во-вторых, за происходящим оказалось следить не интересно и, есть ощущение, что авторы как-то и не сильно пытались зрителя запутать. Общее впечатление: добротно сделано, но не цепляет.

Резервная копия (Bod obnovy, 2023). Неоднозначно. С одной стороны, хороший визуал, есть ощущение, что смотришь качественную фантастику, погружающую тебя в атмосферу недалекого будущего. Но, с другой стороны, неинтересный сюжет и отсутствие чего-то цепляющего в происходящем на экране. В результате добротный середнячок, который не претендует на что-то большее.

Смерч 2 (Twisters, 2024). Красочно сделанный динамичный аттракцион. Но все настолько шаблонно, а персонажи настолько картонные, что все последующие события прочитываются наперед уже после первых 15 минут фильма.

Отчаянные наследники (El favor, 2023). Средненькая комедия из категории "богатые тоже плачут", местами откровенно глупая, местами смешная.

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

Сериалы

Мошенники (первый сезон, 2023). Отлично, посмотрел с большим удовольствием.

ГДР (первый сезон, 2023). Мне не зашел. Во-первых, как-то все подзатянуто. Во-вторых, показались лишними вставки с Горбачевым и его окружением. Вроде как сериал должен был быть шпионским детективом, а не пересказом баек о дурачке Горбачеве. В-третьих, чувствовалось, что бюджет не позволяет воссоздать более-менее большой кусочек Берлина 1989-го года. И это не шло в плюс сериалу. В общем, не рекомендую.

Фурия (Furia, первый сезон, 2021). Во-первых, очень затянуто. Во-вторых, слишком уж часто возникает вопрос "Что за фигню нам здесь показывают?" Так что первый сезон не зашел, желание посмотреть второй сезон не возникло.

Кино вне категории

Пара фильмов, которые я затрудняюсь адекватно оценить.

Бордерлендс (Bordarlands, 2024). Не понял на кого кино рассчитано: для взрослых оно слишком детское, для детей слишком взрослое. На меня произвело впечатление откровенного треша, но такого, про который можно сказать "настолько плохо, что даже хорошо".

Покажи мне Луну (Fly Me to the Moon, 2024). Не являюсь ценителем жанра комедийных мелодрам, так что понятия не имею, как этот фильм оценивать. Но если брать отдельно сюжет, отдельно операторскую работу и отдельно игру актеров, то в целом это один из наиболее добротных и вменяемых фильмов за последние несколько месяцев.

пятница, 30 августа 2024 г.

[job.flame] Откуда такой акцент на софт-скиллз в последние годы?

Позволю себе немного пофлеймить в теме, в которой не разбираюсь (с другой стороны, а разве можно как-то иначе? 😉)

Когда я начинал работать, ни о каких софт-скиллах и речи не было. Это, по моим ощущениям, вообще тема текущего десятилетия. А реальный акцент на этих самых софт-скиллах делается в последние 3-4 года.

И я не мог понять, почем вдруг стало так много разговоров про софт-скиллы. Неужели с хард-скиллами все стало настолько хорошо, что можно уже подумать и о душе софт-скиллах?

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

А давеча посмотрел случайно два интервью с нейробиологом Вячеславом Дубыниным:

ЛЮДИ ТУПЕЮТ! Причина - не алкоголь или никотин. Вячеслав Дубынин.

Вячеслав Дубынин про поиски себя, выгорание, аффирмации. Как работа влияет на мозг?

И у меня появилась другая версия. Практически медицинская 🙂

Суть в том, что выросло поколение, которое с детства было окружено гаджетами и виртуальным общением. А это значит, что представители этого поколения развивались в других условиях.

Мы с раннего детства проводили много времени в разного рода коллективах: будь то детский сад, школа, кружки, спортивные секции, пионерские и спортивные лагеря, не говоря уже про двор. Сплошной реал, никакой виртуальности. В котором навыки этого самого общения, как и навыки существования в рамках коллектива, прививались прямыми и незатейливыми способами. Уже к 4-5-м классам школы практически каждый на своей шкуре усваивал, что трепать языком нужно поменьше, за слова принято отвечать, да и получить в лыч за эти самые слова можно очень даже легко и непринужденно. А можно и не за слова, а просто потому, что оказался не в то время и не в том месте. Всякое бывало... В общем, прямые и незатейливые способы иногда оказывались настолько жесткими и суровыми, что не все вписывались в эти условия, но это уже тема другого разговора.

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

Что наводит меня на следующую мысль: а что если у молодого поколения, которое вместо игры в "войнюшку" всем двором, чатилось в соцсетях, просто оказались недостаточно развиты те самые нейросети для общения и социализации, которые естественным образом прокачивались у нас, старпёров, в нашем далеком уже детстве?

Т.е. тупо часть мозга, которая у поколения 1960-х, 1970-х и 1980-х была хорошо развита вот просто потому, что другого выхода-то и не было, у поколения 2000-х уже просто на недоразвитом (в биологическом смысле) уровне. И, грубо говоря, нынешние 20-летние не могут в человеческое общение на таком же уровне, на котором умели мы.

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

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


Ну ладно, эта медицинская версия слишком уж попахивает стариковским ворчанием по поводу никчемной молодежи. Так что вот еще одна.

Ранее (лет 25 назад) в найме роль HR была не столь существенна, как сейчас. А вот нонче, судя по постам в LinkedIn, практически только через HR.

Могут ли HR адекватно оценить хард-скиллы соискателей? Сильно сомневаюсь. Может и есть единицы таких продвинутых, но именно что единицы.

Тогда как софт-скиллы могут, да еще как. Отсюда и значимость этих самых софт-скиллов: ведь если процессами найма рулят HR, то растет и вес критериев, которые сами HR могут оценить. Отсюда и актуализация софт-скиллов.

Но эта версия, честно говоря, слишком банальна. А потому и не интересна 😎

вторник, 20 августа 2024 г.

[prog.c++] Вынесу из комментариев на Хабре про дебилизм с std::launder и std::start_lifetime_as

В комментариях к статье на Хабре зашел разговор об уместности использования std::launder и std::start_lifetime_as.

И, насколько я смог понять из разговора, вот в такой ситуации у нас нет сейчас простого и понятного способа в точке (1) сделать каст указателя к Demo*:

#include <iostream>
#include <functional>
#include <new>
#include <cstddef>
#include <cstring>
#include <memory>

struct Data {
    int _a{0};
    int _b{1};
    int _c{2};
};

using CreateFn = std::function<void(std::byte*)>;

void make_and_use_object(CreateFn creator) {
    alignas(Data) std::byte buffer[sizeof(Data) * 3];
    std::byte * raw_ptr = buffer + sizeof(Data);
    creator(raw_ptr);
    Data * obj = std::launder(reinterpret_cast<Data *>(raw_ptr)); // (1)
    std::cout << "obj: " << obj->_a << ", " << obj->_b << ", " << obj->_c << std::endl;
}

int main() {
    make_and_use_object([](std::byte * ptr) {
        new(ptr) Data{._a = 1, ._b = 2, ._c = 3};
    });
    make_and_use_object([](std::byte * ptr) {
        Data data{ ._a = 25, ._b = 16, ._c = 890};
        std::memcpy(ptr, &data, sizeof(data));
    });
}

Суть в том, что когда make_and_use_object вызывается с первой лямбдой и по указателю ptr новый объект Data создается через placement new, то затем raw_ptr нельзя просто так скастить к Data*. Тут требуется именно std::launder. Ну вот требуется и все. Иначе UB.

Тогда как при использовании второй лямбды в точке (1) вообще не нужно вызывать ни std::launder, ни std::start_lifetime_as. Вроде как все дело в том, что std::memcpy неявно начинает время жизни нового объекта. И результирующий указатель можно просто скастить к Data* и все.

Но, допустим, что во второй лямбде я не использую std::memcpy, а заполняю переданный мне буфер собственной функцией побайтового чтения из COM-порта. Что-то вроде:

 make_and_use_object([](std::byte * ptr) {
     com_port_reader reader;
     reader.init();
     while(reader.has_data()) {
         *ptr = reader.read_next_byte();
         ++ptr;
     }
 });

Естественно, ничего из этого в стандарте не описано и компилятор понятия не имеет, начинается ли здесь какой-нибудь lifetime или нет.

Соответственно, что делать в make_and_use_object после завершения работы лямбда-функции?

Если содержимое объекта внутри буфера было сформировано побайтовым чтением из COM-порта, то std::launder не поможет, тут нужен как раз std::start_lifetime_as (который завезли в C++23, но который, если не ошибаюсь, пока нигде не реализован).

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

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

Для полноты картины: там же в комментариях указали на наличие пропозала от Антона Полухина. Смысл этого пропозала -- отказаться от необходимости использовать std::launder вот в таких простых ситуациях:

alignas(T) std::byte storage[sizeof(T)];
auto* p1 = ::new (&storage) T();
auto* p2 = reinterpret_cast<T*>(&storage);
bool b = p1 == p2;  // b will have the value true.

Если этот пропозал примут, то ситуация окажется совсем веселая:

  • до C++26 подобный кастинг без std::launder будет считаться UB. Т.е. если вы пишете под C++17 или C++20, то должны использовать std::launder, иначе в вашем коде формальный UB;
  • начиная с C++26 это уже не UB и можно std::launder не писать.

А теперь представим проект (какую-нибудь библиотеку, вроде RapidJSON), который должен собираться и под C++14, и под C++17, и под C++20, и под C++23, и под C++26. И как в таком проекте быть со всеми этими std::launder и std::start_lifetime_as? Кроме как прятать подобные фокусы за фасадом макросов мне ничего в голову не приходит.

Но пусть даже у нас есть набор нужных вспомогательных макросов... Вернемся к самому первому примеру в посте. Как там понять, требуется ли std::launder, std::start_lifetime_as или же вообще ничего не требуется?

Есть ощущение, что никак. Подобный код нужно перепроектировать. А это мне лично не нравится, ибо тогда мы начинаем заниматься не решением собственных задач, а ублажением компилятора, чтобы компилятор нидайбох не подумал, что у нас есть UB, которое можно эксплуатировать.

Собственно, чего хотелось бы иметь:

  • чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A, создали новый объект A (или даже какой-то отнаследованный от него B -- пример), старые указатели "протухли", нужно их "отмыть" через std::launder. Все. Больше ни для чего std::launder не нужен;
  • чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.

И строго так, без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.

Но, боюсь, комитет по стандартизации под давлением разработчиков компиляторов добавит нам еще массу ярких ощущений в борьбе с UB. Особенно с такими, про которые ты даже не догадываешься.

PS. Да, я в курсе, что практику с неявным началом времени жизни при использовании ряда функций (вроде malloc или memcpy) в C++20 ввели для того, чтобы узаконить говнокод, написанный в древности или даже вообще на чистом Си (как в случае с GCC -- сперва это был Сишный код, а потом еще стали компилировать как C++). Но ведь C++ все равно потихоньку отказывался от атавизмов, например, ключевое слово auto кардинально поменяло свой смысл, а ключевое слово register сейчас нельзя использовать по его первоначальному назначению. Так что лично для меня этот аргумент из категории "ну такое себе". А если какие-то комитетчики настаивают на то, что этот говнокод нужно оставить как есть и сделать легальным (да еще и за счет умолчаний, про которые мало кто знает), то хочется сказать таким комитетчикам: ну так оставьте легальным и тот говнокод, в котором std::launder не используется.

PPS. На всякий случай ссылки на посты двухгодичной давности, в которых обсуждалась проблематика std::launder: "Продолжение темы про передачу C++объектов через shared memory. Промежуточные выводы" и "В склерозник: ссылки на тему std::launder"

четверг, 15 августа 2024 г.

[prog.c++] Отличная статья про UB в C++

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

Пара главных впечатлений от прочтения.

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

void do_something(std::string_view what) {...}
...
do_something("simple-string-literal");

Ведь, если вдуматься, подобных неявных преобразований много. Просто за счет того, что они неявные, мы не отдаем себе отчета о том, насколько же они сокращают объем кода.

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

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

А данная статья со своими примерами кладет несколько лишних камешков на чашу именно этой точки зрения.

Во-вторых, количество приводимых примеров UB наводит на мысль о том, что нормальный человек просто не в состоянии оценивать все те грабли, которые можно собрать программируя на C++.

Отчасти это не так страшно, т.к. при нормальном тестировании и использовании санитайзеров и анализаторов изрядную их часть можно собрать еще на этапе разработки.

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

Так что даже не знаю как поведу себя, если когда-нибудь придется проходить собеседование на C++ разработчика и меня попросят рассказать что сделает тот или иной код. Вроде как самым естественным было бы спросить "Ребят, а вы в своем уме вообще?" Но, боюсь, окажусь не понятым 🤣

пятница, 9 августа 2024 г.

[prog.c++.flame] Краткие впечатления от туториала wharehouse-backed от авторов CAF-а

Некоторое время назад авторы CAF-а (он же C++ Actor Framework) выкатили туториал warehouse-backend. Заглянул любопытства ради. Поделюсь некоторыми крайне субъективными наблюдениями.

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

Ну и да, чтобы было лучше понятно о чем написано ниже, было бы здорово сперва посмотреть туториал, хотя бы по диагонали. Если вы никогда в глаза не видели CAF или видели когда-то давно, то это может быть весьма любопытно.

Впечатление первое. Одно из главных

Глядя на код примеров ощущаю себя старым пердуном, застрявшим в начале 1990-х со своим ООП, и ничего не понимающим в красоте функционального кода.

Мне бы обычных классов с методами. А не лямбды внутри лямбды внутри лямбды, объединенные монадическими цепочками .filter().map().observe().transform().subscribe() (или это не монады вообще?)

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

Впечатление второе. Нехороший привкус vendor lock-in

Очень смущает обилие того, что берется из пространства имен caf. Тут тебе и caf::net, и caf::cow_string, и caf::json_object, и caf::logger, и пятое, и десятое.

Такое ощущение, что авторы CAF-а создали вокруг своего инструмента целую замкнутую и самодостаточную экосистему.

Наверное, непритязательным пользователям удобно: берешь один CAF, а вместе с ним получаешь и сеть (включая TLS и HTTP-сервер), и JSON, и конфиги, и еще кучу всего полезного. И, полагаю, это хорошо для подсаживания пользователей на иглу конкретного продукта. Кто сказал vendor lock-in? 😉

Мы когда-то давно вокруг SObjectizer-а тоже пытались что-то подобное построить -- у нас и so_log был, и so_sysconf, и еще что-то. Это было хорошо двадцать лет назад, когда мы были в замкнутом мире внутренних продуктов компании Интервэйл, в которой SObjectizer когда-то появился.

Но сейчас, когда в мире C++ столько всего разного и конкурирующего друг с другом, мне показалось более разумным сконцентрироваться только на ядре SObjectizer-а. А уже пользователь может выбрать для себя Asio или libuv, spdlog или Boost.Log, RESTinio или Beast, nlohmann::json или simdjson, и т.д., и т.п.

Так что я лично придерживаюсь мнения, что инструмент должен делать лишь одну вещь, а остальные потребности, будь то парсинг JSON или прием/отсылка UDP-датаграмм, закрывается внешними инструментами.

Впечатление третье. Незначительное

Есть в туториале пример с typed_actor. На первый взгляд, штука прикольная, когда мы на уровне типа описываем то, на что актор реагирует.

Признаться, сам несколько раз задумывался о том, а можно ли сделать это в SObjectizer. Каждый раз это упиралось в растущий как снежный ком клубок вопросов, что в итоге складывалось впечатление, что овчинка не стоит выделки.

На практике у меня нередко возникают ситуации, когда есть агент со своим набором обрабатываемых сообщений. И на время отладки нужно добавить агенту обработчик еще одно сообщения, а затем убрать из финальной версии. Если агент не типизирован, то делается это легко и непринужденно. Тогда как в случае типизированного агента придется временно менять его описание (т.е. его тип), потом возвращать обратно. Это какие-то слишком уж глобальные изменения.

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

Так что типизированные акторы, имхо, фича прикольная, но на практике не так уж и полезная.

Вместо заключения

Признаюсь честно, лет 7-8 назад у меня еще были и время, и силы, и желание брать и смотреть на чужие похожие разработки (будь то CAF, Akka или Orleans). Теперь уже нет ни того, ни другого, ни третьего.

Все эти реализации модели акторов получаются настолько разными, что смысл подобного сравнения от меня ускользает. Ну вот есть в CAF фича X, а в Akka фича Y. Ну есть и что?

Совсем другое дело, когда ко мне приходят с конкретным вопросом. Типа вот у меня есть такая проблема, как я могу ее решить с помощью SObjectizer-а? И могу ли вообще?

Ну или что-то вроде: вот я в CAF/Akka/Orleans делаю вот это вот так-то, а как это будет в SObjectizer?

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

Но вот на то, чтобы посмотреть что где-то там новенькое появилось... Увы, не хочется.

И, возможно, это и есть для меня самое главное впечатление 🙂

Разработчики же CAF, как я понял, сейчас пытаются монетизировать известность своего продукта. От всей души желаю им успеха на этом непростом пути.


Признаюсь, я уже давно не рассматриваю CAF или Akka в качестве конкурентов SObjectizer-у. Это все настолько разные продукты, что общего в них мне видится очень и очень мало. По сути, это как сравнивать обычный слесарный молоток, кувалду и отбойный молоток. Вроде как все это разновидности молотков, но не так уж и много случаев, когда они могут быть взаимозаменяемы.

понедельник, 5 августа 2024 г.

[prog] Похоже, использование табуляции становится пережитком старых (не)добрых времен

Я уже более 30 лет являюсь приверженцем табуляции для отступов в коде. Но вынужден признать, что часть преимуществ табуляции сейчас уже сложно оправдать (а то и объяснить).

Во-первых, это в 1990-ом году, на 5.25" дискете емкостью 360Kb (да и даже 720Kb) приходилось место экономить. Поэтому было критично, что исходный файл с табуляцией может занимать от 1/3 до 1/2 меньше места, чем файл с пробелами. Сейчас, когда терабайты умещаются на MicroSD карте, об этом даже смешно говорить.

Во-вторых, сейчас сложно найти редактор кода, не говоря уже об IDE, который не умеет сдвигать блоки кода влево/вправо на нужное число позиций. Тогда как раньше в каком-нибудь редакторе Turbo C 2.0 такой фичи не было как класса. Хочешь сдвинуть несколько строк влево? Удаляй лишние символы в начале строки вручную. Хочешь сдвинуть вправо? Добавляй. Опять же вручную. Понятное дело, что табуляция здесь гораздо удобнее, чем пробелы.

В-третьих, экраны сейчас не в пример лучше, везде графические интерфейсы, можно поставить какой хочешь шрифт какого хочешь размера. А раньше, в текстовом режиме на каком-нибудь убогом терминале от ЕС-1840 в режиме 80x25 особо не разгонишься. И если на таком экране исходный код с отступами по 2 или 4 пробела выглядит убого, то ничего ты уже не сделаешь. Другое дело с табуляцей -- настроил под себя, хоть в единичку, хоть в восьмерку, и радуйся.

В-четвертых, раньше мне гораздо чаще приходилось извлекать фрагменты кода и вставлять их в какую-то документацию. Да даже больше: раньше код даже часто печатали на бумаге, просто чтобы поразбираться в спокойной обстановке оставляя пометки на полях (в "спокойной обстановке" ты оказывался еще и потому, что твое машинное время закончилось и нужно уступить компьютер коллеге). Возможность поменять размер отступов не трогая сам текст и здесь была очень в тему. Многие ли сейчас пишут документацию, куда вставляются фрагменты реального кода? Кто-нибудь сейчас печатает код на бумаге, чтобы покорпеть над ним с красным карандашом в руках?

Ну и не могу не поделиться еще одной своей болью последних нескольких месяцев: web-интерфейс Google Mail удаляет табуляции из фрагментов кода когда их вставляешь в текст письма копипастой, весь такой фрагмент оказывается выровнен по левому краю 🙁
Хорошо хоть Google Doc такой фигней пока(?) не страдает...

PS. Прошу понять меня правильно. Я не пытаюсь развести очередной срач на тему "tabs vs spaces". Просто как-то грустно от осознания как давно я в программизме и как много поменялось за это время.

PPS. Xотя, конечно, же, единственный верный способ -- это табуляция, все остальное должно быть предано анафеме и выжжено каленым железом, адназначна! (Желающих мне возразить прошу обратить внимание не тег Humour)