пятница, 6 сентября 2024 г.

[prog.c++] В склерозник: красивый кусок кода для подсчета смещения полей объекта и их выравнивания

В LinkedIn встретил ссылку на реализацию тупла без использования рекурсии: тыц. Реализация активно эксплуатирует C++ный fold expression и std::integer_sequence. Вот прям отличная демонстрация того, как эти фичи могут (и должны?) применяться.

Еще очень удачно совпало, что ссылка эта попала мне на глаза вскоре после того, как я проделал в чем-то похожую работу. Правда, у меня ситуация была чуть сложнее, ведь в тупле все N полей присутствуют всегда, поэтому размер всех туплов одного типа одинаков и фиксирован. Тогда как в моем случае объекты могут состоять из разного набора полей, поэтому нужно размер каждого объекта определять индивидуально, да и расположение полей в каждом объекте может быть уникальным. Так что в своей реализации без метапрограммирования на базе рекурсии я не смог обойтись. Возможно, еще и потому, что у меня мало опыта с fold expression и std::integer_sequence.

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

Этот кусочек кода отвечает за подсчет расположения полей внутри объекта с учетом их правильного выравнивания. В результате своей работы функция calculate_positions возвращает std::array размером (N+1), где элементы 0..(N-1) содержат смещение i-го поля, а элемент N -- общий размер объекта.

Оригинал кода можно увидеть здесь, а вот чуть-чуть модифицированная мной версия:

template <class T>
struct PositionWrapper {};

template <std::size_t _last, std::size_t... _is>
struct Positions
{
  static consteval auto to_array() {
    return std::array<std::size_tsizeof...(_is) + 1>{_is..., _last};      
  }
};

template <class T, std::size_t _last, std::size_t... _is>
consteval auto operator+(
  const Positions<_last, _is...>& /*_sizes*/,
  const PositionWrapper<T>& /*_w*/)
{
  if constexpr (_last % alignof(T) == 0) {
    constexpr auto last_new = _last + sizeof(T);
    return Positions<last_new, _is..., _last>{};
  } else {
    constexpr auto last_corrected = (_last / alignof(T) + 1) * alignof(T);
    constexpr auto last_new = last_corrected + sizeof(T);
    return Positions<last_new, _is..., last_corrected>{};
  }
}

template <class... Types>
consteval auto calculate_positions() {
  return (Positions<0>{} + ... + PositionWrapper<Types>{}).to_array();
}

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

[prog.c++] Конструирование объекта в котором физически могут отсутствовать некоторые поля

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

Пришлось заняться этим потому что в текущем проекте было большое количество объектов вида:

struct data {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  std::vector<first_opt_attribute_type> m_first_type_attrs;
  std::vector<second_opt_attribute_type> m_second_type_attrs;
  std::vector<third_opt_attribute_type> m_third_type_attrs;
};

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

Объектов типа `data` было много, в некоторых случаях десятки миллионов. И на таком количестве хранение пустых std::vector внутри миллионов объектов типа `data` оказывается очень расточительным (ведь каждый пустой std::vector -- это, как минимум, 24 байта -- size, capacity + указатель на блок с данными).

Объекты же `data` создавались в динамической памяти и ссылки на них хранились как std::unique_ptr.

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

В общем, хотелось бы, чтобы поля m_first_type_attrs, m_second_type_attrs и m_third_type_attrs таки оставались внутри `data`, но ничего бы не потребляли, если были пустыми.

Здесь бы очень ко двору пришлись бы массивы вроде чего-то такого:

struct data {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  std::size_t m_first_type_attrs_count;
  first_opt_attribute_type m_first_type_attrs[m_first_type_attrs_count];

  std::size_t m_second_type_attrs_count;
  second_opt_attribute_type m_second_type_attrs[m_second_type_attrs_count];

  std::size_t m_third_type_attrs_count;
  third_opt_attribute_type m_third_type_attrs[m_third_type_attrs_count];
};

Но в C++ таких массивов нет. Это во-первых. А во-вторых, даже такой способ хранения, будь он возможен, все равно был бы расточительным. Ведь если для объекта нет атрибутов, то поля *_attrs_count с нулевыми значениями в нем все равно есть. Три поля std::size_t -- это 24 байта, умножаем на десяток миллионов объектов `data` и теряем пару десятков мегабайт на ровном месте 🙁

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

Получилось что-то вроде такого:

// Описание части, которая в объекте присутствует всегда.
struct data_header {
  mandatory_field_one m_one;
  mandatory_field_two m_two;
  mandatory_field_three m_three;

  // Вспомогательные типы для идентификации опциональных полей.
  struct first_attr_tag {};
  struct second_attr_tag {};
  struct third_attr_tag {};

  // Типы опциональных полей.
  using first_attr_vec = compound::vec<first_attr_tag, first_opt_attribute_type>;
  using second_attr_vec = compound::vec<second_attr_tag, second_opt_attribute_type>;
  using third_attr_vec = compound::vec<third_attr_tag, third_opt_attribute_type>;
};

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

std::vector<second_opt_attribute_type> attrs{...};

auto my_data = data::build(
  // Данные для инициализации фиксированной части должны быть всегда.
  data_header{...},
  // А вот опциональные данные нужны только те, которые в объекте присутствуют.
  data::second_attr_vec::move_from(attrs));

// Доступ к полям фиксированной части возможен напрямую.
std::cout << my_data->m_one << std::endl;
std::cout << my_data->m_two << std::endl;

// Доступ к опциональным полям нужно проверять.
if(my_data->has_field<data::second_attr_tag>()) {
  // Поле есть, можно с ним работать.
  for(const auto & a : m_data->get_field_ref<data::second_attr_tag>()) {
    ...
  }
}

Не буду вдаваться в подробности, т.к. все это делалось в закрытом проекте. Но скажу, что весь фокус тут в функции-фабрике build, которая вычисляет сколько же места потребуется (с учетом необходимых выравниваний) и создает обычный динамический массив std::byte. А уже дальше внутри этого массива посредством placement new размещается все то, что в объекте должно присутствовать.

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

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

Что напрягало, как это функциональный стиль C++ного метапрограммирования (работа велась в рамках C++20). Вся эта рекурсия по спискам типов... 😓 Если сталкиваешься с этим самым метапрограммированием раз в пару лет, то непросто на эти рекурсии перестроится.

Еще, конечно же, доставляла тема с std::launder. Но к ней мне придется вернуться еще раз, как минимум.

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

ЗЫ. Кстати говоря, пригодился трюк вот из этой статьи: "Pulling a single item from a C++ parameter pack by its index".

воскресенье, 1 сентября 2024 г.

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

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

Фильмы

Ненасытные люди (Greedy People, 2024). Как по мне, так это что-то вроде "Фарго", но на минималках. Совершенно не шедевр, но смотреть интересно, а на фоне остального современного шлака, как вообще выглядит нормально сделанным кино.

Зачинщики (The Instigators, 2024). Не очень понял что это было. Для комедии мне не хватило юмора, для серьезной криминальной драмы не хватило серьезности и драмы. Такое впечатление, что материала было на минисериал, но почему-то решили сделать один полнометражный фильм, в котором не удалось раскрыть и десятой доли всех промелькнувших в кадре персонажей.

Нефариус (Nefarious, 2023). Мне почти что зашло. На мой личный вкус первые 3/4 фильма просто шикарные, все держится на диалогах и актерской игре. А вот развязку, имхо, можно было сделать и покруче. Так что финал слегка разочаровал, но вот то, что ему предшествовало мне понравилось.

Материнский инстинкт (Mothers' Instinct, 2024). Качественно снято. Но! Во-первых, никто из героев не вызвал сопереживания. Во-вторых, за происходящим оказалось следить не интересно и, есть ощущение, что авторы как-то и не сильно пытались зрителя запутать. Общее впечатление: добротно сделано, но не цепляет.

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

Смерч 2 (Twisters, 2024). Красочно сделанный динамичный аттракцион. Но все настолько шаблонно, а персонажи настолько картонные, что все последующие события прочитываются наперед уже после первых 15 минут фильма.

Отчаянные наследники (El favor, 2023). Средненькая комедия из категории "богатые тоже плачут", местами откровенно глупая, местами смешная.

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

Сериалы

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

ГДР (первый сезон, 2023). Мне не зашел. Во-первых, как-то все подзатянуто. Во-вторых, показались лишними вставки с Горбачевым и его окружением. Вроде как сериал должен был быть шпионским детективом, а не пересказом баек о дурачке Горбачеве. В-третьих, чувствовалось, что бюджет не позволяет воссоздать более-менее большой кусочек Берлина 1989-го года. И это не шло в плюс сериалу. В общем, не рекомендую.

Фурия (Furia, первый сезон, 2021). Во-первых, очень затянуто. Во-вторых, слишком уж часто возникает вопрос "Что за фигню нам здесь показывают?" Так что первый сезон не зашел, желание посмотреть второй сезон не возникло.

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

Пара фильмов, которые я затрудняюсь адекватно оценить.

Бордерлендс (Bordarlands, 2024). Не понял на кого кино рассчитано: для взрослых оно слишком детское, для детей слишком взрослое. На меня произвело впечатление откровенного треша, но такого, про который можно сказать "настолько плохо, что даже хорошо".

Покажи мне Луну (Fly Me to the Moon, 2024). Не являюсь ценителем жанра комедийных мелодрам, так что понятия не имею, как этот фильм оценивать. Но если брать отдельно сюжет, отдельно операторскую работу и отдельно игру актеров, то в целом это один из наиболее добротных и вменяемых фильмов за последние несколько месяцев.