пятница, 12 июня 2026 г.

[prog.c++] Похоже, что SObjectizer-5 будет использоваться в еще одном проекте

Больше двух лет сотрудничаю с интересным проектом. Занимался в рамках этого проекта разными задачами, начинал вообще с замены C++ REST SDK на RESTinio, а потом пошло поехало по нарастанию сложности 😎. Не все из этого было связано с многопоточностью, но многое.

Многопоточность здесь самая обычная -- std::thread, std::mutex, std::condition_variable, немного std::atomic-ов. Ну и специфика больше про параллельную обработку данных, нежели про событийно-ориентированное программирование.

Местами об отсутствии SO-5 в проекте приходилось жалеть, но, по большому счету, всерьез только один раз. Мне кажется, что та задачка на агентах решалась бы проще, чем на std::thread. Плюс еще пара-тройка мест, где простой агент с периодическим сообщением, на мой взгляд, оказался бы практичнее, чем выделенная нить с ручным циклом вокруг std::this_thread::sleep_for. Т.е. в недавнем прошлом от SO-5 полезный выхлоп вряд ли был заметным.

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

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

Такое распараллеливание средствами Taskflow было сделано, но некоторый осадочек остался:

  • в Taskflow нет встроенной поддержки таймеров. Одна из идей о том как оптимальнее поделить общий объем работы на отдельные кусочки базировалась на том, чтобы контролировать процесс через некоторые интервалы времени. Типа начнем считать на текущем треде, но поставим отложенную на 250ms задачу. Если к моменту ее запуска вычисление не завершится, то задача возьмет на себя часть оставшейся работы. Если же вычисление успеет закончится, то надобность в отложенной задаче отпадет. Только вот провалидировать эту идею из-за отсутствия таймеров в Taskflow сходу не получилось;
  • не был понятно как в Taskflow реализовать квотирование имеющихся ресурсов для того, чтобы одно вычисление не захватило все ресурсы себе, а оставшиеся вычисления сидели бы на "голодном пайке". При этом на горизонте маячит фича по приоритетам вычислений, т.е. каким-то высокоприоритеным вычислениям такая узурпация ресурсов разрешается, а вот низкоприоритетным -- нет.

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

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

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

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

Не знаю, приживется ли SO-5 в проекте окончательно. Развитие идет динамично и у команды очень легкое отношение к включению или изъятию тех или иных зависимостей. Это я с большой осторожностью отношусь к тому что включать, а что нет, и могу долго раздумывать над тем, можно ли обойтись без какой-нибудь внешней библиотеки. Но здесь ребята более шустрые и рисковые -- если подвернулось что-то потенциально полезное, то сходу добавили. Если не оправдало себя, то не менее быстро выбросили 🙂

Так что может быть и SO-5 через месяц-другой-третий постигнет участь Taskflow. Поживем -- увидим. Сам я к этому отношусь спокойно: будет в проекте SO-5 -- хорошо, не будет -- не страшно. Даже если в итоге от SO-5 откажутся, то это даст еще больше полезной информации о том, где SO-5 применим, а где не очень.

Пока же я рад происходящему и есть два воодушевляющих фактора:

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

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

Так что будем пробовать и смотреть что из этого получится.

понедельник, 8 июня 2026 г.

[life.audiophilia.diy] Попробовал давеча 10mm динамики в наушниках-затычках. На свою голову...

Поскольку в области 15.4mm и 14.8mm динамиков для наушников-вкладышей перепробовал уже практически все, что заслуживало внимания, то решился на эксперимент с 10mm динамиками для внутриканальных наушников. Но т.к. заушные мониторы мне не подходят, то остановился на форм-факторе "затычек".

Взял вот эти недорогие (по меркам 10mm динамиков) драйверы:

И установил их вот в такие корпуса (из черного дерева):

Немного помучался с подбором амбушюр...

Но результат получил такой, что даже и не знаю как охарактеризовать.

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

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

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

Однако, есть подозрение, что все это далеко не бесплатно для ушей. Чтобы все эти прелести воспринимать мне нужны амбушюры размера L. И хоть я и выбрал самые мягкие из имеющихся, но все равно ощущение, что вставляешь толстые палки в слуховые каналы. Это ощущение практически исчезает минут через 7-10 после начала прослушивания, хотя того комфорта, который есть со вкладышами, когда "вставил и забыл", нет и близко. Поэтому когда наушники извлекаешь, то чувствуешь пусть и небольшое, но физическое облегчение. А такой дискомфорт, хоть он и не сильный, вряд ли полезен для слуха.

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

понедельник, 1 июня 2026 г.

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

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

Эксперименты проводились под Windows с VS2022 и VS2026 (обновления от мая 2026-го) и ArchLinux с GCC 16.1 и clang 22.1.

Ожидаемые мной результаты (т.е. отсутствие проблем компиляции/линковки) получились только с clang 22.1 и libc++. А вот с GCC и VC++ случились какие-то проблемы, которые мне сложно объяснить.

Во всех случаях сборка осуществлялась через CMake и Ninja.

Исходные коды описанных ниже тестовых программ можно найти в этом репозитории.


Эксперимент первый (case_001 из упомянутого репозитория). Очень простой модуль. Декларация в файле hello.ixx:

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

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

Фильмы

Полный Такос (Operation Taco Gary's, 2026). Совершенно укуренный и классный фильм. Мне зашел, но из-за его специфичности рекомендовать не могу -- наверняка понравится не всем. Однако, хороший пример того как за дешево можно снять приличную юмористическую фантастику.

Эдем (Eden, 2024). Хорошая история, хорошая операторская работа, отличная игра актеров. Но вот рассказана эта история так, что не особо и цепляет. Поэтому самые сильные впечатления производят реальные фотографии и кадры хроники, показанные в самом-самом конце.

Они убьют тебя (They Will Kill You, 2026). Неплохая черная комедия в стиле фэнтези с реками крови и грудами отрубленных конечностей.

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

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

Сезон охоты ("Blood of Man" или "Hunting Season", или "Mermaid", 2025). Добротно, но слишком уж неторопливо и маловато экшОна. По сути, весь фильм держится на харизме Мэла Гибсона.

Мумия ("The Mummy" или "Lee Cronin's The Mummy", 2026). Для своего жанра, в принципе, нормально. Но манера съемки у фильма такая, что воспринимается это все как театральная постановка и поэтому погружения в атмосферу ужаса не происходит.

Охота ("Hunt" или "Heon-teu" или "헌트", 2022). Есть русский бунт, бессмысленный и беспощадный. А есть корейские боевики, не менее бессмысленные и беспощадные 🙂 А если для вас все корейцы на одно лицо и их имена вы не запоминаете вообще, то следить за логикой происходящего на экране будет совсем сложно.

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

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

Сериалы

Пацаны (The Boys, пятый сезон, 2026). Самый слабый из все сезонов. Но если предыдущие понравились, то надо смотреть хотя бы для того, чтобы увидеть чем все закончилось.

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

Гнев (Man on Fire, первый сезон, 2026). Первые серии бодренькие, затем какие-то тягомотные сопли, а в финале какой-то сплошной маразм. Так что можно смело пройти мимо.

вторник, 26 мая 2026 г.

[prog.c++.bugs] Похоже наткнулся на баг в GCC 12/13 под Linux-ом. Или нет.

Дело было так: есть некий объемный и сложный шаблон класса-контейнера. Для тестирования было создано приложение с юнит-тестами на базе Google.Test. В состав этого приложения входит порядка 30 (тридцати) .cpp-файлов. В некоторых из них происходит следующее:

namespace
{

template<typename T>
struct test_traits : public my_container::default_traits<T> {
  static constexpr std::size_t key_size = 3;
};

/* namespace anonymous */

TEST(my_container, some_test)
{
  my_container::my_map<int, test_traits> map;
  ... // какие-то действия с map.
}

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

Все это работало до тех пор, пока не был добавлен еще один .cpp-файл, в котором было практически тоже самое:

namespace
{

template<typename T>
struct test_traits : public my_container::default_traits<T> {
  static constexpr std::size_t key_size = 3;
  static constexpr my_container::mode use_mode =
      my_container::mode::versioned;
};

/* namespace anonymous */

TEST(my_container, some_test_versioned)
{
  my_container::my_map<int, test_traits> map;
  ... // какие-то действия с map.
}

И вот тут-то в some_test_versioned с map стали происходит странные вещи: возникали segmentation faults там, где их быть не должно было. Попытки отладить код приводили к тому, что отладчик показывал, что отрабатывают не те ветки if-ов. А отладочные печати содержали совсем не те значения, которые должны были бы быть.

Было полное ощущение, что GCC сошел с ума.

Проект, в рамках которого все это делается, собирается VC++ под Windows и GCC под Linux-ом. Под Linux-ами используются GCC 12 и 13. Конкретно я работаю с GCC 13, но проверил и под GCC 12. Сам проект уже не очень маленький, плюс подтягивает кучу зависимостей разного калибра (включая Folly и Abseil). Все это к тому, что мероприятие по перекомпиляции проекта под какой-то свежий GCC или clang -- это попытка с негарантированным результатом. Может повезти, а может и нет.

Под Windows проверил, там ничего подобного нет, все работает как и положено. А вот под Linux-овым GCC -- проблемы.

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

После этого все описанные выше магические проблемы разом исчезли.

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

Но на 100% не уверен. Может быть здесь дело еще и в том, что у my_container::map есть шаблонный параметр шаблона, т.е.:

namespace my_container
{

template<typename T, template<typenameclass Traits>
class map { ... };

/* namespace my_container */

Поэтому его параметризация в тесте идет не конкретными типами, а шаблоном:

TEST(my_container, some_test_versioned)
{
  my_container::my_map<
    int// Это конкретный тип.
    test_traits // А это шаблон, который развернется в конкретный
                // тип уже внутри map.
  > map;
  ... // какие-то действия с map.
}

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

четверг, 21 мая 2026 г.

[prog.c++] Обнаружился баг в timertt возрастом более 10 лет

Пользователи обнаружили в SObjectizer проблему, которая была вызвана неправильной работой механизма timer_heap в библиотеке timertt.

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

То, что я допустил достаточно дурацкую ошибку совсем не удивительно. Я вообще умудряюсь делать на удивление много ошибок при реализации простых структур данных (скажем, если приходится вручную программировать интрузивный двусвязный список, то я там обязательно в паре мест накосячу). Дополнительным отягчающим фактором стало то, что специфическое для timer_heap тестирование было проведено "по верхам". Думаю, что если бы в 2014-ом не поленился составить тест на базе примитивного fuzzing-а, то эта проблема вскрылась бы уже тогда. Но невнимательность + разгильдяйство сделали свое темное дело.

Более удивительно то, что этот баг проявился в полный рост только сейчас, в 2026-ом. Вот это внушаить 🤔

Какие выводы можно сделать?

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

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

PPS. Видимо, нужно найти время и вытащить timertt из старого svn-репозитория на SourceForge чтобы он продолжил жить на GitHub-е. Плюс выбросить оттуда MxxRu и перевести все на CMake (собственно, необходимость бодаться с CMake и является основным стоп-фактором). Нужно как-то себя заставить сделать это. Жаль только, что история коммитов при переносе в git потеряется 🙁

PPPS. Обновление для SObjectizer-а уже опубликовано в виде версии 5.8.5.1.

воскресенье, 10 мая 2026 г.

[life.audiophilia.diy] Кратко о динамиках, с которыми довелось познакомиться и некоторые общие впечатления

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

Сперва, очень кратко, перечень с личными впечатлениями. Затем поделюсь своим субъективным взглядом на то, как меняется звук динамиков из магазинчиков на Aliexpress с увеличением их цены.

Итак, вот что вспоминается из недавних приобретений:

15.4mm 32ohm Ceramic Titanium PU Composite Diaphragm (золотистый композит на прозрачной диафрагме).

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

15.4mm 32ohm Ceramic Alloy Diaphragm (оранжевый композит на черной диафрагме).

Самые шикарные ВЧ и СЧ из того, что побывало у меня в руках. При этом полное отсутствие НЧ.

15.4mm 32ohm Ceramic Titanium Alloy Composite Diaphragm (голубой композит на прозрачной диафрагме).

Очень крутые ВЧ и СЧ. ВЧ особенно впечатляют по детализации и "деликатности", по сравнению с ними ВЧ от любимых мной LCP-динамиков воспринимаются уже как грубые, колкие и примитивные. С НЧ ситуация сложная. Во многих случаях мне баса хватает, но не всегда. Любителям накачки на низах точно не хватит. Объективно, громкость этих динамиков заметно падает на частотах ниже 80Hz, поэтому местами эти динамики ощущаются более "светлыми", чем конкуренты. Любопытный эффект: пару-тройку дней слушаешь с удовольствием и наслаждаешься их техничностью и мельчайшими деталями, а потом ловишь себя на том, что звук оказывается очень скучный. Да, техничный. Да, ровный. Да, с проработкой мельчайших деталей. Но очень скучный. Возможно как раз из-за некоторой нехватки НЧ. Тем не менее, это, пожалуй, самое интересное из последних приобретений. После знакомства с данными динамиками слушать любимые мной LCP-динамики стало сложнее, т.к. ВЧ в LCP гораздо "жестче" и с меньшим количеством "оттенков" и "нюансов". Хотя казалось бы... Ведь ранее по детализации у LCP практически не было конкурентов.

15.4mm 32ohm Cobalt Diaphragm. Отличные динамики с хорошей детализацией, очень приятной тоналкой и глубокими НЧ. Являются прямыми конкурентами самых первых динамиков из этого обзора, но чуть более резкие, контрастные и как раз более "прозрачные", чем динамики с керамическо-титаново-полиуретановым композитом. Мне показалось, что по детализации эти чуть чуть, но уступают всем трем перечисленным выше. Поэтому если хочется выслушивать самые-самые тонкие детальки, то нет. А вот если нужен объемный и тяжелый бас, то эти выигрывают. Поэтому-то я их редко слушаю, в какой-то момент складывается впечатление, что НЧ доминирует, а музыка звучит более гнетуще, чем хотелось бы.

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

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

У меня складывается ощущение, что чем дороже динамики, тем больше в них ВЧ и тем меньше НЧ.

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

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

Но, при этом, динамики с недостатком НЧ, как ни странно, производят серьезный ВАУ-эффект. За счет того, что НЧ мало и они отведены далеко назад, происходят две прикольные штуки:

  • лучше слышны мельчайшие детали на ВЧ и больше подробностей на СЧ (добавим сюда еще и то, что в техническом плане дорогие динамики реально более детальные и имеют меньше искажений). Поэтому при переходе с дешевых драйверов на дорогие просто поражаешься тому, насколько же больше ты слышишь нюансов в одной и той же музыке. Как раз то, что НЧ мало, и позволяет сконцентрироваться именно на ВЧ. Т.е. за счет отсутствия информации о НЧ невольно приходится довольствоваться ВЧ;
  • усиливается эффект 3-мерности. Когда НЧ мощные, то ударные и контрабас звучат как будто бы совсем рядом. А когда НЧ мало, то их звук доноситься как бы издалека. И кажется, что вокалист стоит прямо перед тобой, а барабанщик с контрабасистом где-то далеко за ним.

С непривычки эти два фактора поражают настолько, что первые несколько дней оказываешься совершенно очарованным новым звучанием и с большим интересом переслушиваешь всякое разное. При этом часто ловя этот самый ВАУ-эффект, когда старые и заслушанные до дыр композиции начинают звучать по новому. Особенно интересно для меня лично начинает восприниматься легкий инструментальный джаз (если этот жанр применим к таким группам как GoGo Penguin и Tingvall Trio) и тяжелый метал (скажем, альбом Senjutsu от Iron Maiden).

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

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

пятница, 8 мая 2026 г.

[prog.c++] Хочется странного, но теперь уже от std::map

В std::map начиная с C++17 есть отличный метод try_emplace. Он особенно хорош, когда конструирование mapped_type очень дешевое. Например, когда в качестве ключа у нас int, а в качестве значения -- структура с несколькими int-ами внутри. Тогда получается эффективно: попробовали вставить, если ключа в map еще нет, то из параметров сконструировали mapped_type и добавили в map новое значение. А если же в map ключ уже есть, то передача в try_emplace нескольких int-ов как параметров для конструктора mapped_type -- это копейки, на которые во многих случаях можно просто не обращать внимания.

Но вот когда у нас в качестве mapped_type какой-то "тяжелый" объект, вот тогда ситуация грустнее. Например, mapped_type -- это std::unique_ptr с указателем на класс с кучей собственных контейнеров внутри.

Если мы напишем что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::make_unique<heavy_object>(...));

то это явная пессимизация -- ведь нам придется создавать heavy_object при каждом обращении к try_emplace, даже если ключ в map уже есть.

Я вижу два стандартных пути выхода из этой ситуации.

Во-первых, мы можем пойти классическим способом, через find с последующим emplace, если find завершился неудачно:

auto it = my_map.find(key);
if(it == my_map.end()) {
  it = my_map.emplace(key, std::make_unique<heavy_object>(...)).first;
}
// Теперь it указывает на объект внутри std::map.

Но здесь плохо то, что для вставки объекта поиск по std::map нужно будет делать дважды.

Во-вторых, в try_emplace можно передать пустой unique_ptr, а сам объект создать уже после вставки. Т.е. что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  it->second = std::make_unique<heavy_object>(...);
}

Но здесь плохо то, что нам нужно позаботиться об exception safety, ведь вызов make_unique может бросать исключение. И самое худшее, что можно сделать, это написать что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  try {
    it->second = std::make_unique<heavy_object>(...);
  }
  catch(...) {
    // Удаляем только что вставленный пустой указатель.
    my_map.erase(it);
    throw;
  }
}

Гораздо лучше было бы иметь что-то вроде scope(failure) из D. Что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  auto guard = at_failure(
    // Эта лямбда будет вызвана если выход из скоупа произойдет
    // из-за исключения.
    [&it, &my_map]() {
      my_map.erase(it);
    });
  it->second = std::make_unique<heavy_object>(...);
}

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

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  do_with_rollback_on_exception(
    // Что хотим сделать.
    [&it, ...]() {
      it->second = std::make_unique<heavy_object>(...);
    },
    // Эта лямбда будет вызвана если первая лямбда бросит исключение.
    [&it, &my_map]() {
      my_map.erase(it);
    });
}

Но все эти приседания были бы не нужны, если бы был вариант try_emplace, который бы принимал не аргументы для конструктора mapped_type, а фабрику, которая может породить экземпляр mapped_type для вставки:

auto [it, inserted] = my_map.try_emplace_with_factory(key,
  // Эта лямбда будет вызвана, если объекта в map нет.
  [...]() {
    return std::make_unique<heavy_object>(...);
  });

К сожалению, такого варианта try_emplace_with_factory в std::map нет.

PS. Вышесказанное относится и к std::unordered_map.


Upd. В обсужении на LinkedIn посоветовали обходной маневр вида:

#include <map>
#include <string>

class simple_data {
    std::string _data;
public:
    simple_data(const char * s) : _data{ s }
    {}
};

class data_holder {
    std::string _data;
public:
    template<typename... Args>
    data_holder(Args && ...args) : _data{ std::forward<Args>(args)... }
    {}
};

template<typename F>
struct deferred {
    F _f;

    template<typename T>
    operator T() { return _f(); }
};

int main()
{
    std::map<int, simple_data> m1;
    m1.try_emplace(0, deferred{ []{ return simple_data{ "Hello, world" }; } });

    std::map<int, data_holder> m2;
//    m2.try_emplace(0, deferred{ []{ return std::string{ "Hello, world" }; } });
    m2.try_emplace(0"Hello, world");
}

Но не для всех случаев он будет работать. В частности для data_holder-а не сработает, т.к. у data_holder-а есть шаблонный конструктор (цынк).

понедельник, 4 мая 2026 г.

[prog] Любопытное из книги "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations"

Попалась в руки книга "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations". Начал ее листать, т.к. темой низкоуровневых оптимизаций на C++ никогда не занимался. Мне всегда было интересно писать корректно работающий код, который был бы понятным и сопровождабельным, который бы было просто использовать правильно, но сложно неправильно, но в плане скорости работы кода никогда не упарывался. В общем, как однажды сказали про мой код: "получение гарантий корректности времени компиляции при этом не используя Haskell" 🙂

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

Про саму книгу ничего не скажу, только начал с ней знакомиться, а начало тупо пропустил, т.к. там много говорится о специфике HFT, а к HFT вообще не имею никакого отношения. Перешел сразу к главам, где про конкретные приемы рассказывается. И вот в разделе про Branch Prediction наткнулся на вещи, которые мне прям как бальзам на душу, а именно:

Дело в том, что для меня всегда наиболее естественно было писать в стиле:

if(some_condition) {
  ... // тут много строчек кода с выполнением основной логики.
  ...
  ...
}
else {
  return some_error_code;
}

Т.е. большинство действий сосредотачивается именно в ветке then.

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

if(!some_condition) {
  return some_error_code;
}
else {
  ... // тут много строчек кода с выполнением основной логики.
  ...
  ...
}

Или даже вот такой:

if(!some_condition) {
  return some_error_code;
}

... // тут много строчек кода с выполнением основной логики.
...
...

Но оба эти стиля мне не нравятся на каком-то интуитивном уровне. Особенно последний (про этот стиль я уже высказывался: например, в контексте языка Go). Хотя, если мы в проекте придерживаемся принципов defensive programming, то начало метода/функции из if-ов для проверки входных параметров/состояния объекта, т.е. что-то вроде:

int f(int a, int b, int c) {
  if(a < 0) return invalid_parameter_a;
  if(b < 10 || b > 100) return invalid_parameter_b;
  if(c > 1000) return invalid_parameter_c;

  ... // Далее основная логика.
}

то такие короткие if-ы -- это нормально. Но когда проверки входных данных завершены и идет основной код метода/функции, то if-ы с короткими then или if-ы, в которых только return, на мой взгляд, ухудшают код (хуже только циклы, внутри которых короткие if-ы с continue).

И вот листая книгу "C++ Ultra-Low Latency: Multithreading and Low-Level Optimizations" вдруг натыкаюсь на подтверждение того, что привычный для меня способ написания if-ов имеет под собой обоснование еще и с точки зрения эвристик компилятора по обеспечению branch predictions.

пятница, 1 мая 2026 г.

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

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

Фильмы

Проект "Конец света"/Проект «Аве Мария» (Project Hail Mary, 2026). Хорошая, добрая сказочка. Отличное кино для семейного просмотра.

Удачи, веселитесь, не умирайте (или "Удачи, веселья, не сдохни!", Good Luck, Have Fun, Don't Die, 2025). В сюжет, конечно, лучше не вдумываться, но в целом смешно и бодро, временами сатира на современную диджилитизацию прям огонь.

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

Кровь на границе (Frontier Crucible, 2025). Неплохо, но я и положительно отношусь к жанру вестернов. А вот для тех, кто фильмы про Дикий Запад не любят, может быть скучно. Операторскую работу выделю отдельно, местами очень красивые кадры получились.

Наследник (How to Make a Killing, 2026). Не шедевр, но неплохо, мне зашло.

Я иду искать 2 (Ready or Not: Here I Come, 2025). Мне показалось, что это тоже самое, что и первый фильм. Поэтому если первый понравился, можно глянуть и второй, но не нужно ждать чего-то нового и необычного. А если первый не зашел, то от просмотра второй части лучше отказаться.

Битва на Шельде (The Forgotten Battle или De Slag om de Schelde, 2020). Довольно нудно и затянуто, хотя и трагично. Батальных сцен немного, они вызывают двойственные чувства -- взрывы если и не реальной пиротехникой сделаны, но выглядят внушительно, а вот выстрелы из стрелкового оружия нарисованы и компьютере и это сильно заметно.

Трофей (2025). Редкая бредятина, в происходящее на экране невозможно поверить.

Братья под огнем (Brothers Under Fire или Sierra Madre, 2026). Редкой дешевизны халтура. Не смотреть ни в коем случае.

Сериалы

Черное солнце (2023) и Черное солнце-2 (2025). Понравилось, особенно первый сезон.

Больница Питт (The Pitt, второй сезон, 2026). Снято на уровне первого сезона. Но не лучше. И ничего нового не добавляет. Поэтому ощущения странные: с одной стороны, вполне себе нормальное кино, с другой стороны, вполне можно.

Детектив Холе (Harry Hole или Jo Nesbo’s Detective Hole или Jo Nesbøs Harry Hole, первый сезон, 2026). Очень нудно и очень затянуто. Уложили бы все тоже самое в три серии, можно было бы смотреть, а так нудная нудятина.

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

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

пятница, 17 апреля 2026 г.

[prog.c++] Просмотрел чужой доклад о SObjectizer

Марко Арена сделал доклад о SObjectizer и этот доклад публично доступен на YouTube: [Milan Meetup] Concorrenza multiparadigma con SObjectizer (Marco Arena)

Выступление на итальянском языке, но с помощью Yandex Browser можно послушать в переводе.

Дополнительные материалы (слайды на английском и репозиторий с кодом) доступны здесь: https://lao.bz/mcs

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

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

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

среда, 15 апреля 2026 г.

[prog.sadness] Вскрик души в процессе копания в чужом коде

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

PascalCase -- отстой.

PascalCase в совокупности с длинными строками и экономией на пробелах и пустых строках -- отстой вдвойне.

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

Удачно выбранные имена классов рулят. Неинформативные имена или имена, отличающиеся всего одной буквой (например, resource_handler и resources_handler) доставляют (в худшем смысле этого слова) неимоверно.

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

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

Но главное впечатление, еще более субъективное, личное и неутешительное для меня самого: очень сложно работать с кодом, написанным по принципу "сейчас кое как слепим, а потом переделаем по нормальному". Пытаясь разобраться с результатом главная мысль в голове -- "Господь, жги, тут уж ничем не помочь". А жечь то как раз и нельзя 😡

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

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

пятница, 3 апреля 2026 г.

[prog.c++] Эх, давно я не брал в руки SObjectizer...

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

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

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

Передачу memory_pool-а в дочернюю нить сделал через переменные, защищенные mutex-ом. А чтобы эффективно ждать появление значений в этих переменных -- std::condition_variable. Чтобы получить уведомление от дочерней нити о том, что memory_pool перестал использоваться, задействуется std::promise и std::future. Как-то многовато для того, чтобы прокинуть одну команду из родительской нити в дочернюю, а затем один сигнал обратно 🙁

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

И вот после того, как все это было сделано, стала терзать мысль о том, что на SObjectizer-е с mchain-ами должно же было бы получиться проще. Терзала она меня, терзала, и в конце-концов заставила потратить немного времени, чтобы сделать вариант на SO-5.

Ну и что хочу сказать? 😉

На SO-5 и компактнее, и проще, и понятнее. На мой сугубо субъективный взгляд.

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

Получается тривиальное взаимодействие: в родительской нити сперва send, затем receive, а в дочерней нити сперва receive из которого уже делается send в обратном направлении.


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

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

  • автоматическое завершение дочерней нити в SObjectizer-варианте как раз уже обеспечивается за счет использования auto_joiner-ов и auto_closer-ов;
  • контроль тайм-аутов ожидания в случае с so_5::receive добавляется элементарно. В случае с примитивами из C++ной библиотеки, в принципе, тоже не сложно, но телодвижений, имхо, все-таки чуть-чуть побольше потребуется.

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

четверг, 2 апреля 2026 г.

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

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

Фильмы

Аватар: Огонь и пепел (Avatar: Fire and Ash, 2025). Лично мне понравился гораздо больше, чем вторая часть. Ничем не удивил, но это образец отлично сделанного аттракциона, после которого не жалко потраченного времени.

Ограбление в Лос-Анджелесе (Crime 101, 2026). В принципе, норм. Но мне не хватило экшена и показалось, что фильм излишне затянут.

Военная машина (War Machine, 2026). Красочно, динамично, прямолинейно, тупо и пафосно. При желании можно посмотреть, а можно и проигнорировать данное "кино".

Ночной замес (Wake Up, 2023). Хороший пример недорогого молодежного слешера. Любители жанра могут спокойно смотреть, здесь нет ничего лишнего, мне даже понравилось как сыграл главный злодей.

На помощь! (Send Help, 2026). Не понял что это было: для серьезного триллера слишком много дурацкого юмора и откровенного треша, для чернушной треш-комедии слишком мало дурацкого юмора и откровенного треша. Как сюда занесло Рейчел МакАдамс просто загадка.

Крик 7 (Scream 7, 2026). Динамично, кроваво. Но тупость большинства персонажей множит все хорошее на ноль.

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

Тропа гнева (2025). Откровенная халтура, лучше не смотреть.

Иерархия (Hierarchy, 2025). Дешевая поделка. Не стоит тратить на это время.

Сериалы

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

Ограбление (Steal, первый сезон, 2025). Нормально. Есть пара моментов, к которым мне бы хотелось придраться, но в целом можно смотреть.

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

Захваченный рейс (Hijack, второй сезон, 2026). К сожалению, можно лишь повторить то, что писал про первый сезон: "Отлично снято и к игре актеров нет претензий. Пожалуй, это все хорошее, что можно сказать про этот сериал. Остальное, как по мне, одни недостатки." Если кому-то понравился первый сезон, то можно глянуть и второй. А вот если не понравился или не смотрели, то лучше бы пройти мимо.

Незнакомка (The Stranger, 2020). Смотреть интересно, но когда сезон заканчивается и начинаешь анализировать итоги, то складывается ощущение, что "нам втирали какую-то дичь".

понедельник, 30 марта 2026 г.

[prog.c++] Говорят, что основная работа над C++26 завершена

Например, об этом написал Герб Саттер. Типа в C++ теперь есть и рефлексия, и контракты, и даже уменьшено число UB. Уж теперь-то точно заживем.

У меня, однако, отношение к данному событию весьма равнодушное.

Ну вот не отношусь к счастливчикам, которые пилят один проект для одной платформы на одном компиляторе. Посему не могу сидеть на самом свежем GCC (или clang-е, или MSVC) под самой-самой свежей ОС и наслаждаться плюшками самого свежего C++. До меня эти нововведения доходят спустя годы. Так что если смогу задействовать C++26 в продакшене году эдак в 2029-ом, то и хорошо.

Кроме того, в современный C++ завозят, вроде бы, крайне полезные вещи, но в таком виде, что оторопь берет.

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

Ну или взять контракты. В Eiffel-е -- это одна из самых классных фич языка, можно сказать киллер-фича. Тогда как в C++26 я что-то не вижу возможности использования old в постусловиях. Такое ощущение, что в C++ных контрактах мне не получится написать что-то вроде:

void push_back(T v) post(this->size() == old this->size() + 1)
{...}

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

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

понедельник, 9 марта 2026 г.

[prog.c++] Как будто бы недоделки в системе C++ных аллокаторов

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


Первая штука -- это отсутствие каких-то простых инструментов для создания нового объекта посредством аллокатора.

Если нам не нужно связываться с аллокаторами, то мы просто вызываем new T (или make_unique<T> или make_shared<T>) и все.

А вот в случае с аллокаторами сперва нужно вызвать у аллокатора allocate чтобы получить блок памяти под объект. А затем нужно вызывать у аллокатора construct, чтобы сконструировать объект в этом блоке. Т.е. два действия вместо одного. При этом, если construct бросает исключение, то нужно вручную освободить блок памяти посредством обращения к deallocate у аллокатора.

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

Так же нет простого метода, который бы взял ссылку на аллокатор и указатель на удаляемый объект и сам бы сперва вызвал destroy для объекта, а потом deallocate для блока памяти. А хотелось бы иметь готовый, а не делать самостоятельно на коленке.

И да, я знаю, что allocate/construct и destroy/deallocate вызываются через allocator_traits, просто не упоминаю об этом для простоты изложения.


В C++17 в стандартную библиотеку добавили std::pmr::memory_resource. И, вроде как, имеет смысл делать свои арены в виде наследников от memory_resource.

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

И тут вопрос: а как быть, если мы не хотим получать исключение? Например, нам хотелось бы иметь аналог new(std::nothrow). Типа попробовали выделить память, если не получилось, то у нас на руках нулевой указатель и мы можем попробовать обработать эту ситуацию без try..catch блока (ведь try..catch -- это дорого).

Мне кажется, что в memory_resource напрашивается метод allocate формата:

void *
allocate(std::nothrow_t,
  std::size_t bytes,
  std::size_t alignment = alignof(std::max_align_t));

и соответствующий ему метод do_allocate.

Но почему-то этого нет 🙁


У аллокатора есть свойство, которые выражаются вложенным типом propagate_on_container_copy_assignment. Если оно эквивалентно std::true_type, то аллокатор должен копироваться при копировании содержимого контейнера в операторе копирования.

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

some_container original{ ... };
...
some_container copy{ original }; // (1)

В точке (1) должен быть вызов original.get_allocator().select_on_container_copy_construction().

Что мне кажется странным и несколько недодуманным, так это то, что потенциально propagate_on_container_copy_assignment и select_on_container_copy_construction могут быть не согласованы.

Т.е. свойство propagate_on_container_copy_assignment может быть std::false_type (а по умолчанию это так и есть), при этом select_on_container_copy_construction может возвращать тот же самый аллокатор. Что приведет к тому, что в конструкторе копирования у нас будет копироваться аллокатор из контейнера-источника. А вот в операторе копирования мы аллокатор из источника копировать уже не будем.

А может быть и другой вариант: propagate_on_container_copy_assignment -- это std::true_type, тогда как select_on_container_copy_construction будет возвращать новый экземпляр аллокатора (не равный исходному). Тогда в конструкторе копирования мы будем получать новый экземпляр аллокатора, а в операторе копирования -- будем получать копию аллокатора из контейнера-источника.

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

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

понедельник, 2 марта 2026 г.

[prog.c++] Применимость идиом copy-then-swap и move-then-swap при наличии кастомных аллокаторов

В продолжение недавно начатой темы. Есть очень удобная идиома copy-then-swap, которая позволяет легко и просто написать для своего типа оператор копирования, обеспечивающий строгую гарантию безопасности исключений.

Для примера рассмотрим некий вымышленный тип, который содержит внутри пару векторов:

class special_container {
  struct description { ... };
  struct payload { ... };

  std::vector<description> m_descriptions;
  std::vector<payload> m_payloads;
...
};

И мы хотим, чтобы у special_container был оператор копирования со строгой гарантией безопасности исключений. Для этого нам потребуются:

  • обычный конструктор копирования;
  • не бросающий исключений swap.

что достигается весьма просто:

class special_container {
  ...
public:
  // Swap сделаем через свободную функцию.
  friend void swap(special_container & a, special_container & b) noexcept
  {
    using std::swap;
    swap(a.m_descriptions, b.m_descriptions);
    swap(a.m_payloads, b.m_payloads);
  }

  // Конструктор копирования.
  special_container(const special_container & other)
    : m_descriptions{ other.m_descriptions }
    , m_payloads{ other.m_payloads }
  {}
...
};

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

special_container &
special_container::operator=(const special_container & other)
{
  special_container tmp{ other };
  swap(*this, tmp);
  return *this;
}

Фокус здесь в том, что возможные исключения вылетят при формировании объекта tmp. Но при этом ничего не меняется в this. А если при конструировании tmp исключений не случилось, то мы заменяем содержимое this содержимым tmp.

Еще один приятный фокус в том, что такая примитивная реализация прекрасно защищает и от присваивания самому себе. Впрочем, если экземпляры special_container "тяжелые", а вероятности самоприсваивания не нулевая, то можно и по старинке:

special_container &
special_container::operator=(const special_container & other)
{
  if(this != std::addressof(other))
  {
    special_container tmp{ other };
    swap(*this, tmp);
  }
  return *this;
}

Пока что все идет замечательно.

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

template<typename Alloc>
class special_container
{
  struct description {};
  struct payload {};

  using alloc_traits = std::allocator_traits<Alloc>;
  using description_allocator = alloc_traits::template rebind_alloc<description>;
  using payload_allocator = alloc_traits::template rebind_alloc<payload>;

  std::vector<description, description_allocator> m_descriptions;
  std::vector<payload, payload_allocator> m_payloads;
...
};

Сможем ли мы и дальше пользоваться идиомой copy-then-swap?

И вот тут у меня есть сомнения. А в попытках разобраться как раз и получился этот пост.

У аллокатора может быть такое свойство как propagate_on_container_swap. Если это свойство выставлено в std::true_type, то при выполнении swap мы можем обменять аллокаторы для контейнеров.

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

воскресенье, 1 марта 2026 г.

[life.cimena] Очередной кинообзор (2026/02)

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

Фильмы

Человек с Земли (The Man from Earth, 2007). Фильм очень не новый, но посмотрел его только сейчас. Как по мне, так отличное кино, базирующееся на хорошем сюжете и тщательно выстроенных диалогах.

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

Убийца в петле времени (Kill Me Again, 2023). На удивление хорошо. Тот редкий случай, когда не ждешь вообще ничего хорошего, а получаешь нормально сделанное кино с вменяемой развязкой. Ну и, прямо скажем, неожиданная вариация на тему "дня сурка".

Бастион 36 (Bastion 36, 2025). Неплохая криминальная драма. Но мне не хватало динамики, какие-то эпизоды просто проматывал, т.к. ну очень уж скучно было.

Казнить нельзя помиловать (Mercy, 2026). Первые 2/3 было прям хорошо. Затем началось что-то невнятное в виде погони и вокруг нее, что очень сильно испортило впечатление. В конце есть хороший твист, который, к сожалению, испорченное впечатление исправить уже не смог.

Сериалы

Мошенники (второй сезон, 2026). Отличное продолжение отличного сериала.

Ночной администратор (The Night Manager, второй сезон, 2025). Если понравился первый сезон, то можно посмотреть и второй. Местами второй похуже (мне выбор некоторых актеров показался странным), местами получше. Буду ждать продолжения.

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

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

Его и её (His & Hers, первый сезон 2026). Смотрибельно, хотя идиотизм некоторых персонажей меня откровенно раздражал. Финал неожиданный, но, как по мне, выглядит несколько чужеродно и притянуто за уши.

Художник (второй сезон, 2025). Начало еще более-менее смотрибельно, но вот завершение -- это просто какой-то позор.

пятница, 27 февраля 2026 г.

[prog.c++] Обеспечивает ли vector::operator=(const vector &) строгую гарантию безопасности исключений?

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

Однако, не все так однозначно™

Пункт первый, далеко не очевидный

std::vector зависит от аллокатора. Хотя, наверное, мало кому доводилось использовать std::vector с аллокатором, отличным от std::allocator. Тем не менее, у вектора есть аллокатор. А у аллокатора есть такое свойство как propagate_on_container_copy_assignment (см., например, здесь). И если это свойство предписывает скопировать в вектор-приемник аллокатор из вектора-источника, то тут возникает тонкий момент: старое содержимое вектора-источника должно быть удалено посредством старого аллокатора.

Если глянуть как этот момент учитывается в реализациях стандартной библиотеки (libstdc++ от GCC или libcxx от LLVM), то можно увидеть, что сперва уничтожается старое содержимое вектора, и лишь затем делается попытка копирования содержимого вектора источника.

Особенно хорошо это видно на примере libstdc++v3:

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

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

Пункт второй, более очевидный

Многое зависит еще и от того, какие гарантии безопасности исключений дает сам тип T, который хранится в vector-е. Если посмотреть на одну из ветвей работы оператора присваивания в libstdc++v3, то можно увидеть, что новое содержимое записывается поверх старого:

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

  • первые N элементов из вектора-источника будут скопированы;
  • на (N+1) элементе возникнет исключение, операция копирования будет прервана.

Получится, что первые N элементов у вектора-приемника получат новые значения, а оставшиеся -- сохранят старые. При это непонятно что будет с (N+1): если у T::operator= строгая гарантия безопасности исключений, то сохранится старое. А вот если нет... Тогда этот элемент окажется в непонятном состоянии.

В сухом же остатке имеем то, что если в операторе копирования для вектора возникнет исключение в T::operator=, то вектор может изменить свое состояние (часть будет иметь новое значение, часть старое). А это никакая не строгая гарантия безопасности.


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

template<typename T, typename Alloc>
void strong_guarantee_copy(vector<T, Alloc> & dest, const vector<T, Alloc> & src)
{
  vector<T, Alloc> fresh_copy{ src, dest.get_allocator() };
  swap(dest, fresh_copy);
}

В этом случае мы сперва получаем копию. Или исключение, если копия не создается по каким-либо причинам. А уже потом перемещаем это новое значение в dest.

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

пятница, 20 февраля 2026 г.

[prog.c++] Дошел до чистого безумия: new(this) another_object_type

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

В моем случае получился код вроде вот такого:

templatetypename ValueT >
void
special_map< ValueT >::fixed_capacity_node::insert_item( ValueT value )
{
   auto & self = this->self_data();
   if( self.size() < self.capacity() )
   {
      ... // use the existing node.
   }
   else
   {
      ... // prepare internal data for a new node type.

      // Replace the node by an instance of the new type.
      new(this) dynamic_capacity_node{ std::move(new_node_internal_data) };
   }
}

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

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

PS. Никому не советую повторять подобное в домашних условиях. Как говорится, все показанное было выполнено специально подготовленными профессионалами 😎 Были предприняты различные предохранительные меры дабы удостоверится, что fixed_capacty_node и dynamic_capacity_node идентичны и по размеру, и по выравниваю. И что после смены типа объекта никто не обращался к нему по старому указателю без std::launder.

понедельник, 9 февраля 2026 г.

[business] Практические выводы из того, что успешный результат в бизнесе не гарантирован

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

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


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

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

И так снова и снова.

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

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


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

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

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


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

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

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

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


Нужно как-то научиться спокойно относиться к потере собственных денег.

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

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

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

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


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

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

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

Отсюда еще один, если можно так сказать, совет: не стоит затягивать с попыткой начать свое дело. Если в 35 вы задумались о том, что пора открывать что-то свое, то лучше не откладывать это до 45. Можно попробовать в 36 и к 45 совершить несколько попыток. Либо что-то увенчается успехом, либо вы поймете, что в найме все-таки спокойнее и денежнее и будете тихо встречать неизбежно приближающуюся старость ;)

понедельник, 2 февраля 2026 г.

[business] О чем вам не раскажут бизнес-спикеры с YouTube

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

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

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

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

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

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

Значит ли это, что абсолютно все те, кто не побеждает, просто недостаточно подготовлены? Что они просто мало тренируются или не соблюдают спортивный режим? Не следуют рекомендациям тренеров, спортивных врачей и других специалистов?

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

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

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

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

Ага. Как же.

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

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

Ну и как в спорте, важно не сколько раз ты упал, а сколько раз поднялся.

воскресенье, 1 февраля 2026 г.

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

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

Фильмы

Лакомый кусок (The Rip, 2025). Смотрибельно. Но по ходу всего фильма не мог отделаться от ощущения, что диалоги какие-то странные: о чем-то персонажи говорят, но какой-то связности, целостности и последовательности не ощущается. Может быть проблема в переводе.

Достать ножи: Воскрешение покойника (Wake Up Dead Man, 2025). Гораздо лучше, чем вторая часть, вполне можно смотреть. Но для меня здесь слишком много театральщины и пафоса, что сильно портит впечатление.

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

Опасный дуэт (The Wrecking Crew, 2026). Не понравилось. Была всего одна или две приличных экшен-сцен, все остальное какая-то скучная муть. Ну и 50-летние дядьки, которые играют 25-летних малолетних дебилов смотрятся еще большими дебилами, чем их герои.

Джентльмен (Ya No Quedan Junglas, 2025). Откровенная халтура, смело можно не смотреть.

Сериалы

Чудовище внутри меня (The Beast in Me, первый сезон, 2025). Очень даже ничего. Не могу сказать, что прям "Вау!", но сюжет хороший, не затянуто, отличная игра актеров. Даже гомосексуальная повестка, которую сюда впихнули, в принципе, оказалась оправдана.

Злые люди (первый сезон, 2025). Очень посредственно. Все очень лубочное и все персонажи какие-то картонные, безжизненные.

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