четверг, 3 июля 2025 г.

[prog.c++] Шаблоны против копипасты 11: теперь в сочетании с if constexpr

Был у меня код вроде вот такого ([[nodiscard]] и прочие элементы хорошего стиля убраны для упрощения):

class element_finder_t
{
public:
  search_result_t
  find_element(const search_conditions_t & conditions)
  {
    if(should_use_first_algorithm(conditions))
      return find_by_first_algorithm(conditions);
    else if(should_use_second_algorithm(conditions))
      return find_by_second_algorithm(conditions);
    ...
  }

private:
  search_result_t
  find_by_first_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }

  search_result_t
  find_by_second_algorithm(const search_conditions_t & conditions)
  {
    ... // Какой-то код для поиска элемента в цикле.
    if(is_conditions_fulfilled(element_info, conditions))
      return element_info;
    ... // Какой-то код для подготовки следующей итерации.
  }
};

В один прекрасный день потребовалось кроме метода find_element добавить еще и метод find_element_with_postprocessing. Отличие нового метода от старого find_element в том, что когда элемент удовлетворяющий условиям поиска найден, то нужно сделать некую дополнительную постобработку. К сожалению, постобработка может привести к тому, что элемент перестанет удовлетворять условиям поиска. В таком случае нужно попытаться найти следующий подходящий элемент.

В лоб такое расширение element_finder_t можно было бы сделать так:

вторник, 1 июля 2025 г.

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

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

Фильмы

Маскарад (Mascarade, 2022). Отличное кино. Наверное лучшее, что довелось посмотреть за последние месяцы.

Хищник: Убийца убийц (Predator: Killer of Killers, 2025). Редко даю ссылки на полнометражные мультфильмы. Но этот точно заслуживает внимание тех, кому нравятся фильмы про Хищника.

Пункт назначения: узы крови (Final Destination: Bloodlines, 2025). Мне понравилось, любители жанра могут смело смотреть. Сравнивать с самым первым фильмом "Пункт назначения", который сейчас чуть ли не культовым статусом обладает, не буду, т.к. плохо его помню.

Воздушный перевозчик (La extorsión, 2023). На удивление неплохо. А если бы актеры еще и поменьше кривлялись, то было бы и просто отлично. Но это, наверное, особенность национальной актерской школы.

Игра вдовы (Widow's Game/A La viuda negra, 2025). Простенько. И это не детектив, т.к. быстро становится понятно что к чему. Но как рассказ о незамысловатой, но трагичной, криминальной истории, вполне себе норм. Большим плюсом лично для меня то, что фильм европейский и по своей стилистики сильно отличается от голливудской продукции.

Дело «Мальдорор» (Maldoror, 2024). Специфическое кино. Далеко не шедевр. И советовать его к просмотру сложно. Но оно хотя бы по своей стилистике очень и очень сильно отличается от красочного голливудского гламура.

Новичок (The Amateur, 2025). Снято, вроде бы, нормально. И актеров неплохих подтянули. Но при просмотре возникает ощущение, что смотришь какую-то сказочку, которая и заканчивается счастливым сказочным образом.

Грешники (Sinners, 2025). Как-то по шуму вокруг и восторженным отзывам ждал чего-то вроде "От заката до рассвета", только с неграми, а вышло что-то затянуто нудное и, я бы даже сказал, детское. Сцена после титров слегка спасает происходящее на экране, но не сильно.

Сериалы

Ганстерленд (MobLand, первый сезон, 2025). Круто, конечно, снято. И смотреть интересно. А вот финал разочаровал. Ожидал кровавой бойни на весь Лондон и окрестности. Вышло же не то, чтобы предсказуемо, но без удивления.

Ночной агент (The Night Agent, первый сезон, 2023). Очень простенько, местами предсказуемо, местами совершенно невероятно. Но зато бодренько.

Невозможно поверить (Unbelievable, первый сезон, 2019). В принципе, неплохо. Но в последней серии развели столько соплей, что прям караул, сильно подпортили общее впечатление.

Отдел нераскрытых дел (Dept. Q, первый сезон, 2025). Чем смотреть это с мучением растянутое на 9 серий "нечто" лучше глянуть датский "Мистериум. Начало" от 2013-го года. По тому же произведению и с теми же общими чертами, но гораздо более удачный фильм.

суббота, 28 июня 2025 г.

[prog.c++.dreams] В очередной раз выяснил, что в C++ using это не strong typedef :(

В С++ есть отличная штука под названием using. Позволяет вводить удобные псевдонимы для сложночитаемых имен типов или же прикрывать тонкой завесой детали реализации.

Но, к сожалению, using не может сделать совершенно новый тип. Поэтому если у вас есть что-то вроде:

using type_a = ...;
using type_b = ...;

void do_something(type_a v) {...}
void do_something(type_b v) {...}

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

Например, раньше было:

using small_data = std::map<some_key, some_value>;
using large_data = std::unordered_map<special_key, some_value>;

using type_a = small_data::iterator;
using type_b = large_data::iterator;

А в один прекрасный момент стало:

using special_key = some_key;

using small_data = std::map<some_key, some_value>;
using large_data = std::map<special_key, some_value>;

И все 🙁
Типы type_a и type_b оказались одинаковыми.

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

namespace processing
{

class processor {...};

template<typename Data>
void
handle(const processor & how, const Data & what)
{
  // Должна быть найдена подходящая функция за счет ADL.
  apply(what, how);
}

// namespace processing

namespace data_type_a
{

struct data {...};

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_a

namespace data_type_b
{

struct data {...};

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_b

И т.д.

Т.е. смысл в том, что в конкретном пространстве имен data_type_X должна быть функция apply, который компилятор посредством ADL находит для вызова внутри processing::process.

Все шло хорошо до момента, пока не появились data_type_i и data_type_j, в которых тип data был определен через using:

namespace data_type_i
{

using data = std::map<...>;

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_i

namespace data_type_j
{

using data = std::vector<...>;

void apply(const data & what, const processing::processor & how) {...}

// namespace data_type_j

И вот когда эти типы начали отдавать в processing::process, то код перестал компилироваться. Причем далеко не сразу удалось понять, почему ни одна из определенных в правильных пространствах имен apply не выбиралась компилятором как подходящая.

А дело в том, что если заменить псевдонимы, то получались функции вида:

void apply(const std::map<...> &, const processing::processor&);
void apply(const std::vector<...> &, const processing::processor&);

И естественно, что ADL не мог их найти ни в пространстве имен std, ни в processing.

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

В очередной раз захотелось, чтобы using в С++ мог работать как strong typedef. Чтобы можно было написать что-то вроде:

namespace data_type_i
{

using(new) data = std::map<int, std::string>;

// namespace data_type_i

И чтобы компилятор начал считать, что data и std::map<int, std::string> теперь разные типы. И что тип data теперь принадлежит пространству имен data_type_i, а не std.

PS. В свете добавления в C++ рефлексии может оказаться, что наколхозить какой-то нестандартый strong_typedef_for, типа:

namespace data_type_i
{

using data = my::strong_typedef_for< std::map<int, std::string> >;

// namespace data_type_i

через рефлексию будет быстрее и проще, чем дождаться появления using(new) в стандарте. Обычная традиция C++: если что-то можно собрать своими руками дендро-фекальным методом, то включать в стандарт удобный и нормальный вариант никто не будет.

понедельник, 23 июня 2025 г.

[prog.c++] Есть ли теперь смысл при разработке C++ библиотек придерживаться не самых свежих стандартов?

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

Поясню в чем дело.

В C++ на протяжении десятков лет есть специфическая картина: существует официальный С++ на "бумаге", т.е. описанный в соответствующем стандарте, будь то С++98, С++11 или C++23, и есть реальный C++, доступный конкретному разработчику в конкретном компиляторе. И, как правило, в имеющихся в нашем распоряжении компиляторах далеко не все фичи из самого последнего официального стандарта реализованы.

Эта картина особенно важна при разработке кросс-платформенных библиотек. Если ты хочешь, чтобы твою библиотеку использовали разные люди в разных проектах и на разных платформах, то ты вынужден занижать версию стандарта. Например, вместо C++23 (который вроде как уже два года нам доступен) делать библиотеку на C++17 или даже C++14.

При разработке софта для конечного пользователя ситуация, зачастую, гораздо проще: очень часто софт пишется под конкретную платформу и вполне можно заложиться на конкретный компилятор. Но с разработкой библиотек не так. В проекте X библиотека может работать уже в режиме C++23, тогда как в проекте Y -- все еще в C++17.

До недавнего времени я воспринимал такую ситуацию как данность. Типа в разработке прикладного софта для конечного пользователя своя специфика, а при разработке библиотек -- своя. Нужно смириться и получать удовольствие.

Но после C++20 смиряться все сложнее.

C++11 стал совсем другим языком в сравнении с C++98. Два следующих стандарта, C++14 и С++17, инкрементально улучшали C++, но не могу сказать, что они переводили язык на какой-то принципиально другой уровень (даже не смотря на такие крутые фичи C++17 как structured binding и if constexpr). А вот начиная с C++20 все принципиально поменялось:

  • C++20 добавил концепты и operator spaceship (про модули промолчу ибо не пробовал);
  • С++23 добавил deducing this. Не смотря на то, что (на мой взгляд) сделали это через одно место (и можно было бы по-другому), но таки важную реальную проблему эта фича решает;
  • С++26 добавляет compile-time рефексию и, я очень надеюсь, контракты.

Все это вместе, не побоюсь этого слова, делает из C++ совсем другой язык в такой же степени, как C++11 после C++98. Если не в большей.

И вот глядя на все это великолепие в свежих стандартах С++ я не могу найти для самого себя ответ на вопрос: а зачем на при разработке своих библиотек оставаться на C++17/14/11?

Вот реально.

Ладно бы нам за наши OpenSource проекты платили. Но этого нет. И RESTinio, и SObjectizer приносят нам деньги не напрямую, а приводя к нам клиентов через репутацию. Зачастую новые фичи в тот же SObjectizer добавляются "just for fun" -- просто что-то выглядит интересным или представляет из себя вызов, поэтому делаешь это ради получения удовольствия.

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

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

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

вторник, 17 июня 2025 г.

[prog.c++] Еще раз про noexcept в C++: это очень специфическая гарантия отсутствия исключений

Увидел вот такое в ленте LinkedIn:

У этого "откровения" полсотни лайков и несколько репостов. Ужас.

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

И уж совсем дико читать про "Makes code safer and more predictable" и, особенно, про "Prevents unexpected termination during exception propagation". К сожалению, пока в C++ недостаточно средств для того, чтобы noexcept было удобно использовать для написания безопасного и предсказуемого кода. Но это тема отдельного большого разговора.

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

понедельник, 16 июня 2025 г.

[prog.thoughts.flame] Прочел давеча статью про вайб-кодинг с использованием LLM

На RSDN-е поделились ссылкой на статью "Вайб-кодинг: практика, о которой почему-то не говорят" в которой рассказывается про опыт разработки некого проектика на Go посредством "искусственного интеллекта" (Sonnet 3.7).

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

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

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

Первый грубый пример, который сходу вспоминается, -- это мода на всяческие Wizard-ы, которые начали появляться в IDE когда стало возникать само понятие IDE (середина-конец 1990-х). Помнится, в каком-нибудь условном Borland C++ 2.0 кучу кода нужно было написать вручную прежде чем у тебя появится примитивное приложение с одним окошком. А в каком-нибудь Borland C++ 4 или VisualStudio 98 ты кликнул на несколько кнопок в Wizard-е и получил готовый каркас приложения с тем самым одним окошком.

Еще один вспомнившийся пример: простые декларации модели данных средствами ActiveRecord в Ruby-On-Rails скрывают за собой кучу ORM-кода.

Более сложный пример, который, возможно, не все сочтут релевантным, -- это генераторы парсеров, вроде yacc/bison, coco/r, ragel или ANTLR. Ведь чтобы вручную написать разбор даже относительно несложной грамматики придется написать не одну сотню строк кода. Тогда как в случае с bison-ом ты описываешь лишь то, что тебе нужно, на специальном DSL, и получаешь здоровенную простыню автоматически сгенерированного кода.

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

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

Только вот вместо собственноручного написания условных Wizard-ов для своих задач, они использовали LLM в качестве такого Wizard-а.


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

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

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

Грубо говоря, если у вас задача просуммировать значения в столбцах матрицы 1M на 1M, то придется написать кучу бойлерплейта на чистом Си и обойтись всего лишь несколькими строчками в каком-то специализированном DSL.

И до недавнего времени задача разработчиков (по крайней мере той части разработчиков, которая занимались языками программирования и фреймворками) состояла именно в том, чтобы дать прикладным программистам те самые высокоуровневые, заточенные под задачу инструменты (в виде продвинутых ЯП, специализированных DSL или же фреймворков для ЯП общего назначения).

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

Т.е. высокоуровневыми инструментами становятся не ЯП/DSL/фреймворки, а генеративные модели, которые запросто выплевывают мегатонны кода на уже существующих языках общего назначения. А исходником становится текст промпта для AI.

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

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

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

А вот тут у меня возникают опасения.

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

Причем не всегда речь идет о багах. Временами проблема может быть в неожиданных просадках производительности, слишком длительной блокировке каких-то ресурсов и т.п. Грубо говоря, сложность какой-то операции, о которой мы не задумывались, может оказаться O(n*n), и это проявляется только в каких-то специфических сценариях у VIP-заказчика.

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

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

Второй же момент возникает из того, что автор статьи пишет в других местах:

Понимание архитектуры и умение читать чужой код становятся критически важными.
Ценность навыка написания кода снижается в пользу навыка чтения чужого кода.

Что-то я весьма скептически отношусь к такой вещи, как "умение читать чужой код".

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

Попробуйте ответить сами себе: насколько вы любите задачи вида "вот написан код, что получится в результате его работы?

Насколько хорошо вы решаете такие задачи?

А если код не одна страничка текста, а 150-200-250 и более строк?

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

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

Но этого не происходит.

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


Если же все-таки попробовать подвести некий итог от прочитанного, то есть ощущение, что прикладное программирование с пришествием AI должно очень серьезно измениться. Под прикладным я понимаю решение задач, необходимых конечным пользователям программного обеспечения, типа "посмотреть историю движения остатков по складам за три последних квартала" или "сделать интеграцию с сервисом N для того, чтобы видеть последние M товаров, интересных пользователям пришедшим с площадки K".

Другой вопрос: а что будет с теми, кто занимается не рутиной, а какими-то уникальными велосипедами? Например, сможет ли AI в обозримое время заменить тех, кто сделал Roaring Bitmap?

Вот не знаю. Впрочем, таких велосипедостроителей и раньше много не требовалось.

В общем, не знаю чего ожидать. Есть ощущение, что ничего хорошего.

PS. Убежден в том, что развитие тех самых IDE из середины 1990-х с их Wizard-ами, автоматизированными рефакторингами, интеллектуальным автодополнением и мгновенной навигацией по коду сделало ситуацию в программизме только хуже: стало больше говнокодеров и, соответственно, говнокода. А производство кода посредством AI лишь усугубит дело -- это как самые продвинутые IDE на конских дозах стероидов + массовое непонимание того, что творится в сгенерированном коде. Так что да, есть ощущение, что ничего хорошего таких как я не ждет.

среда, 4 июня 2025 г.

[prog.c++] Осваиваю C++ные концепты: одна или две константны внутри типа

Недавно довелось столкнутся с задачкой, в которой пользователь должен определить тип с перечнем свойств. Что-то вроде traits из нашего проекта RESTinio. При этом внутри такого типа обязательно должна быть константа high_watermark типа std::size_t. Например:

struct my_traits {
  ... // Какие-то определения типов.

  // Максимальный размер при достижении которого нужно провести чистку данных.
  static constexpr std::size_t high_watermark = 64 * 1024 * 1024;
};

А кроме этого может быть определена и вторая константа, low_watermark. Т.е. класс свойств может выглядеть и так:

struct my_traits {
  ... // Какие-то определения типов.

  // Максимальный размер при достижении которого нужно провести чистку данных.
  static constexpr std::size_t high_watermark = 64 * 1024 * 1024;

  // Размер при достижении которого чистку данных следует остановить.
  static constexpr std::size_t low_watermark = 16 * 1024 * 1024;
};

При использовании таких типов свойств нужно было как-то понять, есть ли в свойствах константа low_watermark. Если есть, то нужно было использовать именно ее значение. А если нет, то оставалось высчитывать нижний порог из high_watermark.

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

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

Полный код под катом, а проверить его можно, например, на Wandbox.

Upd. Под катом так же и второй вариант, более компактный за счет отсутствия одного из концептов и использования if constexpr внутри вспомогательной функции low_watermark.

Upd. Более безопасно использовать std::same_as вместо std::is_same_v.

воскресенье, 1 июня 2025 г.

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

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

Фильмы

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

Громовержцы* (Thunderbolts*, 2025). Первые 2/3 мне отлично зашли, т.к. там было столько иронии над супергеройской темой, что я уж подумал, что Марвелл отважилась отстебать своё же супергеройское кино. Потом начался закос под серьезность, что сильно подпортило впечатление, но самый-самый конец и титры чуть-чуть ситуацию выправили. Получился забавный эксперимент, который вполне можно и посмотреть.

Тайна в её глазах (Magpie, 2024). Развязка хорошая, но то, что ей предшествовало выглядело скучным и не вполне естественным.

Источник вечной молодости (Fountain of Youth, 2025). Красочно, динамично, но очень уж все наивно и простенько. Такое ощущение, что это фильм для семейного просмотра с детьми младшего школьного возраста.

Дыхание шторма (Last Breath, 2025). Взяли невероятную историю, подтянули хороших актеров, сделали классную картинку... Но самым интересным и впечатляющим оказались кадры из семейной кинохроники прототипа главного героя, показанные в самом конце. Смотреть можно не потому, что хороший фильм, а потому, что в нем рассказывается про уникальный случай.

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

Дроп (Drop, 2025). Такой фильм мог бы зацепить, если бы возникло ощущение реальности происходящего. Но такого ощущения не возникло. Какое-то все слишком гламурное и нарочитое. В общем, не зашло.

Объект преследования (Target, 2023). Поначалу было вполне себе нормально. В финале сотворили что-то невообразимое, что основательно подпортило впечатление от кино.

Курьер (El correo, 2024). Вроде бы бодренько. Но не зацепило, как-то шаблонно все.

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

Последний подозреваемый (Zheng jiu xian yi ren, 2023). Редкий маразм + обилие чрезмерного кривляния, характерного для азиатского кино. Смело можно проходить мимо.

Сериалы

Операция "Карпаты" (первый и второй сезоны, 2024). По большей части возникает ощущение "нам втирают какую-то дичь", но зато все динамично, нет сериальной затянутости.

Мосгаз. Дело № 11. Розыгрыш (2025). Не рекомендую. Может быть по уровню маразма и не сравнялись с "Мосгаз. Последнее дело Черкасова", но все равно слишком часто возникало ощущение, что нам втирают какую-то дичь.

четверг, 29 мая 2025 г.

[prog.c++] Не нужно использовать расширение .h для имен заголовочных файлов в C++

В именовании файлов должна быть и логика, и практичность.

Например, использование расширения .h для заголовочных файлов С++, как по мне, и не логично, и не практично.

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

Непрактично с точки зрения поиска в исходных текстах. Например, у меня есть каталог с исходниками, хочу найти те файлы, в которых встречается какая-то подстрока. Какую маску имен файлов задавать для grep? А если у меня в распоряжении не grep, а FAR Manager в Windows? Или если я привык пользоваться Midnight Commander и не хочу бодаться с дедовским grep?

Гораздо удобнее использовать расширения .hpp для заголовочных, и .cpp для файлов реализации. Ну или .hxx и .cxx.

Почему?

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

Во-вторых, маски поиска (хоть для grep, хоть для FAR, хоть для MC) задаются не просто, а очень просто: *.?pp (ну или *.[hc]pp для тех, кто любит точность).

А расширение .h в C++ных проектах нужно оставить для кода, который совместим с чистым Си.


Я еще применяю расширение .ipp. Обычно для файлов, которые содержат куски кода не являющиеся самостоятельными и самодостаточными, а предназначены для включения в другие исходные файлы посредством #include. Иногда это куски кода, которые сгенерированы внешними инструментами.

Удобство расширения .ipp в том, что оно попадает под ту же самую маску поиска: *.?pp.


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

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


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

среда, 21 мая 2025 г.

[prog.c++] Попробовал было сделать что-то вроде small object optimization с использованием std::pmr::monotonic_buffer_resource...

...но что-то пошло не совсем так.

Был у меня объект со словарем внутри. Что-то вроде:

struct info_t {...};

using info_map_t = std::map<key_t, info_t>;

class info_holder_t {
  ...
private:
  info_map_t m_infos;
  ...
};

Захотелось немного оптимизировать этот info_holder_t::m_infos. Дело в том, что сбор статистики по реальному использованию info_holder_t показал, что в подавляющем большинстве случаев (сильно больше 95%) в m_infos хранится либо один, либо два экземпляра info_t.

Более того, была еще одна очень важная особенность: в абсолютном большинстве случаев m_infos только наполняется, ничего оттуда не удаляется до самого уничтожения info_holder_t.

Вот я и подумал, что std::map -- это же дерево, где каждый узел -- это динамически созданный объект. А new/delete не самые дешевые операции. Что, если завести в info_holder_t буфер под два info_map_t::value_type и "выделять" память оттуда? А уже когда этот буфер исчерпается, переходить на использование обычных new/delete.

При этом очень и очень хотелось сохранить реализацию основных методов info_holder_t прежней. Т.е. если бы остался именно m_infos типа info_map_t, то это было бы идеально. А вот если бы вместо m_infos оказался какой-то std::variant, в котором был бы отдельный кейс для крошечного словаря, и отдельный кейс для полноценного std::map, то пришлось бы перелопатить более тысячи строк уже отлаженного и проверенного на разных сценариях кода, что не вселяло оптимизма.

Оказалось, что благодаря std::pmr такой оптимизированный std::map можно сделать с минимальными усилиями. Всего-то нужно:

struct info_t {...};

// Вместо просто std::map теперь std::pmr::map.
using info_map_t = std::pmr::map<key_t, info_t>;

class info_holder_t {
  ...
private:
  // Локальный буфер.
  std::array<std::byte, какой-то-размер> m_small_map_fixed_buffer;
  // То, что будет превращать m_small_map_fixed_buffer в арену
  // памяти для m_infos.
  std::pmr::monotonic_buffer_resource m_infos_memory_resource{
    m_small_map_fixed_buffer.data(), m_small_map_fixed_buffer.size()
  };

  // Модицифированный словарь.
  info_map_t m_infos{ std::addressof(m_infos_memory_resource) };
  ...
};

Получается, что для первых нескольких элементов в m_infos память "выделяется" из m_small_map_fixed_buffer через m_infos_memory_resource. А если элементов становится больше, то m_infos_memory_resource сам обращается к динамической памяти посредством std::pmr::get_default_resource. Как раз то, что мне было нужно.

Казалось бы, золотой ключик уже в кармане.

Но есть несколько "Но".

Во-первых, какое выравнивание должно быть у m_small_map_fixed_buffer?

Во-вторых, какой же размер должен быть у m_small_map_fixed_buffer?

Оба эти вопроса упираются в то, что std::map::value_type -- это же не реальный тип узла дерева. Это только тип того, что внутри дерева поиска содержится. Но кроме этого в узле должны быть еще и какие-то указатели (по крайней мере на дочерние узлы и, вероятно, на родительский узел).

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

И если с выравниванием есть надежное (но может и не оптимальное) решение:

// Локальный буфер.
alignas(std::max_align_t)
std::array<std::byte, какой-то-размер> m_small_map_fixed_buffer;

То вот с размером этого буфера начинаются какие-то пляски с бубном. Для эксперимента просто взял (sizeof(info_map_t::value_type) * 10)), этого хватило, чтобы сделать замеры производительности.

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

Полагаю, здесь сказывается несколько факторов:

  • в проекте уже используется mimalloc, а он делает операции new/delete очень быстрыми. Особенно когда и new, и delete происходят на контексте одной и той же нити (что и имело место для моего info_holder-а);
  • появление в info_holder-е дополнительной "начинки", которая требует инициализации (речь про конструктор std::pmr::monotonic_buffer_resource и передачу указателя на него в m_infos) утяжеляет процедуру создания info_holder_t. Плюс к тому, работа monotonic_buffer_resource все же не бесплатна.

Добавлю сюда два обозначенных выше вопроса, хороших ответов на которые у меня нет, и получаю, что простая попытка сделать small object optimization на базе std::pmr::monotonic_buffer_resource завершилась с отрицательным результатом.

PS. Вообще, идея std::pmr, на первый взгляд, очень простая и красивая. Кажется, что разобраться с этим не так уж и сложно. Но, как обычно в C++, кроличья нора гораздо глубже, чем кажется.

понедельник, 19 мая 2025 г.

[prog.c++] Простой способ ускорить поиск в map/unordered_map в случае "тяжелых" ключей

Все знают про наличие в std::map/unordered_map методов find. Но, наверное, не все знают, что у этого find-а есть несколько вариантов. Кроме старого и привычного:

iterator find( const Key& key );

В std::map начиная с C++14 есть еще и:

templateclass K >
iterator find( const K& x );

А в std::unordered_map второй вариант find-а был добавлен в C++20.

Что же следует из того, что в find можно передать не Key, которым мы параметризовали свой map/unordered_map, а какой-то другой тип?

Следует то, что мы можем сделать поиск в map/unordered_map более эффективным если тип Key дорог в конструировании, например, когда внутри Key есть std::string-и или другие типы, динамически выделяющие и освобождающие память.

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

using full_key_t = std::pair<std::size_t, std::string>;

Соответственно, поиск выполнялся как-то так:

const auto id = get_id(...);
const std::string & description = get_description(...);
if(const auto it = _map.find(full_key_t{id, description}); it != _map.end())
{
  ...
}

Проблема в том, что экземпляр full_key_t нужно полностью сконструировать. А значит нужно сделать полную копию description и поместить эту копию в новый объект full_key_t. Что недешево. И, что особенно обидно, объект full_key_t разрушается сразу после завершения поиска. Т.е. мы создаем временный ключ, тратим на это время, а затем тупо выбрасываем временный ключ за ненадобностью.

К счастью, упомянутые выше "новые" варианты find-а позволяют нам избежать создания "тяжелых" экземпляров full_key_t. Теперь в C++ можно делать вот так:

using light_key_t = std::pair<std::size_tconst std::string*>;
...
const auto id = get_id(...);
const std::string & description = get_description(...);
if(const auto it = _map.find(light_key_t{id, &description}); it != _map.end())
{
  ...
}

Однако, чтобы это работало, нужно сделать несколько дополнительных телодвижений. Давайте рассмотрим их на примере std::unordered_map.

Первое, что нам потребуется -- это такой тип хэш-функции, который будет применим как к full_key_t, так и к light_key_t. Поскольку обычно хэш для unordered_map -- это структура со специально написанным operator(), то мы можем просто сделать несколько перегрузок operator():

struct special_hash_t
{
   using is_transparent = void;

   std::size_t
   operator()(const full_key_t & k) const noexcept
   {
      return std::hash<std::size_t>{}(k.first) + 0x9e3779b9 +
            std::hash<std::string>{}(second_key_part(k));
   }

   std::size_t
   operator()(const light_key_t & k) const noexcept
   {
      return std::hash<std::size_t>{}(k.first) + 0x9e3779b9 +
            std::hash<std::string>{}(second_key_part(k));
   }
};

Где second_part_key -- это вспомогательные функции вида:

[[nodiscard]]
const std::string &
second_key_part(const full_key_t & k) noexcept
{
   return k.second;
}

[[nodiscard]]
const std::string &
second_key_part(const light_key_t & k) noexcept
{
   return *(k.second);
}

Внутри special_hash_t можно было обойтись и без них, но вот на следующем шаге они нам сильно помогут.

А следующим шагом будет определение собственного компаратора, который может сравнивать full_key_t и light_key_t на равенство. Этот компаратор мы подсунем std::unordered_map в качестве параметра шаблона KeyEqual. Для нашего примера компаратор может выглядеть так:

struct special_equal_to_t
{
   using is_transparent = void;

   template<typename K1, typename K2>
   bool
   operator()(const K1 & k1, const K2 & k2) const noexcept
   {
      return (k1.first == k2.first) &&
            (second_key_part(k1) == second_key_part(k2));
   }
};

За счет того, что в special_equal_to_t у нас operator() -- это шаблон метода, то он может принимать разные сочетания: как full_key_t с light_key_t, так и light_key_t с full_key_t. Здесь функции second_key_part помогают избежать дублирования кода.

Ключевой момент в этом всем -- это наличие определения is_transparent в special_key_t и special_equal_to_t. Без такого вложенного типа вся обсуждаемая машинерия с "новыми" find работать не будет. Причем, если забыть определить is_transparent, то компилятор может выдать такую простыню невнятных сообщений об ошибках, что причина проблемы будет совершенно непонятна (тут, конечно же от степени вменяемости компилятора зависит).

Далее остается просто определить тип нужного нам unordered_map:

using values_map_t = std::unordered_map<
      full_key_t,
      your_value_type,
      special_hash_t,
      special_equal_to_t>;

Какой же выигрыш дает этот трюк и дает ли вообще?

Если строковые ключи поиска короткие, т.е. срабатывает small string optimization (SSO), то практически не дает. Но вот если строчки достаточно длинные, чтобы SSO остался не у дел, то разница почти в два раза по моим замерам.

Под катом полнофункциональная программка, которая демонстрирует данный подход. Так вот, на i7-8550U с VC++ из VisualStudio 17.4.0 она выдает, например, следующие результаты:

*** find_by_full_key: 1512721us ***
values found: 10000000
*** find_by_light_key: 844164us ***
values found: 10000000

Тогда как MinGW 13.2.0 на той же машине показывает время чуть получше и все равно с почти что двухкратным разрывом:

*** find_by_full_key: 1137556us ***
values found: 10000000
*** find_by_light_key: 557952us ***
values found: 10000000

Так что трюк вполне себе рабочий. Но есть с ним две небольшие проблемки:

  • про него легко забыть, особенно когда голова забита решением прикладных проблем, а глаз уже "замылился";
  • нужно помнить про вложенный тип is_transparent, без него не работает, а компилятор своими сообщениями может только запутать.

Собственно, данный пост возник для того, чтобы зафиксировать этот трюк в склерознике (в особенности его часть про is_transparent).

вторник, 13 мая 2025 г.

[prog.c++] Введение в C++ типов std2::uint*_t, std2::int*_t, std2::float_t, std2::double_t?

Пост навеян флеймообразующей статьей с Хабра под названием "Как Мэтт Годболт «продал» мне Rust (рассказав о C++)" (которая является переводом статьи Matt Godbolt sold me on Rust (by showing me C++)). Сама статья мне не нравится тем, что на C++ навешивают собак, унаследованных из чистого Си. Да еще и демонстрируют использование приемов, за применение которых в коде надо бы отрывать руки (я про вызов atoi для выделения числа из строки).

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

А раз проблема для C++ актуальна, то было бы хорошо ее решить.

Внезапно™ подумалось, что сделать это может быть не так уж и сложно.

Нужно ввести новые стандартные типы:

  • std2::int8_t, std2::int16_t, std2::int32_t, std2::int64_t;
  • std2::uint8_t, std2::uint16_t, std2::uint32_t, std2::uint64_t;
  • std2::int_fast8_t, std2::int_fast16_t, std2::int_fast32_t, std2::int_fast64_t;
  • std2::uint_fast8_t, std2::uint_fast16_t, std2::uint_fast32_t, std2::uint_fast64_t;
  • std2::int_least8_t, std2::int_least16_t, std2::int_least32_t, std2::int_least64_t;
  • std2::uint_least8_t, std2::uint_least16_t, std2::uint_least32_t, std2::uint_least64_t;
  • std2::uintmax_t
  • std2::uintptr_t
  • std2::float_t
  • std2::double_t

Главным (и, возможно, единственным) отличием от их старых собратьев будет то, что компилятор будет запрещать неявные преобразования значений между этими типами или типами из предыдущих стандартов C++ (включая унаследованные из Си и, ИМХО, совершенно бесполезные в современном мире short, int, long и пр.) Т.е. если мы написали так:

void f(std2::uint32_t n) {...}

То компилятор не позволит нам сделать так:

f(-1);

или так:

f(1.2);

или так:

int i = some_calculation();
f(i);

и даже так:

std2::uint_fast32_t r = another_calculation();
f(f);

То, что новые типы будут определены в новом пространстве имен, позволит не ломать старый код.

Если же в старом коде использовались std::int*_t и мы захотели адаптировать его под новый стандарт, то достаточно будет просто заменить std:: на std2:: простым контекстным поиском.

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

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

среда, 7 мая 2025 г.

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

Подсмотрел вот в этом блог-посте - A New Approach to Build-Time Library Configuration - интересный трюк, который захотелось утащить к себе в склерозник на всякий случай.

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

Например, допустим, что мы используем в библиотеке какие-то временные вектора на стеке (скажем std::array<unsigned char, N>) и пользователь должен уметь управлять значением N. Чтобы он мог увеличить N когда это нужно или уменьшить N когда не нужно.

Традиционно в C++ для этих целей используют унаследованный из чистого Си подход с определением символов (они же define-ы). Например, определяют MY_LIB_DEFAULT_N через ключики компиляции. Что-то вроде:

g++ -D MY_LIB_DEFAULT_N=10240 ...

А в коде библиотеки мы делаем что-то вроде:

#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif

namespace my_lib {

constexpr tmp_array_size = MY_LIB_DEFAULT_N;

...
void some_func() {
  std::array<unsigned char, tmp_array_size> tmp_array;
  ...
}

/* namespace my_lib */

Когда таких ручек для тонкой настройки много, все они собираются в некий user-config.h, который должен быть предоставлен пользователем библиотеки (а дефолтная версия user-config.h может генерироваться при установке библиотеки). Получается что-то вроде:

#define MY_LIB_DEFAULT_N 10240
#define MY_LIB_LOGGING_POLICY 0
#define MY_LIB_TRACING_MODE 3
...

В самой же библиотеке мы имеем специальный заголовочный файл impl/config.h, который будет иметь вид:

// Подключаем то, что выставил пользователь.
#include "user-config.h"

// А потом разбираемся с тем, что пользователь выставил или не выставил.
#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif
#if !defined(MY_LIB_LOGGING_POLICY)
  #define MY_LIB_LOGGING_POLICY 2
#endif
#if !defined(MY_LIB_TRACING_MODE)
  #define MY_LIB_TRACING_MODE 10
#endif
... // Далее преобразуем define-ы в типизированные константы.

Для проверки наличия файла user-config.h, как показано в упомянутом выше блог-посте, можно использовать __has_include:

// Подключаем то, что выставил пользователь.
#if __has_include("user-config.h"
  #include "user-config.h"
#endif

// А потом разбираемся с тем, что пользователь выставил или не выставил.
#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif
#if !defined(MY_LIB_LOGGING_POLICY)
  #define MY_LIB_LOGGING_POLICY 2
#endif
#if !defined(MY_LIB_TRACING_MODE)
  #define MY_LIB_TRACING_MODE 10
#endif
... // Далее преобразуем define-ы в типизированные константы.

Но это Си-ное наследие. А ведь можно использовать и чисто C++ные механизмы.

Так, в нашем impl/config.h может быть:

namespace my_lib {

...

namespace config_defaults {
  constexpr std::size_t tmp_array_size = 4096;
  constexpr logging_policy_t logging_policy = logging_policy_t::minimal;
  constexpr tracing_mode_t tracing_mode = tracing_mode_t::runtime_control;
  ...
/* namespace config_defaults */

using namespace config_defaults; // А вот и трюк.

/* namespace my_lib */

// Подключаем то, что выставил пользователь.
#if __has_include("user-config.h"
  #include "user-config.h"
#endif

Теперь пользователь в своем user-config.h может написать, например, так:

namespace my_lib {

constexpr std::size_t tmp_array_size = 10240;
constexpr logging_policy_t logging_policy = logging_policy_t::detailed;
constexpr tracing_mode_t tracing_mode = tracing_mode_t::off;
  ...

/* namespace my_lib */

И эти значения будут иметь больший приоритет, чем те, которые были определены в нашем my_lib::config_defaults, а затем введены в область видимости my_lib через using namespace.


Любопытно, что что-то похожее мы использовали в RESTinio для упрощения настройки свойств сервера (пример). Но у нас нужно было наследоваться от trait-класса. А тут без наследования, что для каких-то случаев гораздо удобнее.

четверг, 1 мая 2025 г.

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

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

Фильмы

Я слежу за тобой (Geunyeoga jukeotta, 2024). Очень даже неплохо. И сюжет интересный, и подача прикольная, и нет чрезмерного кривляния, присущего азиатским фильмам.

Наследство (Inheritance, 2025). Сильно двойственные чувства. С одной стороны, отличный пример того, как задешево снять хорошую историю. С другой стороны, манера съемки меня стала раздражать уже через 10 минут, а к концу откровенно бесила.

Опустошение (Havoc, 2025). Сам по себе фильм средний, но зато динамичный с большим количеством экшОна и кровищи. А вот что его выделяет -- так это обилие компьютерной графики. Если в последние лет пять-шесть в практически во всех боевиках выстрелы из стрелкового оружия нарисованы на компьютере, то здесь пошли еще дальше. У меня сложилось ощущение, что в "Опустошении" даже сцена погони на автомобилях была отрисована полностью посредством компьютерной графики.

Расплата 2 (The Accountant 2, 2025). Если понравилась первая часть, то можно смотреть и вторую. Но, как по мне, вторая была покруче (в том числе и в том, как были сняты перестрелки), хотя и более занудная.

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

Денежная игла (Body Brokers, 2020). По качеству средне, и не хорошо, и не плохо. Меня история не зацепила, но может быть тем, кому интересны социальные проблемы США и понравится.

Любовь - боль (Love Hurts, 2025). Досмотреть-то досмотрел, но жалею, что не выключил где-то после 25-30 минут. Вообще после просмотра главный вопрос: для кого вообще это снимают. А как рекомендация: не смотреть и не тратить свое время.

Сериалы

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

Тихая гавань (Safe Harbor, 2025). Слишком уж скучно, нужно и затянуто. Уместили бы все это в 4 серии, было бы гораздо лучше. Ну и лично для меня в этом сериале не оказалось ни одного персонажа, за которого не то, чтобы хотелось переживать, но вообще хотя бы следить за его судьбой.

Джек Ричер (Reacher, третий сезон, 2025). Если первые два сезона понравились, то можно посмотреть и третий. Но если уже от второго возникло отторжение, то от просмотра очередного сезона лучше воздержаться. В финальной серии, традиционно, какое-то царство победившего маразма.

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

Начинал, но не закончил

Переходный возраст (Adolescence, первый сезон, 2025). Осилил только первую серию. Мне показалось, что это кино для девочек. Начал смотреть только потому, что говорят, что там каждая серия снята одним дублем. Вот и интересно было увидеть, как это выглядит. ИМХО, именно как кино от такого подхода сериал теряет, т.к. при наличии нормального монтажа ту же самую первую серию можно было бы сделать гораздо драматичнее.

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

вторник, 29 апреля 2025 г.

[prog.c++] Захотелось тут странного для std::vector

Недавно столкнулся с ситуацией, когда хотелось у std::vector вызвать метод resize для увеличения размера вектора, но без инициализации новых элементов. Что-то вроде:

std::vector<some_value> unpacked;
std::size_t num_items = detect_number_of_items_to_unpack(packed_data);
// Явно указываем, что начального значения нет.
unpacked.resize(num_items, std::keep_uninitialized);
// Просто перезаписываем память, которая уже выделена.
unpack_to(packed_data, unpacked.data());

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

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


А еще моя давнишняя мечта иметь в std::vector конструктор, который бы позволял задавать не size, а capacity. Чтобы можно было писать:

std::vector<some_value> unpacked{ std::with_capacity(n) };

вместо:

std::vector<some_value> unpacked;
unpacked.reserve(n);

суббота, 26 апреля 2025 г.

[prog.c++] Еще раз вспомнил про easy_parser из RESTinio

Потребовалось намедни разобрать строку, в которой содержатся разделенные запятыми неотрицательные целые числа. Делал такое недавно посредством std::regex и мне не понравилось. Скорее всего потому, что и с регулярными выражениями на "Вы", и интерфейс std::regex в C++, по моим ощущениям, делали какие-то инопланетяне (не, наверное там все сделано по уму и с прицелом на сценарии, которые мне даже и не снились). Но когда прикасаешься к std::regex раз в два года, то как-то все слишком сложно 🙁

В общем, не понравился мне недавний опыт.

Тут-то и вспомнилась штука, которую мы делали в RESTinio для упрощения разбора HTTP-заголовков. easy_parser называется.

Вспомнилась, решил восстановить в памяти, полез смотреть что и как...

Слегка прифигел от собственной крутизны 🙂

Начать с того, что эта штука была описана. Да, не сильно подробно, но основные принципы изложены. Чтобы вспомнить что к чему этого хватило. Плюс к тому рассказывали про easy_parser на Хабре на русском (здесь, но и здесь есть немного). Т.е. понять как пользоваться можно из описаний, а не из разрозненных демонстрационных примеров.

Продолжить можно тем, что построен easy_parser на простых идеях: есть producer-ы, которые производят значения из разобранного текста, есть consumer-ы, которые потребляют произведенные значения. Связь producer-а и consumer-а образует выражение (clause). Можно делать составных producer-ов из последовательности clause-ов. Вот, собственно, и все.

Если вникнуть в эту незамысловатую схему, то дальше сложностей не возникает.

Да, конечно, когда в дело вступают transformer-ы, или когда нужно порождать не единичные значения, а наборы значений (std::vector чего-нибудь, к примеру), то потребуется въехать чуть поглубже. Но тоже не сказать, чтобы архисложно.

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

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

Но это было давно. Последний раз прикасался к easy_parser, если мне не изменяет склероз, в декабре 2023-го года.

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

Вот что было сделано чтобы разобрать строку с последовательностью чисел, разделенных запятыми:

[[nodiscard]]
std::vector<std::size_t>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto parser = ep::produce< std::vector<std::size_t> >(
            ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container())
         );


   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of dimensions size ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Ключевой момент, имеющий отношение к easy_parser, выделен жирным. Там все просто ;) Нужно получить вектор чисел. Для этого требуется одно число (которое сразу же помещается в контейнер). А за ним может быть еще 0 или более последовательностей из запятой и еще одного числа (которое сразу же записывается в контейнер).

Вот, собственно, и всё.

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

Но я подозревал, что простого списка чисел мне окажется недостаточно.

И, действительно, буквально несколькими часами спустя этот список уже приобрел вид:

size[ '(' start [',' step] ')' [ ',' size [ '(' start [ ',' step] ')' ...]]

Т.е. каждому значению size может быть привязано два необязательных значения start и step. Если start и step есть, то они должны быть заключены в круглые скобки. Может быть либо одно значение start, либо и start, и step. В последнем случае start и step разделяются запятой.

Если я еще не забыл полностью PEG, то в PEG это должно выражаться чем-то вроде:

values := one_value (',' one_value)*
one_value := NUMBER start_step?
start_step := '(' NUMBER (',' NUMBER)? ')'

Для чего показанный выше пример был переписан вот так:

struct DimParamsFromUser {
   std::size_t _size{};
   std::optional<int> _startFrom{ std::nullopt };
   std::optional<int> _step{ std::nullopt };
};

[[nodiscard]]
std::vector<DimParamsFromUser>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto oneDimParamsP = ep::produce<DimParamsFromUser>(
            ep::non_negative_decimal_number_p<std::size_t>()
                  >> &DimParamsFromUser::_size,
            ep::maybe(
               ep::symbol('('),
               ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_startFrom,
               ep::maybe(
                  ep::symbol(','),
                  ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_step
               ),
               ep::symbol(')')
            )
         );

   auto parser = ep::produce< std::vector<DimParamsFromUser> >(
            oneDimParamsP >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               oneDimParamsP >> ep::to_container())
         );

   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of sizes of dimensions ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Если присмотреться, то можно узнать чуть ли не оригинальные PEG-правила. С поправкой на то, что ()? из PEG записывается как maybe, а ()* в C++ коде выражается через repeat(0, N, ...).

Но самое, на мой взляд, красивое -- это то, что определение для переменной parser осталось практически таким же, только `std::size_t` заменили на `DimParamsFromUser`, а `non_negative_decimal_number_p` на `oneDimParamsP`.

Оно, конечно же, понятно, что каждый кулик свое болото хвалит... Но ведь лепота же! 🙂


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

суббота, 19 апреля 2025 г.

[prog.c++] Наткнулся на образчик кода из категории "Да не дай боже такое сопровождать!"

В очередной раз с трудом удерживаюсь, чтобы не ввязаться в публичное обсуждение на профильном ресурсе. В этот раз опять на RSDN ;)

Недавно там образовалась тема с самодельным аналогом std::format/fmt::format. Над происходящим в нёй я уже слегка поугорал в LinkedIn. Но т.к. автор сего велосипеда в излишне поучительном (на мой субъективный взгляд, конечно же) тоне выступает в другом треде, то решил краем глаза вглянуть на то, какой же код производит данный оратор. Ну по принципу talk is cheap, show me the code.

Заглянул в первый попавшийся файл и прифигел (если выражаться цензурным языком, что непросто). Там функция на 700+ строк. Причем вряд ли сгенерированная автоматически, больше похоже на написанную вручную. Еще и с goto, для полноты ощущений.

Как по мне, так подобное не что иное как говнокод. Говнокод как он есть. В чистом, дистиллированном виде.

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

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


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


Простите, дальше будет совсем грубо.

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

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

Но при этом этот работающий код оказывается откровенным говном. Как в примере выше.

Но работающим же.

И когда такому коллеге пытаешься объяснить, что вообще-то так нельзя, у них есть убийственный аргумент: "Так оно же работает!"

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

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

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

среда, 16 апреля 2025 г.

[prog.thoughts] Реплика в блог: так ли плохо иметь в C++ отдельный класс для сетевого пакета?

Поскольку я зарекся вступать в публичные споры на LOR/RSDN/Habr, а в Интернете, как водится, кто-то неправ, то попробую разместить свою ремарку здесь, в уютненьком ;)

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

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

Как мне показалось, одна из проблем данного спора в том, что некоторые товарищи путают представление данных в рамках некого условного протокола обмена данными и сам протокол.

Представление данных -- это всего лишь конкретный механизм сериализации. Скажем, использование принципа TLV (Tag-Length-Value), как в ASN.1 BER. Или плотная побитовая упаковка как в ASN.1 PER. Или же тегированное текстовое представление, вроде JSON или XML (должны сдохнуть в муках оба, шутка).

Кусок кода, который привели в обсуждении на RSDN, он как раз про представление данных.

Тогда как есть еще одна важная часть, когда мы говорим о каких-то протоколах обмена данными -- это из каких сообщений (PDU -- protocol data unit) состоит сам обмен.

Типа того, что есть сообщение handshake и ответное сообщение handshake_ack. Есть сообщение subscribe и есть ответы sub_ack и sub_nack. Есть сообщение publish и есть ответные pub_ack и pub_nack, а также сопутствующее delivery_report. И т.д., и т.п.

Так вот, представление данных определяет то, как содержимое PDU сериализуется.

Но представление данных не есть удобный способ работать с самими PDU в коде.

Достаточно часто в наше время встречается ситуация, когда PDU представляется в виде JSON. Входящий пакет парсится, у нас в коде появляется что-то вроде nlohman::json или RapidJson::Value. И дальше программисты любятся с этими JSON-объектами как умеют. А умеют практически никак 😣 Например, если в PDU есть поле priority, то из JSON-объекта запрашивают это поле вручную по строковому имени. Если это поле нуждается в какой-то валидации (хоть при извлечении, хоть при сохранении), то это тоже делают вручную. Если делают.

Мне же думается, что работать с PDU в программе гораздо удобнее, когда PDU представляется в виде конкретного C++ного класса с нужными методами getter-/setter-ами. И когда в эти самые методы вставлена валидация значений. Более того, такие классы могут (и должны, когда есть возможность) проверять корректность значений при сериализации/десериализации. Например, если поле A в PDU имеет вот такое значение, то поле B не может быть пустым, а поле C, напротив, должно быть пустым.

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

Однажды в прошлом с чем-то таким столкнулся. Даже рассказывал об этом на RSDN и упоминал в блоге. То решение на Ruby оказалось удобным. Если доведется еще раз с подобной задачей, то буду смотреть в ту же сторону (не важно, Ruby будет использоваться для кодогенерации, Python или еще что-то). Хотя современный C++ в области шаблонной магии далеко ушел от C++03, но все равно не думаю, что подобная задача будет хорошо решаться в рамках C++20 или C++23.


PS. Еще раз по поводу кода, который показали на RSDN. Как раз тот случай, когда смотришь в код, внутренний голос спрашивает "а что так навороченно то, нельзя ли пропроще?", но без вдумчивого изучения ответа на этот вопрос нет. А вдумчиво изучать нет желания ;)


PPS. Мораль всего поста: не беритесь судить без глубокого погружения.

понедельник, 14 апреля 2025 г.

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

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

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

Изредка приходится делать маленькие (или не очень маленькие) программки "на выброс". Буквально на один-два проверочных запуска, после чего все написанное отправляется в корзину. Вот там, как правило, вообще никаких комментариев нет. Точнее не бывало. До недавних пор.

Как раз давеча пришлось такими маленькими программками заниматься.

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

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

Чтобы не быть голословным, вот эти одноразовые программулины, о которых я только что говорил. С этого все начиналось, а вот к чему в итоге пришло: тыц, тыц и тыц. Сам в шоке.

Весь этот код, по сути, одноразовый. Заливал его на github только ради простоты переноса между разными компьютерами. Ну и экспериментировать с кодом, когда он под контролем версий, таки спокойнее и удобнее.

PS. Неприятным побочным эффектом от привычки писать комментарии становится все возрастающая нетерпимость к коду без комментариев.

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

PPPS. Проблема с бесполезными и тривиальными комментариями не в том, что они бесполезны. Проблема в том, что программистов не научили комментарии писать. Поэтому эти самые программисты и пишут бесполезные комментарии. Ну не умеют писать полезные и это объясняет текущее состояние дел почти на 95%.

PPPPS. Есть у меня ощущение, что в последние 15 лет программистов-то и программировать уже не учат. Что уж тут говорить про обучение написанию комментариев 🥺 ИМХО, это один из китов, на которых покоится миф о самодокументирующемся коде.

суббота, 12 апреля 2025 г.

[prog.c++] Довелось тут вспомнить свой старый PR в spdlog

В проект, которому сейчас помогаю, коллеги подключили spdlog. В частности, потребовался функционал автоматической ротации log-файлов. Но, к сожалению, не без сложностей т.к. штатный rotating_file_sink не подошел. Генерация имен новых log-файлов в нем захардкожена намертво, а требовалось добавлять в каждое имя специфическую для проекта информацию. Пришлось делать свой тип sink-а.

В rotating_file_sink за формирование имени очередного файла отвечает статический метод calc_filename, вызов которого зашит в код rotating_file_sink. Поэтому просто так отнаследоваться от rotating_file_sink и переопределить calc_filename в собственном производном классе нельзя.

Пришлось коллегам копипастить код rotating_file_sink к себе в проект и дорабатывать его напильником в нужных местах.

Внезапно™ вспомнилось, что когда-то давно я сам засылал PR в spdlog, который даже был принят, но забылось что именно я там предлагал делать. А оказалось, что предлагал как раз решение подобной проблемы. Но только для daily_file_sink.

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

Удивительно то, что хотя я заслал это изменение девять лет назад, этот подход с дополнительным параметром шаблона почему-то не применяется для rotating_file_sink. Хотя используется, например, в hourly_file_sink. Такое ощущение, что соответствующий PR никто не присылал, а автор spdlog не стал переделывать старую реализацию rotating_file_sink.


Думаю, что если бы я готовил подобный PR сейчас, то постарался бы обойти одну небольшую проблемку, которая есть в параметре FileNameCalc. Т.к. метод calc_filename у FileNameCalc должен быть статическим, то мы имеем дело со stateless-генерацией имен файлов. Что OK в большинстве случаев, но не всегда. Иногда может быть нужно, чтобы calc_filename сохранял какую-то информацию для использования в следующем вызове. Но сохранять ее негде. Поэтому сейчас бы я думал в сторону того, чтобы поддерживалась stateful-генерация имен.

В C++20 это вообще бы не представляло проблемы:

template <typename Mutex, typename FileNameCalc = daily_filename_calculator>
class daily_file_sink final : public base_sink<Mutex> {
  [[no_unique_address]]
  FileNameCalc _filеNameCalc;
  ...
public:

Если фактический тип FileNameCalc пуст, то _fileNameCalc не увеличивал бы размер daily_file_sink.

Но в более старых версиях C++ пришлось бы наследоваться от FileNameCalc дабы воспользоваться empty base optimization. Что не так очевидно и красиво.

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

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