вторник, 1 января 2030 г.

О блоге

Более тридцати лет я занимался разработкой ПО, в основном как программист и тим-лид, а в 2012-2014гг как руководитель департамента разработки и внедрения ПО в компании Интервэйл (подробнее на LinkedIn). В настоящее время занимаюсь развитием компании по разработке ПО stiffstream, в которой являюсь одним из соучредителей. Поэтому в моем блоге много заметок о работе, в частности о программировании и компьютерах, а так же об управлении.

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

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

[life.photo] Характерный портрет: вы и ваш мир моими глазами. Безвозмездно :)

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

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

пятница, 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 мы можем обменять аллокаторы для контейнеров.

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