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

О блоге

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

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

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

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

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

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

вторник, 13 мая 2025 г.

[prog.c++] Введение в C++ типов std2::uint*_t, std2::int*_t, std2::float_t, std2::double_t?

Пост навеян флеймообразующей статьей с Хабра под названием "Как Мэтт Годболт «продал» мне Rust (рассказав о C++)" (которая является переводом статьи Matt Godbolt sold me on Rust (by showing me C++)). Сама статья мне не нравится тем, что на C++ навешивают собак, унаследованных из чистого Си. Да еще и демонстрируют использование приемов, за применение которых в коде надо бы отрывать руки (я про вызов atoi для выделения числа из строки).

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

А раз проблема для C++ актуальна, то было бы хорошо ее решить.

Внезапно™ подумалось, что сделать это может быть не так уж и сложно.

Нужно ввести новые стандартные типы:

  • std2::int8_t, std2::int16_t, std2::int32_t, std2::int64_t;
  • std2::uint8_t, std2::uint16_t, std2::uint32_t, std2::uint64_t;
  • std2::int_fast8_t, std2::int_fast16_t, std2::int_fast32_t, std2::int_fast64_t;
  • std2::uint_fast8_t, std2::uint_fast16_t, std2::uint_fast32_t, std2::uint_fast64_t;
  • std2::int_least8_t, std2::int_least16_t, std2::int_least32_t, std2::int_least64_t;
  • std2::uint_least8_t, std2::uint_least16_t, std2::uint_least32_t, std2::uint_least64_t;
  • std2::uintmax_t
  • std2::uintptr_t
  • std2::float_t
  • std2::double_t

Главным (и, возможно, единственным) отличием от их старых собратьев будет то, что компилятор будет запрещать неявные преобразования значений между этими типами или типами из предыдущих стандартов C++ (включая унаследованные из Си и, ИМХО, совершенно бесполезные в современном мире short, int, long и пр.) Т.е. если мы написали так:

void f(std2::uint32_t n) {...}

То компилятор не позволит нам сделать так:

f(-1);

или так:

f(1.2);

или так:

int i = some_calculation();
f(i);

и даже так:

std2::uint_fast32_t r = another_calculation();
f(f);

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

Если же в старом коде использовались std::int*_t и мы захотели адаптировать его под новый стандарт, то достаточно будет просто заменить std:: на std2:: простым контекстным поиском.

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

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

среда, 7 мая 2025 г.

[prog.c++] Настройка библиотеки под нужды пользователя чисто C++ными средствами

Подсмотрел вот в этом блог-посте - A New Approach to Build-Time Library Configuration - интересный трюк, который захотелось утащить к себе в склерозник на всякий случай.

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

Например, допустим, что мы используем в библиотеке какие-то временные вектора на стеке (скажем std::array<unsigned char, N>) и пользователь должен уметь управлять значением N. Чтобы он мог увеличить N когда это нужно или уменьшить N когда не нужно.

Традиционно в C++ для этих целей используют унаследованный из чистого Си подход с определением символов (они же define-ы). Например, определяют MY_LIB_DEFAULT_N через ключики компиляции. Что-то вроде:

g++ -D MY_LIB_DEFAULT_N=10240 ...

А в коде библиотеки мы делаем что-то вроде:

#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif

namespace my_lib {

constexpr tmp_array_size = MY_LIB_DEFAULT_N;

...
void some_func() {
  std::array<unsigned char, tmp_array_size> tmp_array;
  ...
}

/* namespace my_lib */

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

#define MY_LIB_DEFAULT_N 10240
#define MY_LIB_LOGGING_POLICY 0
#define MY_LIB_TRACING_MODE 3
...

В самой же библиотеке мы имеем специальный заголовочный файл impl/config.h, который будет иметь вид:

// Подключаем то, что выставил пользователь.
#include "user-config.h"

// А потом разбираемся с тем, что пользователь выставил или не выставил.
#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif
#if !defined(MY_LIB_LOGGING_POLICY)
  #define MY_LIB_LOGGING_POLICY 2
#endif
#if !defined(MY_LIB_TRACING_MODE)
  #define MY_LIB_TRACING_MODE 10
#endif
... // Далее преобразуем define-ы в типизированные константы.

Для проверки наличия файла user-config.h, как показано в упомянутом выше блог-посте, можно использовать __has_include:

// Подключаем то, что выставил пользователь.
#if __has_include("user-config.h"
  #include "user-config.h"
#endif

// А потом разбираемся с тем, что пользователь выставил или не выставил.
#if !defined(MY_LIB_DEFAULT_N)
  #define MY_LIB_DEFAULT_N 4096
#endif
#if !defined(MY_LIB_LOGGING_POLICY)
  #define MY_LIB_LOGGING_POLICY 2
#endif
#if !defined(MY_LIB_TRACING_MODE)
  #define MY_LIB_TRACING_MODE 10
#endif
... // Далее преобразуем define-ы в типизированные константы.

Но это Си-ное наследие. А ведь можно использовать и чисто C++ные механизмы.

Так, в нашем impl/config.h может быть:

namespace my_lib {

...

namespace config_defaults {
  constexpr std::size_t tmp_array_size = 4096;
  constexpr logging_policy_t logging_policy = logging_policy_t::minimal;
  constexpr tracing_mode_t tracing_mode = tracing_mode_t::runtime_control;
  ...
/* namespace config_defaults */

using namespace config_defaults; // А вот и трюк.

/* namespace my_lib */

// Подключаем то, что выставил пользователь.
#if __has_include("user-config.h"
  #include "user-config.h"
#endif

Теперь пользователь в своем user-config.h может написать, например, так:

namespace my_lib {

constexpr std::size_t tmp_array_size = 10240;
constexpr logging_policy_t logging_policy = logging_policy_t::detailed;
constexpr tracing_mode_t tracing_mode = tracing_mode_t::off;
  ...

/* namespace my_lib */

И эти значения будут иметь больший приоритет, чем те, которые были определены в нашем my_lib::config_defaults, а затем введены в область видимости my_lib через using namespace.


Любопытно, что что-то похожее мы использовали в RESTinio для упрощения настройки свойств сервера (пример). Но у нас нужно было наследоваться от trait-класса. А тут без наследования, что для каких-то случаев гораздо удобнее.

четверг, 1 мая 2025 г.

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

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

Фильмы

Я слежу за тобой (Geunyeoga jukeotta, 2024). Очень даже неплохо. И сюжет интересный, и подача прикольная, и нет чрезмерного кривляния, присущего азиатским фильмам.

Наследство (Inheritance, 2025). Сильно двойственные чувства. С одной стороны, отличный пример того, как задешево снять хорошую историю. С другой стороны, манера съемки меня стала раздражать уже через 10 минут, а к концу откровенно бесила.

Опустошение (Havoc, 2025). Сам по себе фильм средний, но зато динамичный с большим количеством экшОна и кровищи. А вот что его выделяет -- так это обилие компьютерной графики. Если в последние лет пять-шесть в практически во всех боевиках выстрелы из стрелкового оружия нарисованы на компьютере, то здесь пошли еще дальше. У меня сложилось ощущение, что в "Опустошении" даже сцена погони на автомобилях была отрисована полностью посредством компьютерной графики.

Расплата 2 (The Accountant 2, 2025). Если понравилась первая часть, то можно смотреть и вторую. Но, как по мне, вторая была покруче (в том числе и в том, как были сняты перестрелки), хотя и более занудная.

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

Денежная игла (Body Brokers, 2020). По качеству средне, и не хорошо, и не плохо. Меня история не зацепила, но может быть тем, кому интересны социальные проблемы США и понравится.

Любовь - боль (Love Hurts, 2025). Досмотреть-то досмотрел, но жалею, что не выключил где-то после 25-30 минут. Вообще после просмотра главный вопрос: для кого вообще это снимают. А как рекомендация: не смотреть и не тратить свое время.

Сериалы

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

Тихая гавань (Safe Harbor, 2025). Слишком уж скучно, нужно и затянуто. Уместили бы все это в 4 серии, было бы гораздо лучше. Ну и лично для меня в этом сериале не оказалось ни одного персонажа, за которого не то, чтобы хотелось переживать, но вообще хотя бы следить за его судьбой.

Джек Ричер (Reacher, третий сезон, 2025). Если первые два сезона понравились, то можно посмотреть и третий. Но если уже от второго возникло отторжение, то от просмотра очередного сезона лучше воздержаться. В финальной серии, традиционно, какое-то царство победившего маразма.

Хороший человек (первый сезон, 2020). Мне показалась, что это полная муть, да еще и снятая больными на всю голову людьми.

Начинал, но не закончил

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

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

вторник, 29 апреля 2025 г.

[prog.c++] Захотелось тут странного для std::vector

Недавно столкнулся с ситуацией, когда хотелось у std::vector вызвать метод resize для увеличения размера вектора, но без инициализации новых элементов. Что-то вроде:

std::vector<some_value> unpacked;
std::size_t num_items = detect_number_of_items_to_unpack(packed_data);
// Явно указываем, что начального значения нет.
unpacked.resize(num_items, std::keep_uninitialized);
// Просто перезаписываем память, которая уже выделена.
unpack_to(packed_data, unpacked.data());

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

Понимаю, что в std::vector этого не будет никогда, т.к. слишком уж легко ошибиться и после подобного resize обратиться к неинициализированному объекту. Но вот в данном конкретном случае захотелось, чтобы такое в std::vector было.


А еще моя давнишняя мечта иметь в std::vector конструктор, который бы позволял задавать не size, а capacity. Чтобы можно было писать:

std::vector<some_value> unpacked{ std::with_capacity(n) };

вместо:

std::vector<some_value> unpacked;
unpacked.reserve(n);

суббота, 26 апреля 2025 г.

[prog.c++] Еще раз вспомнил про easy_parser из RESTinio

Потребовалось намедни разобрать строку, в которой содержатся разделенные запятыми неотрицательные целые числа. Делал такое недавно посредством std::regex и мне не понравилось. Скорее всего потому, что и с регулярными выражениями на "Вы", и интерфейс std::regex в C++, по моим ощущениям, делали какие-то инопланетяне (не, наверное там все сделано по уму и с прицелом на сценарии, которые мне даже и не снились). Но когда прикасаешься к std::regex раз в два года, то как-то все слишком сложно 🙁

В общем, не понравился мне недавний опыт.

Тут-то и вспомнилась штука, которую мы делали в RESTinio для упрощения разбора HTTP-заголовков. easy_parser называется.

Вспомнилась, решил восстановить в памяти, полез смотреть что и как...

Слегка прифигел от собственной крутизны 🙂

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

Продолжить можно тем, что построен easy_parser на простых идеях: есть producer-ы, которые производят значения из разобранного текста, есть consumer-ы, которые потребляют произведенные значения. Связь producer-а и consumer-а образует выражение (clause). Можно делать составных producer-ов из последовательности clause-ов. Вот, собственно, и все.

Если вникнуть в эту незамысловатую схему, то дальше сложностей не возникает.

Да, конечно, когда в дело вступают transformer-ы, или когда нужно порождать не единичные значения, а наборы значений (std::vector чего-нибудь, к примеру), то потребуется въехать чуть поглубже. Но тоже не сказать, чтобы архисложно.

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

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

Но это было давно. Последний раз прикасался к easy_parser, если мне не изменяет склероз, в декабре 2023-го года.

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

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

[[nodiscard]]
std::vector<std::size_t>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto parser = ep::produce< std::vector<std::size_t> >(
            ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container())
         );


   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of dimensions size ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Ключевой момент, имеющий отношение к easy_parser, выделен жирным. Там все просто ;) Нужно получить вектор чисел. Для этого требуется одно число (которое сразу же помещается в контейнер). А за ним может быть еще 0 или более последовательностей из запятой и еще одного числа (которое сразу же записывается в контейнер).

Вот, собственно, и всё.

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

Но я подозревал, что простого списка чисел мне окажется недостаточно.

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

size[ '(' start [',' step] ')' [ ',' size [ '(' start [ ',' step] ')' ...]]

Т.е. каждому значению size может быть привязано два необязательных значения start и step. Если start и step есть, то они должны быть заключены в круглые скобки. Может быть либо одно значение start, либо и start, и step. В последнем случае start и step разделяются запятой.

Если я еще не забыл полностью PEG, то в PEG это должно выражаться чем-то вроде:

values := one_value (',' one_value)*
one_value := NUMBER start_step?
start_step := '(' NUMBER (',' NUMBER)? ')'

Для чего показанный выше пример был переписан вот так:

struct DimParamsFromUser {
   std::size_t _size{};
   std::optional<int> _startFrom{ std::nullopt };
   std::optional<int> _step{ std::nullopt };
};

[[nodiscard]]
std::vector<DimParamsFromUser>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto oneDimParamsP = ep::produce<DimParamsFromUser>(
            ep::non_negative_decimal_number_p<std::size_t>()
                  >> &DimParamsFromUser::_size,
            ep::maybe(
               ep::symbol('('),
               ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_startFrom,
               ep::maybe(
                  ep::symbol(','),
                  ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_step
               ),
               ep::symbol(')')
            )
         );

   auto parser = ep::produce< std::vector<DimParamsFromUser> >(
            oneDimParamsP >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               oneDimParamsP >> ep::to_container())
         );

   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of sizes of dimensions ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Если присмотреться, то можно узнать чуть ли не оригинальные PEG-правила. С поправкой на то, что ()? из PEG записывается как maybe, а ()* в C++ коде выражается через repeat(0, N, ...).

Но самое, на мой взляд, красивое -- это то, что определение для переменной parser осталось практически таким же, только `std::size_t` заменили на `DimParamsFromUser`, а `non_negative_decimal_number_p` на `oneDimParamsP`.

Оно, конечно же, понятно, что каждый кулик свое болото хвалит... Но ведь лепота же! 🙂


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