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

О блоге

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

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

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

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

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

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

понедельник, 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.