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

О блоге

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

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

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

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

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

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

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

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

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

В очередной раз с трудом удерживаюсь, чтобы не ввязаться в публичное обсуждение на профильном ресурсе. В этот раз опять на RSDN ;)

Недавно там образовалась тема с самодельным аналогом std::format/fmt::format. Над происходящим в нёй я уже слегка поугорал в LinkedIn. Но т.к. автор сего велосипеда в излишне поучительном (на мой субъективный взгляд, конечно же) тоне выступает в другом треде, то решил краем глаза вглянуть на то, какой же код производит данный оратор. Ну по принципу talk is cheap, show me the code.

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

Как по мне, так подобное не что иное как говнокод. Говнокод как он есть. В чистом, дистиллированном виде.

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

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


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


Простите, дальше будет совсем грубо.

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

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

Но при этом этот работающий код оказывается откровенным говном. Как в примере выше.

Но работающим же.

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

Противопоставить такому аргументу лично я могу только "Ну OK, только я ни за что не буду это сопровождать".

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

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

среда, 16 апреля 2025 г.

[prog.thoughts] Реплика в блог: так ли плохо иметь в C++ отдельный класс для сетевого пакета?

Поскольку я зарекся вступать в публичные споры на LOR/RSDN/Habr, а в Интернете, как водится, кто-то неправ, то попробую разместить свою ремарку здесь, в уютненьком ;)

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

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

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

Представление данных -- это всего лишь конкретный механизм сериализации. Скажем, использование принципа TLV (Tag-Length-Value), как в ASN.1 BER. Или плотная побитовая упаковка как в ASN.1 PER. Или же тегированное текстовое представление, вроде JSON или XML (должны сдохнуть в муках оба, шутка).

Кусок кода, который привели в обсуждении на RSDN, он как раз про представление данных.

Тогда как есть еще одна важная часть, когда мы говорим о каких-то протоколах обмена данными -- это из каких сообщений (PDU -- protocol data unit) состоит сам обмен.

Типа того, что есть сообщение handshake и ответное сообщение handshake_ack. Есть сообщение subscribe и есть ответы sub_ack и sub_nack. Есть сообщение publish и есть ответные pub_ack и pub_nack, а также сопутствующее delivery_report. И т.д., и т.п.

Так вот, представление данных определяет то, как содержимое PDU сериализуется.

Но представление данных не есть удобный способ работать с самими PDU в коде.

Достаточно часто в наше время встречается ситуация, когда PDU представляется в виде JSON. Входящий пакет парсится, у нас в коде появляется что-то вроде nlohman::json или RapidJson::Value. И дальше программисты любятся с этими JSON-объектами как умеют. А умеют практически никак 😣 Например, если в PDU есть поле priority, то из JSON-объекта запрашивают это поле вручную по строковому имени. Если это поле нуждается в какой-то валидации (хоть при извлечении, хоть при сохранении), то это тоже делают вручную. Если делают.

Мне же думается, что работать с PDU в программе гораздо удобнее, когда PDU представляется в виде конкретного C++ного класса с нужными методами getter-/setter-ами. И когда в эти самые методы вставлена валидация значений. Более того, такие классы могут (и должны, когда есть возможность) проверять корректность значений при сериализации/десериализации. Например, если поле A в PDU имеет вот такое значение, то поле B не может быть пустым, а поле C, напротив, должно быть пустым.

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

Однажды в прошлом с чем-то таким столкнулся. Даже рассказывал об этом на RSDN и упоминал в блоге. То решение на Ruby оказалось удобным. Если доведется еще раз с подобной задачей, то буду смотреть в ту же сторону (не важно, Ruby будет использоваться для кодогенерации, Python или еще что-то). Хотя современный C++ в области шаблонной магии далеко ушел от C++03, но все равно не думаю, что подобная задача будет хорошо решаться в рамках C++20 или C++23.


PS. Еще раз по поводу кода, который показали на RSDN. Как раз тот случай, когда смотришь в код, внутренний голос спрашивает "а что так навороченно то, нельзя ли пропроще?", но без вдумчивого изучения ответа на этот вопрос нет. А вдумчиво изучать нет желания ;)


PPS. Мораль всего поста: не беритесь судить без глубокого погружения.

понедельник, 14 апреля 2025 г.

[prog] Похоже, у меня уже не получается писать код без комментариев

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

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

Изредка приходится делать маленькие (или не очень маленькие) программки "на выброс". Буквально на один-два проверочных запуска, после чего все написанное отправляется в корзину. Вот там, как правило, вообще никаких комментариев нет. Точнее не бывало. До недавних пор.

Как раз давеча пришлось такими маленькими программками заниматься.

И вот что удивительно: после того как объем кода превысил 150-200 строк я внезапно поймал себя на том, что больше не могу без комментариев. Руки сами отказываются дописывать новые строки без предварительный поясняющих комментариев.

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

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

Весь этот код, по сути, одноразовый. Заливал его на github только ради простоты переноса между разными компьютерами. Ну и экспериментировать с кодом, когда он под контролем версий, таки спокойнее и удобнее.

PS. Неприятным побочным эффектом от привычки писать комментарии становится все возрастающая нетерпимость к коду без комментариев.

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

PPPS. Проблема с бесполезными и тривиальными комментариями не в том, что они бесполезны. Проблема в том, что программистов не научили комментарии писать. Поэтому эти самые программисты и пишут бесполезные комментарии. Ну не умеют писать полезные и это объясняет текущее состояние дел почти на 95%.

PPPPS. Есть у меня ощущение, что в последние 15 лет программистов-то и программировать уже не учат. Что уж тут говорить про обучение написанию комментариев 🥺 ИМХО, это один из китов, на которых покоится миф о самодокументирующемся коде.