среда, 1 октября 2025 г.

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

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

Фильмы

Диспетчер (Relay, 2024). Не шедевр, но весьма неплохо. А на фоне остального шлака так даже и хорошо.

Священная дорога (Hallow Road, 2025). Мне понравилось как фильм сделан. Разочаровал разве что открытый финал, который оставляет зрителя без конкретного ответа о том, а что же это было. Но фильм специфический, моей жене, например, совсем не понравился. Так что рекомендовать к просмотру не возьмусь.

84 м² (84jegopmiteo, 2025). Странный фильм. Если нет опыта просмотра корейского кино, то может сильно удивить и разочаровать. Ибо он даже для корейского кино странный. Но мне было интересно.

Никто 2 (Nobody 2, 2025). Я воспринял этот фильм как своеобразную черную комедию с кучей трупов. В этом жанре вполне себе норм. Ни разу не шедевр, но норм. С первым же фильмом, как по мне, сравнивать вообще нельзя, как будто два совершенно разных кино.

Сверху вниз (Highest 2 Lowest, 2025). Скучно и затянуто, плюс на мой взгляд актерская игра в среднем никакая.

Хани, не надо! (Honey Don't!, 2025) Не понял ни что это, ни зачем это. Не увидел ни цели, ни смысла, ни сюжета. Потерянного времени жалко.

Сериалы

Больница Питт (The Pitt, 2025). Мне понравилось, хорошее кино.

13 клиническая. Начало (2024). Если понравился сериал "13 клиническая", то смело можно смотреть и этот. Мне понравились оба.

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

Чужой: Земля (Alien: Earth, первый сезон 2025). Редкая хрень. Хотя даже несмотря на творившийся маразм было три серии, которые смотрелись интересно и в них чувствовалась атмосфера "Чужих". Но всего три серии из восьми... В общем, не рекомендую и следующий сезон, если даже его решаться сделать, смотреть не собираюсь.

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

Орудия (Weapons, 2025). Не могу оценить. Может быть слишком давно не смотрел подобного кино. Может быть стал слишком старый для этого жанра. Но не оценил. Как по мне, текущая оценка в 7.1 на Кинопоиске слишком завышена.

Миссия невыполнима: Финальная расплата (Mission: Impossible - The Final Reckoning, 2025). Самое точное определение этому дала жена во время просмотра: "Это уже не фантастика и даже не фэнтези -- это уже маразм".

вторник, 16 сентября 2025 г.

[prog.flame] Пример комментария, который бы хотелось видеть в коде сразу

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

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

Однако, в этот раз я получил разрешение от заказчика на публикацию небольшого фрагмента. Без подробностей, просто в качестве иллюстрации.

Исходно был вот такой кусочек:

    catch (const std::exception& e)
    {
        // если были записаны данные, то нужно обновить все
        // FIXME: тут пока не будем разделять
        updateAfterSetCells(std::move(isf), anyUpdated);
        throw(e);
    }

    // обновление таблицы консолидации на основании новых ячеек
    updateAfterSetCells(InsertionStatus_Facts{ getCubeIndex() }, anyUpdated);

    return isf;
}

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

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

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

    catch (...)
    {
        // Не важно что за исключение возникло.
        //
        // Часть ячеек могла быть изменена/добавлена, и эта часть
        // должна быть зафиксирована. Если не делать фиксацию, то
        // часть информации об ячейках может быть потеряна, а
        // FactsTable останется в непонятном состоянии (т.к. внутри
        // FactsTable сейчас идет промежуточное накопление новых фактов
        // для оптимизации их добавления, эти новые факты в промежуточном
        // буфере внутри FactsTable должны быть сохранены).
        //
        // ПРИМЕЧАНИЕ: важно то, что в updateAfterSetCells идет объект
        // isf, возможно, с актуальной локальной копией FactsTable.
        // Если эта локальная копия существует, то будут обновлены и
        // фидеры. Это обновление фидеров должно быть сделано, ведь из-за
        // выброса исключений не будет обновление фидеров внутри процесса.
        //
        // FIXME: тут пока не будем разделять
        updateAfterSetCells(isf, tableCopy, anyUpdated);

        throw;
    }

    // Обновление таблицы консолидации на основании новых ячеек
    //
    // ПРИМЕЧАНИЕ: этот вызов принципиально отличается от аналогичного
    // в блоке catch потому, что обновление фидеров (если в isf есть актуальная
    // локальная копия FactsTable) будет явным образом выполняться процессом
    // выше по стеку.
    //
    // Специально отдаем в updateAfterSetCells пустой InsertionStatus_Facts,
    // чтобы не было обновлений фидеров.
    InsertionStatus_Facts emptyIsf{ getCubeIndex() };
    updateAfterSetCells(emptyIsf, tableCopy, anyUpdated);

    return isf;
}

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

суббота, 13 сентября 2025 г.

[prog.c++] Наткнулся на любопытное с std::ranges::views::iota

Вчера мое утро началось с того, что под Linux-ом компилятор GCC 13 отказался компилировать код вроде вот такого (это не реальный код, а минимизированая версия на которой проблема воспроизводится):

template< typename T, typename Range >
[[nodiscard]]
T
make_from( const Range & r )
{
   return T{ r.begin(), r.end() };
}

int main()
{
   std::string src{ "Hello, World" };
   auto v = make_from< values_container >(
         std::ranges::views::iota( 0u, src.size()) );

   std::cout << v.front() << " - " << v.back() << std::endl;
}

Изначально код был написан под другую платформу с более свежим С++ным компилятором, поэтому там make_from был реализован иначе, проблем с компиляцией не было. А вот под уже довольно древним по современным меркам GCC возникли проблемы: компилятор не видел у values_container конструктора, который бы получал два итератора.

Конкретно в моем случае в качестве values_container был folly::fbvector, но это не суть. Вполне мог бы быть и std::vector, проблема была бы точно такой же.

Ошибка несколько обескуражила, т.к. я точно знал, что у values_container есть конструктор, принимающий итераторы first и last.

Но с этим-то удалось разобраться достаточно быстро: first и last должны быть одного типа. А в моем случае объект-рэндж, возвращенный вызовом iota, имел разные типы для begin() и для end(). Соответственно, раз типы итераторов разные, то и компилятор не может найти подходящий конструктор у values_container.

Ну OK, раз типы у итераторов разные, то можно к этой ситуации приспособить реализацию make_from. Например, написать ее так:

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

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

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

Фильмы

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

Последний выстрел (Dead Shot, 2023). Простенько, но атмосферно. Нормально рассказанная история, за которой интересно следить, но не шедевр.

Эники-Беники (Eenie Meanie, 2025). Странное кино: начиналось как дурацкая молодежная комедия, закончилась же серьезной драмой с кучей трупов.

Долина теней (Valle de sombras, 2023). Это не "криминал", как заявлено в описании, а драма. Красиво снятая, но скучноватая.

Мир Юрского периода: Возрождение (Jurassic World: Rebirth, 2025). Скучно и предсказуемо, ничего не цепляет. Местами очень красиво снято и нарисовано, местами очень заметно, что съемки велись на фоне зеленого экрана. Но в целом сойдет, чтобы скоротать семьей вечер перед экраном.

Ограбление (The Pickup, 2025). Очень и очень посредственно. Можно глянуть только если больше ну вот совсем ничего нет.

Сериалы

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

Город тайн (первый сезон, 2020). Нормально, хотя было несколько моментов по ходу событий, когда возникал вопрос "но как так-то?"

Бабочка (Butterfly, первый сезон, 2025). Тупенько, но бодренько. Пожалуй, главное достоинство -- это короткий сериал.

В глуши (Untamed, первый сезон, 2025). Затянуто. Есть ощущение, что все события можно было бы уместить если не в две серии, так в три точно.

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

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

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


Специализированный mutex, который в сложившейся терминологии C++ной библиотеки можно было бы назвать как recursive_shared_timed_mutex.

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

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

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


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

В моем представлении версионированные структуры -- это такие, в которых каждый пользователь видит свою независимую версию этой самой структуры.

Чтобы было понятнее, давайте вообразим себе std::map<int, std::string>, с которым несколько нитей работают одновременно. Первая нить читает значения по ключам 0, 1, 2, 3, 4, 5, вторая нить читает значения по ключам 4, 5, 6, 7, 8, а третья нить изменяет значение для ключа 0, удаляет значение для ключа 5 и добавляет значения с ключами 9, 10 и 11.

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

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

При этом нити №1 и №2 все равно не будут видеть новую версию, поскольку они работают с map через "старую" ссылку. Чтобы перейти на актуальное содержимое map им нужно обновить эту ссылку у себя, т.е. явным образом обратиться к map с просьбой дать ссылку на последнюю актуальную версию.

Может оказаться так, что кроме нити №3 модификации в map сейчас вносит и нить №4. И свой commit четвертая нить может выполнить еще до того, как третья нить завершит свои модификации. В этом случае при commit-е нить №3 обнаружит, что актуальный map уже совсем другой.

Это все сильно напоминает работу с ветками в VCS: ты отпочковался от master-а (или trunk-а), сделал изменения в своей ветке, хочешь влить в master, а master-то уже ушел вперед. И нужно делать слияние (merging). Иногда оно происходит автоматически и безболезненно, а иногда приходится разбираться с конфликтами.

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

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

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

четверг, 21 августа 2025 г.

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

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

Эта привычка помогала и после перехода на Си, а затем и на C++.

Почему-то мне казалось, что в те старые времена и в Си, и в C++ была культура использования typedef-ов для создания псевдонимов типов. Вероятно из-за того, что платформ было много, платформы были разные. А псевдонимы, сделанные через typedef, помогали справляться с их различием. Достаточно вспомнить приключения при переходе от 16 бит к 32 битам и обратно.

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

void do_something(
  std::vector<std::pair<int, std::shared_ptr<my_obj>>> & first,
  std::map<std::string, std::tuple<int, int, std::string>> & second);

А еще хуже, на мой взгляд, когда один и тот же фундаментальный тип (вроде int-а или unsigned long-а) используется для совершенно разных целей:

struct some_item {
  int _x;
  int _y;
  int _width;
  int _height;
  int _usage_percentage;
  int _reusing_mode;
  int _logging_level;
  ...
};

Когда со временем меняешь int на что-нибудь другое, то обязательно выясняется что где-то как-то параметры перепутали -- выдали _width за _height или _usage_percentage вместо _reusing_mode.

Конечно, от многих подобных проблем в C++ можно было бы избавиться, если бы в C++ из коробки была поддержка strong typedef. Но даже в ее отсутствии обычные псевдонимы типов, сделанные через using, сильно повышают читабельность кода и упрощают его сопровождение (или портирование на другую платформу).

Вроде бы уже не раз говорил в разных местах о достоинствах using-ов:

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

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

Но если мне это очевидно, то почему это не очевидно другим C++разработчикам?

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

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

PS. Очень надеюсь, что рано или поздно, но C++ обзаведется штатным, доступным прямо в стандартной библиотеке, механизмом strong typedef. И если в вашем коде using используется должным образом, то переход к применение strong typedef-а окажется более простым и менее болезненным.

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

[prog] Мои подходы по именованию веток в git-е

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

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

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

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

С большим удивлением для себя регулярно обнаруживаю, что часто в именованиях веток нет вообще никакой системы. Обычно встречаются ветки с именами "bug-fix" или "memory-usage". Иногда к именам веток добавляются префиксы типа "bug/" (тогда получаются имена вида "bug/login-crach" или "bug/memory-leak") или "feature/" (тогда получаются имена вида "feature/open-auth-login" или "feature/sparse-table"), или "experimental/" (тогда получаются имена вида "experimental/simd-json"). Хотя, как по мне, эти префиксы не особо-то и полезны.

Но самое плохое, что лично мне такие имена ни о чем не говорят 🙁

Ситуация получше когда для всех задач проекта есть тикеты в условной JIRA и номер тикета. Особенно когда к номеру тикета добавляется еще что-то поясняющее, вроде "issue-1416-attempt-2". Но и это не идеал.

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


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

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

  • имя мажорной версии продукта. Например, сейчас развивается версия 5.8 для SObjectizer. Поэтому первая составляющая будет иметь вид "5.8-dev", т.е. основная dev-ветка для версии 5.8;
  • имя конкретной версии продукта. Например, сейчас в работе версия 5.8.5, которая пока до релиза не дошла. Поэтому вторая составляющая будет иметь вид "5.8.5";
  • краткое именование того, над чем идет работа. Например, "reduce-state-size". Возможно, с указанием того, какая это итерация, например, "reduce-state-size-v2".

В результате получаются ветки с именами "5.8-dev-5.8.5-reduce-state-size-v2".

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

5.8-dev
5.8-dev-5.8.5
5.8-dev-5.8.5-cmake-files-update
5.8-dev-5.8.5-issue-96
5.8-dev-5.8.5-state-time-limit-v2
5.8-dev-5.8.5-issue-93
...

Можно обратить внимание, что имена образуют иерархичность и указывают что и куда затем должно вливаться. Так, изменения из "5.8-dev-5.8.5-cmake-files-update" будут влиты в "5.8-dev-5.8.5", а затем изменения из "5.8-dev-5.8.5" будут влиты в "5.8-dev".

Как-то у меня не прижился прямой слэш в качестве разделителя, поэтому мне удобнее работать с именами вида "5.8-dev-5.8.5-cmake-files-update", а не с "5.8-dev/5.8.5/cmake-files-update". Вкусовщина, конечно же, но у меня вот так.


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

  • никнейм основного автора кода в ветке, в моем случае это "eao197";
  • дата начала работы над веткой в формате YYYYMMDD;
  • краткое именование того, над чем идет работа. Например, "reduce-state-size". Возможно, с указанием того, какая это итерация, например, "reduce-state-size-v2".

Что в результате дает имена веток вида: "eao197-20250814-versioned-data-v2-tmp". Если для задачи есть номер тикета, то он включается в третью часть. Типа "eao197-20250818-issue-77889-attempt-1".

Такое именование лично для меня оказывается полезным тем, что:

  • сразу видно кто отвечает за ветку. В частности, мне легко понято что было сделано (и/или забыто) лично мной;
  • степень "свежести" изменений. Когда прямо в имени ветки видишь, что она относится к декабрю 2023-го года, то это сразу наводит на мысль, что код в ней довольно древний. Можно даже не делать git log 😉

К счастью, пока мне за такую схему по рукам не били. Видимо, она устраивает не только меня. По крайней мере хочется в это верить.


Интересно, а какие схемы именования веток в git-е вы считаете удобными и, может быть, даже практикуете?