вторник, 2 декабря 2025 г.

[prog.c++] Интересно, а какой код понятнее?

В современном C++ одни и те же вещи можно сделать по разному.

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

Можно сделать это вот так:

template<typename... Types>
void for_each_type_via_lambda(int arg) {
    const auto action = [arg]<typename T>() {
        std::cout << typeid(T).name() << " - " << arg << std::endl;
    };
    (action.template operator()<Types>(), ...);
}

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

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

template<typename T>
void do_something(int arg) {
    std::cout << typeid(T).name() << " - " << arg << std::endl;
}

template<typename... Types>
void for_each_type(int arg) {
    (do_something<Types>(arg), ...);
}

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

Интересно, а какой из вариантов более понятен для вас?

PS. Этот пример на wandbox "для поиграться".

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

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

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

Фильмы

Хищник: Планета смерти (Predator: Badlands, 2025). Посмотрел с удовольствием, мне понравилось. Наверное, это лучшее, что сняли после второй части "Хищника".

Кто она? (The Artifice Girl, 2022). Мне понравилось. Но не-айтишникам следить за происходящим на экране может быть сложновато.

Операция "Наполеон" (Napóleonsskjölin, 2023). Это что-то вроде современного Индианы Джонса на минималках, бюджет там явно был мизерный. Но по итогу на удивление неплохо и вполне себе смотрибельно.

Геля (2025). Вполне смотрибельно. Не всё понял по сюжету, но фильм смешной.

Франкенштейн (Frankenstein, 2025). Отлично снятая сказка. Если такой жанр нравится, то смело можно смотреть.

Призрак на поле боя (Un fantasma en la batalla, 2025). По сюжету очень перекликается с испанским же Агент под прикрытием. Но "Призрак..." чуть более суровый и мрачный, как мне показалось. Не шедевр, но смотреть можно.

Святая ночь. Охотники на демонов (Georukhan bam: demon heonteoseu, 2025). Простенько, бюджетненько, но вполне себе смотрибельно. Однако, этот фильм, скорее всего, для очень узкой аудитории: для тех кому нравится корейское кино и, одновременно, кино про демонов и экзорцизм.

Разрушитель миров (Worldbreaker, 2025). В принципе, не самое плохое фэнтези для подростков. Но мне совершенно не хватило экшОна, а без экшОна повествование получилось пресным и затянутым.

Туман (2023). Снято красиво, да. Но это единственное, что есть хорошего в фильме. Сама рассказанная история выглядит каким-то бредом.

Укрытие номер один (Safe House, 2025). Очень бюджетно и очень тупо. Лучше пройти мимо этого фильма.

Сериалы

Константинополь (первый сезон, 2025). Разочарован. Ждал чего-то вроде современной версии фильма "Бег", но увидел что-то невнятное про борьбу вымышленных ОПГ в вымышленном псевдоисторическом антураже. Жаль потраченного времени.

Лихие (оба сезона, 2024-2025). Развязка просто говно говна. Поэтому не рекомендую тратить на это кино свое время.

Ночной экспресс (Nightsleeper, первый сезон, 2024). Купился на высокий рейтинг на Кинопоиске. Редкий маразм. Жаль потраченного времени.

пятница, 28 ноября 2025 г.

[prog.c++] История с потреблением памяти из-за фрагментации хипа в mimalloc-е

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

Что, собственно, до некоторых пор и происходило: по показаниям htop было видно, как сперва потребление памяти растет, затем резко падает.

Но в какой-то момент поведение изменилось: потребление памяти росло во время первичной обработки, потом уменьшалось, но уменьшалось не настолько, насколько ожидалось. Грубо говоря, раньше в пике съедали 1Gb, затем падали до 400Mb. Теперь же в пике съедаем 1Gb, но затем падаем всего до 600Mb.

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

Сразу извинения: я не уверен на 100%, что термин "фрагментация" здесь является точным. Но лучшего, увы, не придумал. Поэтому прошу гнилыми помидорами не бросаться.

В проекте в качестве дефолтного аллокатора используется mimalloc. Хвала его создателям, в API mimalloc-а есть средства для интроспекции содержимого хипа, что и позволило обнаружить эффект фрагментации.

Нужно сказать, что mimalloc спроектирован так, чтобы максимально избавиться от вероятности фрагментации (насколько это в принципе возможно для языков без compacting GC): он использует арены (areas) под объекты фиксированного размера. Так, объекты размером в 16 байт создаются в своей арене, объекты размером в 32 байта -- в своей, размером 48 байт -- в своей и т.д. Тем самым mimalloc частично избавляется от эффекта "дырок в сыре" -- когда суммарно свободной памяти много, но вся она рассредоточена в мелких фрагментах, чередующихся с занятыми фрагментами.

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


Почему же так произошло?

Первичная обработка данных при старте создает в динамической памяти множество объектов. Среди них огромный процент объектов как раз имеют размер 16 и 48 байт. Под них mimalloc динамически выделяет все новые и новые арены, запрашивая память у ОС.

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

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

Происходило это потому, что в процессе обработки начали создаваться не только "старые" объекты размером 16 и 48 байт, но и "новые" -- внутри этого самого нового контейнера. Только вот содержимое нового контейнера должно было остаться до конца работы программы, поэтому эти "новые" объекты не удалялись. В отличии от "старых" объектов, нужных только на время первичной обработки.

Получалось, что сплошным потоком создаются объекты размером, например, 48 байт. Львиная их часть относится к "старым" объектам, но иногда создаются и "новые". Пропорция приблизительно 500 "старых" объектов к одному "новому". Т.е. создали 500 "старых" объектов, затем один "новый", затем еще 500 "старых", затем еще один "новый" и т.д.

Физически это выглядит так, что на очередной арене mimalloc-а подряд размещается 500 "старых" объектов, за ними один "новый", затем еще 500 "старых", следом еще один "новый", затем арена заканчивается, начинается новая, на которой сперва идет 500 "старых" объектов, затем один "новый" и т.д.

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

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


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

Отсюда и возникала пропорция одного "нового" объекта (тот самый узел дерева) к пятистам "старым" объектам.

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


Для меня главный практический вывод -- это то, насколько языки с GC (особенно с продвинутыми compacting GC) могут облегчить жизнь программиста. Да, там возникают другие проблемы. Но в данном случае compacting GC был бы очень в тему, на мой взгляд.

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

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

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

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

[prog.c++] Не нужно делать собственные специализации std::hash<std::pair<T,U>>

Некоторое время назад я сам наткнулся на грабли при использовании библиотеки Folly. Мне потребовался std::hash для std::pair<int, some_my_type>, при этом std::hash для some_my_type у меня уже был. Поэтому я сделал специализацию std::hash для std::pair<int, some_my_type>, в которой использовал std::hash<some_my_type>. Собрал код, стал проверять и выяснил, что на некоторых тестовых сценариях происходит что-то странное. Начал разбираться и внезапно обнаружил, что мой std::hash для std::pair<int, some_my_type> не вызывается вообще.

Дело оказалось в том, что разработчики Folly подложили свинью своим пользователям и определили специализацию std::hash<std::pair<T, U>>. Чего делать по стандарту нельзя (за цитату из стандарта большое спасибо Константину Шарону):

• [namespace.std] (C++23 draft, §16.4.2.2/1):
The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std, unless otherwise specified.
• The only allowed exception is that you may specialize certain templates in std for user-defined types.

[unord.hash] (C++23 draft, §24.5.4.2/1): For all object types Key for which there exists a specialization std::hash, the program may define additional specializations of std::hash for user-defined types.

Тут особый упор нужно сделать на "for user-defined types", тогда как std::pair<T, U> -- это не user-defined type. Вот std::pair<int, some_my_type> -- это уже user-defined type и для него специализацию std::hash делать можно.

Об этом несколько месяцев назад я писал в LinkedIn. Но недавно на Reddit-е обнаружил ссылку на статью Simplify hashing in C++ в которой, ни много, ни мало, предлагается делать тоже самое, т.е. определять специализации std::hash для std::pair<T, U>.

Поскольку такое заблуждение существует, то приходится заострять на этом внимание. Поэтому, пожалуйста, не делайте собственные специализации std::hash<std::pair<T, U>>.

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

  • вы разрабатываете программу и определили какой-то собственный тип your_type;
  • вы захотели использовать your_type в качестве ключа в unordered_map и сделали для your_type специализацию std::hash;
  • затем вы захотели сэкономить себе время и силы и сделали специализацию std::hash для std::pair<T, U>. Теперь у вас автоматически поддерживается хеширование и для std::pair<int, your_type>, и для std::pair<your_type, int>, и прочих сочетаний your_type с другими типами, поддерживающими кэширование;
  • где-то в программе вы используете std::unordered_set<std::pair<int, your_type>>
  • все у вас работает нормально;
  • затем в один прекрасный момент вы подключаете в проект стороннюю библиотеку, в которую отдаете your_type, и которая использует std::pair<int, your_type> в качестве ключа в Folly::F14FastMap;
  • и тут оказывается, что у вас в проекте есть два определения для std::hash<std::pair<int, your_type>>

Поздравляю, у вас в коде Undefined Behaviour из-за нарушения one definition rule.

Реально хотите собственными руками подсаживать UB в код?


Разработчиков Folly понять можно: они делали Folly для своих собственных нужд и в их программах, разработанных под задачи Facebook-а, вряд ли могут встретиться другие специализации для std::hash. А то, что Folly применяют и за пределами Facebook-а, в том числе смешивая в одном проекте Folly с кучей других библиотек -- это не проблемы разработчиков Folly.

пятница, 21 ноября 2025 г.

[prog.c++] SObjectizer-5 пятнадцать лет!

Неумолимо быстро летит время и вот уже SObjectizer-5 исполняется пятнадцать лет.

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

Но прежде чем перейти к подробностям, слова искренней благодарности всем, кто тем или иным способом поддерживал наш проект. Вряд ли бы мы справились, если бы не ваш интерес и внимание. Спасибо от всей души!


Работа над SObjectizer в последние пять лет шла урывками.

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

Т.е. зарабатывать на жизнь приходится не развивая SObjectizer за деньги внешних спонсоров, а трудясь на чужих проектах, зачастую никак с SObjectizer-ом (или RESTinio) не связанных. Что отнимает значительную часть времени и сил. А уже на том, что остается, делаются очередные версии SObjectizer-а.

Такова была ситуация в последние годы. Таковой она, судя по всему, и останется в будущем.

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

Тем более что...


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

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

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

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


Одна из вещей, которой мы уделяли особое влияние, была совместимость между версиями SO-5. В течении почти пяти лет мы старались не ломать совместимость в рамках ветки 5.5. В середине 2019-го появилась ветка 5.6, в которой пришлось резать по живому.

А за 5.6 последовали ветки 5.7 (январь 2020-го) и 5.8 (июнь 2023-го). Но их я сам рассматриваю как последовательное развитие ветки 5.6. К сожалению, без ломающих изменений не обошлось. Однако, они не настолько кардинальные, как это было при переходе от 5.5 к 5.6. В основном изменения между 5.7 и 5.8 в касались очень специфических внутренних интерфейсов SObjectizer-а и в гораздо меньшей степени затрагивали обычный пользовательский код.

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


Есть одна важная фраза, написанная пять лет назад:

В общем, SObjectizer-5 поступательно развивается уже 10 лет, что лично меня приятно удивляет. Ведь запаса SObjectizer-4 хватило всего-то на 3-4 года. Тогда как SObjectizer-5 может эволюционировать еще несколько лет и насущной необходимости делать условный SObjectizer-6 на новых идеях пока не видно.

Можно констатировать, что время подтвердило эти слова: SObjectizer-5 все еще развивается и насущной необходимости делать условный SObjectizer-6 на новых идеях пока что не видно.

Это значит, что из опыта SObjectizer-4 были сделаны более чем правильные выводы.


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

В 2021-ом году SObjectizer-ом заинтересовался Марко Арена, лидер сообщества Italian C++ Community. И со временем он внес очень существенный вклад как в популяризацию SObjectizer-а, так и в его развитие. Так, сперва мой коллега, Коля Гродзицкий, сделал доклад о SObjectizer-е на виртуальном митапе. Затем Марко опубликовал замечательную серию статей о SObjectizer-е. А недавно Марко еще и выступил с собственным докладом о SObjectizer-е. Это первый из известных мне докладов о SO-5, который был сделан кем-то не из команды разработки SObjectizer-а.

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

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


Ну и самый важный лично для меня аспект, а именно ответ на вопрос "почему я все еще занимаюсь SObjectizer-ом?"

Пять лет назад я уже дал исчерпывающий ответ на этот вопрос. Но этот ответ был актуален на 2020-й год: главной (но не единственной) причиной было желание сделать из SObjectizer-а полноценный продукт за который не стыдно.

Это было сделано. За SObjectizer-5 уже давно не стыдно. И за прошедшее время, надеюсь, наша разработка стала только лучше.

Но если эта цель была достигнута, то что движет мной теперь?

Вероятно, действуют вот эти причины:

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

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

Состояние SObjectizer-а и его перспективы

Брать или не брать такой SObjectizer в работу?

Там все уже было сказано. Сказать лучше не получится. Надеюсь только, что сам факт развития SObjectizer-5 в течении 15 лет что-то да говорит.


Чем сейчас можно помочь SObjectizer-у?

На мой взгляд, сейчас, как и 5 лет назад, SObjectizer больше всего нуждается в двух вещах:

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

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

Добавлена возможность брать под контроль создание рабочих нитей в штатных диспетчерах SObjectizer-а: тыц.

В класс agent_t добавлен метод so_deactivate_agent: тыц. Затем в довесок был добавлен метод so_drop_all_subscriptions_and_filters: тыц.

Для агента теперь можно назначить собственный MPSC-mbox в качестве direct-mbox-а: тыц.

Теперь delivery filters можно устанавливать даже на MPSC-mbox-ы: тыц.

Серьезно изменился интерфейс abstract_message_box_t. Добавилось понятие delivery_mode для того, чтобы можно было понять из какого контекста сообщения отсылаются (например, если сообщение идет с нити таймера, то в этом случае нельзя блокировать отправителя сообщения). При этом теперь mbox-ам не нужно заниматься контролем message limits. Тыц.

Добавлено понятие message_sink-а. Теперь в качестве подписчика для mbox-а может выступать не только агент, но и вообще произвольна сущность, реализующая интерфейс abstract_message_sink: тыц.

Добавлена возможность регистрировать в SOEnv именованные mbox-ы, которые были созданы пользователем, а не SObjectizer-ом: тыц.

Сделаны шаги в сторону лучшего обеспечения noexcept-гарантий со стороны SObjectizer-а при выполнении дерегистрации кооперации: изменен интерфейс event_queue_t, добавлены диспетчеры nef_one_thread, nef_thread_pool: тыц.

В класс agent_t добавлены методы so_5::agent_t::so_this_agent_disp_binder() и so_5::agent_t::so_this_coop_disp_binder() чтобы можно было узнать, с какими диспетчерами агент связан: тыц.

Появилась поддержка имен для агентов: тыц.

Расширен набор инструментов для юнит-тестирования агентов: тыц и тыц, и тыц.

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


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

вторник, 18 ноября 2025 г.

[prog.c++.sobjectizer] Релиз версии 5.8.5

Намедни состоялся релиз очередной версии 5.8.5.

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

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

По итогу, в 2025-ом удалось сделать два обновления для SO-5 -- одно в январе, одно в ноябре. Что уже неплохо. А если до конца года получится сделать еще одно, хотя бы даже самое маленькое, то будет вообще фантастика.

Очень вероятно, что версия 5.8.5 стала последней, в которой для SObjectizer-а еще используется моя собственная build-система для C++ под названием MxxRu. К сожалению, современным реалиям она уже не удовлетворяет -- нет поддержки параллельной сборки. В теории, можно было бы тряхнуть стариной и попробовать добавить такую возможность в MxxRu.

Но есть другая беда в виде модулей в C++20, которые комитет почему-то счел возможным добавить в стандарт. И вот тыкать эту известную субстанцию не хочется даже трехметровой палкой. Поэтому MxxRu таки уходит в историю. Что печально, т.к. к CMake я все никак не могу привыкнуть и для моего стиля работы в ряде моментов CMake ну вот категорически неудобен 🥺

Если затронуть вопрос дальнейших планов, то в черновом варианте они таковы:

  1. изъять следы MxxRu из SObjectizer-а;
  2. перевести сопутствующий проект so5extra с MxxRu на CMake. Это, увы, большой кусок неприятной работы;
  3. попробовать продвинутся в SObjectizer-е по вот этим двум темам: A custom event_queue inside the agent и The first thoughts about "poison pill" messages. Есть ощущение, что они взаимоувязаны. И если получится что-то придумать с кастомными очередями заявок агентов, то можно добавить в SObjectizer целый ряд новых возможностей: от приоритетов для событий до возможности "откладывать" на время заявки, которые агенту не интересны в данный момент (что-то вроде selective receive из Erlang-a).

В общем, на ближайшие месяцы есть чем заняться. Главное, чтобы возможности для этого находились.

вторник, 11 ноября 2025 г.

[prog.c++] А зачем в современных условиях нужен обновляемый раз в три года стандарт C++?

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

For trivial relocatability, we found a showstopper bug that the group decided could not be fixed in time for C++26, so the strong consensus was to remove this feature from C++26.

Т.е. в одной из фич C++26 обнаружили проблему, исправить эту проблему до принятия C++26 не успевают, поэтому фича из стандарта была выброшена вообще. И, в лучшем случае, она станет частью C++ только в 2029-ом, в рамках C++29.

Что приводит к вопросу о том, а хорошо ли, что C++ обновляется всего раз в три года?

Почему бы не принимать обновленные редакции два раза в год?

Скажем, C++26.1 в марте 2026, а C++26.2 в сентябре 2026. Потом C++27.1 в марте 2027, а C++27.2 в сентябре 2027. И т.д.

Когда конкретное предложение (например, по trivial relocatability) будет готово, тогда оно и попадет в ближайшую редакцию. Не успеет к C++26.1 -- попадет в C++26.2, не успеет к C++26.2 -- значит попадет в C++27.1.

Я это к тому, что этикетка, скажем, C++23, в современных обстоятельствах ничего не значит. Есть на бумаге стандарт C++23, а на практике от этого может не быть никакого толку. Даже если вы сидите на самых свежих компиляторах, все равно в них может не быть полной поддержки C++23. Поэтому отталкиваться приходится не от стандарта, а от версии компилятора. А раз так, то какая разница, не поддерживает компилятор обновляющийся раз в три года стандарт, или же он не поддерживает выходящую два раза в год спецификацию? Как по мне, то никакой.

Так уж получается, что в конце 2025-го года совершенно все равно, является ли std::start_lifetime_as частью C++23 или же частью C++20, или же частью условного C++27.2. Нет start_lifetime_as в нужных мне компиляторах и непонятно когда появится, так что без разницы, приняли ли start_lifetime_as в C++20 или C++23.

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

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

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

Почему роль этого самого "заявления о намерениях" не может играть некая спецификация, публикуемая от имени некоторой некоммерческой организации? Зачем сейчас развивать C++ под эгидой ISO?

Можно понять значимость стандартизации в начале 1990-х, когда было с полдюжины живых C++ных компиляторов от совершенно разных поставщиков. Некоторые из которых (Borland и Microsoft, как минимум) еще и норовили свои расширения в язык добавлять. В таких условиях роль толстого фолианта с печатью ISO стандарта была понятна.

Но сейчас-то? На фоне того, как постепенно удушающие С++ в смертельных объятиях Java, C#, Go и Rust, вообще не парятся какой-либо стандартизацией. Просто развиваются и просто делают C++ все менее и менее нужным.

В общем, иногда мне кажется, что главным врагом C++ становится сам комитет по стандартизации и его приверженность странным (мягко говорям) правилам работы.

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

[notebooks] Повсеместный переход на векторные шрифты делают недорогие ноутбуки непригодными для работы

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

За прошедшие годы на собственном опыте наблюдал рост разрешения ноутбучных дисплеев от 640x480 к 800x600, затем к 1024x768, затем 1400x1050... и так до 3840x2160. И вот что характерно: каждый раз переходя от меньшего разрешения к большему происходил рост качества отображения информации на экране. Что графической, что текстовой. Особенно текстовой.

Однако, лет шесть назад попал ко мне в руки замечательный 12.5" ноутбук Dell Latitude в комплектации с TN-матрицей с HD-разрешением (1366x768). И вот тут-то внезапно © выяснилось, что при не самом плохом разрешении для 12.5" экрана качество текста на этом самом экране отвратительное. К концу рабочего дня у меня начинали сильно болеть глаза, а времени за компьютером тогда приходилось проводить очень много. Поэтому через полгода был вынужден перейти на 14" ноутбук с FullHD разрешением (1920x1080).

А год назад попытался пересесть на другой 14" ноутбук тоже с FullHD разрешением, но с соотношением сторон 16:10 (итоговое разрешение 1920x1200). Не смог на нем проработать и месяца 😡 Точно так же болели глаза после 5-6 часов работы за ноутбуком.

Причину для себя нашел в том, что современные ОС (что Windows, что Linux) используют только векторные шрифты. А для того, чтобы эти самые векторные шрифты более-менее нормально выглядели на экранах с невысоким DPI/PPI, применяется сглаживание (в Windows оно известно как ClearType, в KDE оно обозначается как Anti-aliasing).

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

В качестве эксперимента попробовал подружить Kubuntu на уже упомянутом Dell Latitude с растровыми шрифтами. И заодно отключил Anti-aliasing. Получилось как-то так.

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

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

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

Вот как это выглядит при 4-х кратном увеличении.

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

Собственно к чему я это все?

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

Среди того, что у нас в РБ продается за вменяемые деньги (т.е. где-то до ~1500USD) внимание привлек ASUS Vivobook 18 с 18.4" экраном, но разрешением всего лишь FullHD -- 1920x1200. К сожалению, живьем увидеть его не представляется возможным. Но FullHD на 18.4" -- это всего лишь 123 DPI. Что совсем печально. Это даже меньше, чем у HD на 12.5" 🙁

От этого немного грустно.

В продаже имеется некоторое количество ноутбуков от нормальных производителей (Asus, Lenovo, MSI) с приличным железом (современный CPU, 24Gb или 32Gb RAM, SSD от 512Gb), но с FullHD экранами (что 15.6", что 16", что 17", что 18.4"). И этим добром можно было бы спокойно пользоваться, если бы современные ОС и современные приложения (такие как Chrome или Telegram) признавали бы растровые шрифты. Но это, увы, не так.

Поэтому приходится смотреть в сторону ноутбуков с хотя бы 2.5K экранами. Хотя у меня есть подозрение, переходящее в уверенность, что 2.5K на 16" -- это сильно получше, чем FullHD на 14", но все равно эффект "мыла" на тексте для меня заметен. Что заставляет смотреть в сторону 3K экранов. Лучше всего было бы, конечно, 4K. Но ноутбуки с 4K экранами нынче стоят неприлично дорого.

PS. Сейчас у меня два основных рабочих ноутбука: под Linux-ом используется 14" с 2.8K экраном, под Windows -- 14.5" c 3K экраном. На обоих качество текста просто мое почтение. На 15.6" с 4K, конечно же, еще лучше. Но тут уж соревнование отличного с прекрасным 😀 Разницу нужно с микроскопом выискивать.

PPS. Мне кажется, что если ноутбук используется для листания картинок в ленте какой-нибудь сети, просмотре видео из Интернета или игр, то и FullHD на 16" будет вполне достаточно. Но вот у меня все время уходит на работу с текстом -- то постоянно читаешь, то постоянно пишешь, то чередуешь одно с другим. И вот в таких условиях лучшее качество текста на экране из-за больших показателей DPI/PPI становится критически важным.

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

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

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

Фильмы

Трон: Арес (Tron: Ares, 2025). Отличное детское кино. Очень и очень красиво снятое. Достойное продолжение "Трон: Наследие". Как по мне, так хороший фильм для семейного просмотра. Единственное, чего лично мне не хватило, так это настолько же эпичного саундтрека, как в "Трон: Наследие". Но тот саундтрек -- это вообще уникальное явление, которое происходит раз в несколько десятков лет.

Пойман с поличным (Caught Stealing, 2025). Весьма неплохо, понравилось. Хотя элементы комедии при таком количестве трупов выглядят странно.

Код 3 (Code 3, 2025). Очень специфическое кино. Полагаю, сильно завязанную на особенности американского здравоохранения. Можно посмотреть хотя бы потому, что это очень специфическое кино.

Игрок (Dead Money, 2024). Хороший сюжет. И с юмором все в порядке. Должен был бы получиться хороший криминальный фильм. Но чего-то не хватило. Может быть серьезности -- воспринимается это все-таки как комедия.

Девушка из каюты №10 (The Woman in Cabin 10, 2025). Отличная картинка и к игре актеров нет претензий. Но по сюжету откровенная шляпа. В итоге крайне посредственное кино. Но хотя бы не полный отстой, скоротать вечер вполне можно.

Осада школы в Букит-Дури (Pengepungan di Bukit Duri, 2025). Общее впечатление чуть ниже среднего. Местами слишком затянуто, местами слишком много истерик, местами компьютерная графика уровнем в районе плинтуса. Ну и общая драма как будто из индийского кино. Но при этом есть какой-никакой экшон, переходящий, местами, в откровенную жесть.

Грязная игра (Play Dirty, 2025). Как по мне, так откровенная халтура. Хотя юмор в фильме немного спасает ситуацию. Но именно что немного.

Крутой поворот (Sharp Corner, 2024). Скучно, нудно, затянуто и ни о чем.

Сериалы

Бар "Один звонок" (первый сезон, 2023). Мне понравилось.

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

Рай (Paradise, первый сезон, 2025). Его бы сократить по хронометражу раза в два. Ну и это тот случай, когда на вопрос "а где логика в происходящем?" временами невозможно дать ответ.

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

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

[prog.c++.bug] Забавный баг подсадил давеча в код

Любопытный случай, достойный того, чтобы быть сохраненным на память.

Был код по типу вот такого:

do {
  ... // Что-то делаем.
} while(!cnt.empty());

Т.е. выполнение каких-то действий до тех пор, пока контейнер не пуст.

После внесения в код новой функциональности данный фрагмент принял вид:

while(a < params.max_value && !cnt.empty()) {
  ... // Что-то делаем.
} while(!cnt.empty());

Т.е. do я убрал и поставил while, но тот while, который остался от do, не удалил 🙁

И, что самое забавное, этот код у меня работал без проблем 🧐
Я даже не знал, что проблема существует, пока коллеги не подсказали.

Очень редко компилируюсь в режиме Debug. В основном в Release, иногда в RelWithDbgInfo. Но не в Debug.

А как раз в Debug ошибка и проявилась. Оставшийся while начал работать как бесконечный цикл.

Полагаю, при компиляции со включенной оптимизацией компилятор трансформировал код так, что при выходе из первого while во второй мы уже не попадали в принципе. А в Debug-режиме оптимизатор ничего не удалял и исполнение после выхода из первого цикла попадало во второй. И баг проявлялся.

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

воскресенье, 19 октября 2025 г.

[prog.c++] Прекрасное от автора функции на 700 строк...

Некоторое время назад в блоге был пост Наткнулся на образчик кода из категории "Да не дай боже такое сопровождать!". Я там прошелся по качеству кода от одного из RSDN-овских форумчан. Код, как по мне, из категории "лучше бы такого никогда не видеть, тем более не сопровождать".

А на днях этот же форумчанин открыл на RSDN-е новую тему, от которой я испытал что-то вроде шока. Почему шока постараюсь объяснить ниже (хотя не уверен, что это получится).

Итак, суть в том, что у человека образовалась цепочка из if-else в количестве 200 (двухсот(!!!)) штук. Из-за чего VC++ отказался компилировать данную развесистую конструкцию с ошибкой "MSVC C1061: compiler limit: blocks nested too deeply". И автор топика спрашивает у читателей что можно сделать в такой ситуации.

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

else if ( opt.setParam("VAR:VAL")
      || opt.isOption("set-var") || opt.isOption("set-condition-var") || opt.isOption('C')
      || opt.setDescription("Set variable valie for conditions and substitutions"))
{
   if (argsParser.hasHelpOption) return 0;

   if (!opt.hasArg())
   {
       LOG_ERR<<"Setting condition variable requires argument (--set-condition-var)\n";
       return -1;
   }

   auto optArg = opt.optArg;
   if (!appConfig.addConditionVar(optArg))
   {
       LOG_ERR<<"Setting condition variable failed, invalid argument: '" << optArg << "' (--set-condition-var)\n";
       return -1;
   }

   return 0;
}

При этом автор заявляет буквально следующее:

Обработка ключей командной строки.

На какой-нибудь map с хэндлерами не переделать, потому что у меня в режиме --help пробегается по этим условиям и собирает выдаваемую потом информацию. Тут в if сразу и длинный ключ set-var задаётся, и короткий C, и подсказка по формату значения VAR:VAL и описание опции Set variable valie for conditions and substitutions — в одном месте всё задаётся

Это, собственно, и подвигло меня написать данный пост. Т.е. сперва я был шокирован самим фактом появления данной темы на RSDN: это же сделал профессиональный программист, которому деньги платят за написание кода. Написание нормального кода. Т.е. задачей программиста изначально является то, чтобы такого говна в коде не было. Вот просто от слова совсем. И если ты этого обеспечить не можешь, то может нужно профессию сменить? В менеджмент уйти, к пример. Ну или подучиться немного этой самой профессии...

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

Я даже не буду затрагивать тему того, что в продвинутых инструментах для работы с аргументами командной строки многие проблемы уже решены "by design". Например, можно посмотреть на Lyra или args, или даже boost::program_options. Мало ли какие условия заставили человека использовать что-то специфическое (хотя, подозреваю, что им же и написанное). Поэтому сконцентрируемся только на длинной цепочке if-else.

В общем, поскольку в нашем распоряжении только небольшой кусочек демонстрационного кода, то будем отталкиваться именно от него. Из-за чего предлагаемые ниже решения, скорее всего, не подойдут на 100% автору RSDN-новского топика, т.к. наверняка есть какие-то неозвученные на RSDN-е моменты. Но и нет цели показать 100% решение, смысл в том, чтобы показать тот вариант, который лично мне сразу же пришел в голову. И от которого можно оттолкнуться, чтобы избежать длинной портянки из if-else.

Суть в том, что с каждым if-ом у нас есть два связанных блока кода. Первый отвечает за описание одного аргумента командной строки (он помещается внутри условия в if-е). Второй отвечает за обработку аргумента (он помещается внутри then-ветки для if-а).

А раз так, то пусть каждый из блоков представляется в виде std::function. И оба эти блока пусть объединяются в рамках одной структуры:

среда, 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 отказался компилировать код вроде вот такого (это не реальный код, а минимизированая версия на которой проблема воспроизводится):

templatetypename 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-е вы считаете удобными и, может быть, даже практикуете?

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

[prog.c++] dynamic_cast и непубличные базовые классы

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

Вот пример:

#include <iostream>

class iface {
public:
    virtual ~iface() = default;

    virtual void f() = 0;
};

class basic_impl {
public:
    virtual ~basic_impl() = default;

    void impl_f() {}
};

class first_derived final : public iface, protected basic_impl
{
public:
    void f() override { impl_f(); }
};

class second_derived final : public iface, public basic_impl
{
public:
    void f() override { impl_f(); }
};

void try_dynamic_cast(iface * base)
{
    std::cout << "trying to cast " << typeid(*base).name() << std::flush;
    if(auto * p = dynamic_cast<basic_impl *>(base); p)
        std::cout << "... OK" << std::endl;
    else
        std::cout << "... FAILURE" << std::endl;
}

int main() {
    first_derived f;
    second_derived s;

    try_dynamic_cast(&f);
    try_dynamic_cast(&s);
}

Обращаю внимание, что для first_derived класс basic_impl -- это protected-база.

В результате его запуска получим:

trying to cast 13first_derived... FAILURE
trying to cast 14second_derived... OK

Цынк.

PS. Вот так: век живи, век учись. А помрешь все равно дураком 🙂

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

[prog.c++] Оператор <=> и оператор ==

Надеюсь, многие знают, что в C++20 появилась замечательная штука operator<=>. Стоит его определить, как компилятор на его основе выводит другие операторы сравнения, вроде operator< или operator>=.

Причем, что особенно хорошо, можно попросить компилятор сгенерировать operator<=> для нас автоматически:

struct example {
  ... // Какие-то поля.

  auto operator<=>(const example &) const = default;
};

И все. Остальное за нас сделает компилятор.

Однако, думаю, не все знают, что если нам не подходит штатная реализация и мы пишем operator<=> сами, то компилятор потребует, чтобы мы еще и определили свой собственный operator==.

Например, допустим, что у нас в example три поля, а в сравнении должно участвовать только два из них:

struct example {
  int m_a;
  int m_b;
  int m_c;

  auto operator<=>(const example & o) const noexcept
  {
    return std::tie(m_a, m_c) <=> std::tie(o.m_a, o.m_c);
  }
};

В этом случае мы внезапно© обнаружим, что у нас нет operator==.

Таков путь: если мы определили свой operator<=>, то компилятор не будет автоматически выводить равенство и неравенство, а потребует, чтобы мы предоставили operator==.

Может быть вам будет интересно почему?

Если да, то лучше всего прочитать раздел с мотивацией из предложения P1185 и вот этот ответ на Stackoverflow. В двух же словах: реализация сравнения на строгое равенство на базе operator<=> может быть весьма неэффективна.

Например, представим, что мы сравниваем две строки: s1="AA" и s2="AAA".

При лексикографическом сравнении s1 меньше, чем s2: общая подстрока в s1 и s2 совпадает, но в s1 символов меньше.

Когда мы для наших строк s1 и s2 вызываем operator<=>, то этот оператор пытается выяснить как именно соотносятся s1 и s2: меньше ли s1, чем s2, или же s1 больше, чем s2, или же они равны.

Теперь допустим, что в s1 находится миллион символов "A", а в s2 -- миллион и один символ "A". При вызове operator<=> придется сравнить миллион символов в общей подстроке прежде чем мы сможем понять, что s1 все-таки меньше, т.к. у нее символов меньше.

При этом мы не можем прервать сравнение раньше. Допустим, что в s1 будет девятьсот девяносто девять тысяч и девятьсот девяносто девять символов "A", а за ними единственный символ "B", тогда как в s2 все миллион + один символ -- это "A". В таком случае строка s1 окажется больше, не смотря на то, что символов в ней меньше.

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

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

среда, 6 августа 2025 г.

[prog.c++.thoughts] Design By Contract и приватные методы классов в C++

В C++26 будут включены контракты. Т.е. можно будет сказать, что элементы Design By Contract наконец-то доберутся и до C++.

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

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

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


Технология Design By Contract (DbC) берет свое начало в языке Eiffel (вот еще одно описание от первоисточника). Если вы не знакомы с Eiffel хотя бы в части понимания DbC, то вы, к сожалению, многое упустили в своей профессиональной подготовке. Если же знакомы, то, надеюсь, понимать написанное ниже будет проще.

Ключевыми для нашего разговора будут три вещи (на самом деле в Eiffel есть еще и инварианты для циклов, и утверждения check, похожие на C-шный assert, но их мы спокойно можем игнорировать):

вторник, 5 августа 2025 г.

[prog.c++] Forward declarations для типов хорошо бы держать под контролем и в одном месте

С давних пор с настороженностью отношусь к опережающим объявления (forward declarations) типов в коде. Когда вижу что-то вроде:

class A;
class B;
class C {
  const A * _a;
  B & _b;
  ...
};

то невольно начинаю ждать неприятных приключений. Коих, по моим наблюдениям, бывает два вида.

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

class A;

и

struct A;

Поэтому если в каком-то заголовочном файле есть что-то вроде:

class A;
void f(const A &);

А затем в файле реализации:

struct A { ... };
void f(const A & a) { ... };

то при линковке нужная версия f может и не обнаружиться.

Во-вторых, может оказаться, что где-то осталось:

class A;

Тогда как основное определение A было преобразовано в:

template<typename T>
class SomeTrickyTemplate { ... };

using A = SomeTrickyTemplate<int>;

И мы опять же получим приключения при линковке.


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

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

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

пятница, 1 августа 2025 г.

[prog.c++.bugs] Пример типичной C++ной ошибки

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

Вся суть вот в этом фрагменте:

class lock_getter
{
   const std::chrono::steady_clock::duration & m_wait_time_limit;
   ...
   std::condition_variable m_wakeup_cv;
   ...
   bool m_access_granted;

public:
   lock_getter(
      ...,
      std::chono::steady_clock::duration wait_time_limit,
      ...)
      : ...
      , m_wait_time_limit{ wait_time_limit }
      , ...
   {}
   ...
private:
   void try_acquire_or_wait()
   {
      ...
      m_access_granted = false;
      m_wakeup_cv.wait_for(m_lock, m_wait_time_limit,
            [this]() { return m_access_granted; });
      if(!m_access_granted)
         throw std::runtime_error{ "lock can't be acquired" };
      ...
   }
};

Нить A пыталась захватить некий ресурс, который ей по запросу должна была отдать нить B. В ожиданнии подтверждения нить A засыпала, как раз в методе try_acquire_or_wait. Нить B точно разрешала нити A захват ресурса и вызывала для m_wakeup_cv метод notify_one (т.е. точно будила нить A). Но проснувшись нить A почему-то считала, что ресурс ей не дали и порождала исключение. Хотя ресурс ей дали. Но нить A все равно считала, что нет, и бросала исключения.

В общем-то, вся разгадка уже на экране. Нужно только внимательно посмотреть :)

Кому лень смотреть, милости прошу под кат.

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

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

Фильмы

Агент под прикрытием (La infiltrada, 2024). Показалось, что этот фильм должен быть более интересен испанской аудитории, чем международной, т.к. там рассказывается о проблеме сепаратизма басков. Мне же не хватило экшена и напряжения в картине.

Кирпич (Brick, 2025). Смотреть интересно, хотя было несколько моментов, в которых хочется спросить "ну что это за фигня, а?"

М3ГАН (M3GAN, 2022). Местами возникают вопросы из категории "что это, блин, такое было?", но в целом очень бодренько. Так что пойдет чтобы отключить мозги и скоротать вечер.

Апокалипсис Z: Начало конца (Apocalipsis Z: El principio del fin, 2024). Ничего хорошего от фильма не ждал, поэтому был приятно удивлен. Бюджетно, но эта бюджетность играет фильму в плюс, т.к. показывает как будет выглядеть зомби апокалипсис не в Лондоне, Нью-Йорке, Токио или в каком-нибудь другом мегаполисе, а в условном Зажопинске.

Бессмертная гвардия 2 (The Old Guard 2). Если первый фильм понравился, то можно смотреть и второй -- увидите все тоже самое. А если первая часть вызывала рвотный рефлекс, то лучше пройти мимо.

М3ГАН 2.0 (M3GAN 2.0, 2025). Если первый фильм еще пытался быть похожим если не на фильм ужасов, то на триллер, то вторая часть -- это, в лучшем случае, откровенный стеб и тупая подростковая комедия. Вероятно, для компании молодых людей под пиво, зайдет. Но мне не понравилось.

Балерина (Ballerina, 2025). Тупое мочилово ради тупого мочилова. Местами настолько невероятное, что фильм хочется переквалифицировать из "боевика" в "фэнтези".

28 лет спустя (28 Years Later, 2025). Редкостное говно. Неудобоваримая смесь психоделики и маразма. Жаль потраченного на просмотр времени.

Сериалы

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

Роковая афера (Factice, первый сезон, 2025). Странное впечатление. Вроде как замах на незамысловатую криминальную комедию. Но для легкой комедии там слишком много насилия и убийств. Тогда как на полноценную криминальную драму или добротный триллер совершенно не тянет. Тем не менее, на фоне второго сезона "Фишера" выглядит добротно сделанным кино.

Фишер (второй сезон, 2025). Первые три серии были еще ничего, последние три серии -- откровенный шлак, а развязка -- унылое говно. В итоге жалко потраченного времени. Добавлю еще, что на мой взгляд, Ирина Старшенбаум в роли и.о. начальника милиции ну совершенно неубедительна, абсолютный мискаст.

Вне категории

Главы государств (Heads of State, 2025). Это невозможно воспринимать всерьез: авиаперелет из Лондона в итальянский Триест над территорией Белоруссии и эти наши знаменитые горные долины... После такого происходящее на экране превращается в сплошной цирк. Хорошо хоть не уродов, но тут как посмотреть.