понедельник, 22 апреля 2024 г.

[prog.c++] Вредных советов пост: пара-тройка способов усложнить жизнь тем, кто будет сопровождать код после вас

Хочу поделиться многолетним опытом. Работает на 100%, а иногда и на все 146%. Так смело можете брать на вооружение, если решили конкретно поднасрать своим коллегам.

Во-первых, используйте std::pair и std::tuple для долгоживущих объектов. Особенно если эти объекты свободно перемещаются между различными частями программы.

Поверьте, настоящие программисты (тм) всегда будут точно знать, что означают it->first или std::get<3>(*it).

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

Не обращайте на них внимания. Это же склеротики, которые не могут удержать в памяти даже два простых названия -- first и second, хотя что может быть проще? Ну и наверняка им платят за строчки кода, а не за решение проблем. Вот они и фигачат по 100500 строк там, где можно обойтись одним std::pair-ом.

Во-вторых, забудьте такую вещь, как strong typedef. Тем более, что в C++ из коробки ее и нет. Так что это вообще не ваш путь, а те, кто про пытается заикаться о strong typedef, просто учились программировать на Pascal-е, а значит старпёры и ничего не понимают в современной разработке софта.

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

Вводить какие-то новые типы UserName и PlainTextPassword? Ради чего? Ради того, чтобы случайно не пихнуть пароль туда, где ожидается имя пользователя?

Да это вообще смешно. Ведь так никто не делает. А если вдруг, так это от кривизны рук. Настоящие программисты (тм) таких ошибок не допускают.

Но даже если какой-то джуниор-разработчик такую ошибку допустит, то он же сам себе злобный буратино, да и тестами все это ловится на раз-два.

Так что никаких strong typedef-ов. Забудьте как страшный сон.

Ну и в качестве бонуса: вам не нужны ни using, ни typedef. Если у вас есть map, значением которого будет vector из queue, то вот прямо так и нужно записывать. Никаких вспомогательных using-ов и typedef-ов!

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

void process_appropriate_items(
      const std::vector<
            std::pair<
                  std::pair<intint>,
                  std::tuple<std::string, std::string, std::string, std::string> > > & what)
{
   for(const auto & p : what)
      if(p.first.second > 0)
         process(std::get<3>(p.second));
}

Зачем выдумывать что-то еще? Неужели вам не понятно что означает p.first.second и std::get<3>(p.second)? Да ну, не может такого быть!


Перестрахуюсь на всякий случай: в каждой фразе здесь должен быть тег "сарказм". Иногда и не один.

среда, 17 апреля 2024 г.

[life;work;business] 10 лет с начала нового этапа в профессиональной жизни

Если мне не изменяет склероз, где-то в этих числах апреля 2014-го года я окончательно ушел из тогдашнего "Интервэйл". Причем уходил уже подспудно понимая, что дальше нужно пробовать создавать свой маленький свечной заводик свое собственное дело, а не искать новую работу на дядю. Хотя для того, чтобы "созреть" и решиться зарегистрировать свою фирму потребовалось более двух лет.

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

Главный упрек, который многократно доводилось слышать в адрес разработчиков (т.е. в том числе и в свой) от ТОП-менеджеров, особенно от "продажников": программисты не знают, откуда берутся деньги на оплату их труда.

Теперь знаю 😉

Теперь и сам могу упрекнуть кого хошь 😆

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

Так что фактор "не иметь никого над головой" имеет место быть. Однако, он с лихвой компенсируется другими факторами. Так что тут не все так однозначно... Почти как в анекдоте: "Быть подкаблучником означает, по меньшей мере, наличие крыши над головой" 😀

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

Советовать кому-то пойти по этому же пути не буду.

Все это непросто. И одна из вещей, которые сложно осознать заранее -- это то, что вся ответственность и весь груз оказывается на ваших плечах. Если не вы, то никто. Да, это банально, но так и есть. К сожалению, пока не попробуешь, не прочувствуешь в полной мере. А когда прочувствуешь, то метаться уже поздно 😉

Так что могу лишь перефразировать классика: открывать свое дело нужно не тогда, когда ты можешь это сделать, а тогда когда не можешь не сделать.

Вопрос лишь в том, чтобы отличить одно от другого.

Ну а я продолжу свой путь дальше. Посмотрим, что будет в последующие десятилетие. Как показывают последние 4-5 лет, случится может такое, что и нафантазировать трудно. Поэтому не исключаю и сценария, когда буду обычным программистом с минимумом ответственности, работающим с 9 до 17 над тасками из Jira, все мысли которого посвящены выращиванию капусты на домашнем огороде. Who knows, who knows... 🤔

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

ЗЫ. Что-то много смайликов в посте. Не специально, просто "...Это нужно для того, чтобы человечество весело расставалось со своим прошлым" (с).

ЗЗЫ. Тогдашний "Интервэйл" все еще вспоминается с большой теплотой. Действительно, чуть ли не лучшие годы и все такое... Но то тогдашний. Вроде как от тогдашнего сейчас мало что осталось.


Поясню для тех, кто во фразе "обычным программистом с минимумом ответственности" видит только "обычным программистом". Такие персонажи из-за недостатка мозгов (простите, но это самое политкорректное определение, которое смог придумать) могут начать спрашивать "Ну и зачем было тратить годы на изучение C++/карьерный рост/развитие бизнеса/нужное-вписать, чтобы затем оказаться обычным программистом?"

Акцент же следует делать на "с минимумом ответственности". Когда ты лично отвечаешь за все и платишь наемным сотрудникам собственные деньги из собственного кармана, то этой самой ответственности временами становится ну очень уж много. Отсюда и мысли о том, а почему бы не починять тихо и мирно примус и никого не трогать? И ключевое не в "починять примус", а в "тихо и мирно". Надеюсь, что доступно изложил.

понедельник, 15 апреля 2024 г.

[work] Открыт для сотрудничества в качестве C++ разработчика

В виде (суб)контракта с нашей компанией СтифСтрим.

Прикладной специализации не имею, за тридцать лет в программизме приходилось заниматься разными вещами. Очень часто это были инфраструктурные вещи -- фреймворки, библиотеки и утилиты, которые затем использовались для решения прикладных задач. В последние годы поддерживал и развивал C++ные библиотеки SObjectizer, RESTinio и json_dto.

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

Работаю не быстро, но качественно, беру недорого.

Оценить мой уровень можно, например, про проекту aragata, реализованному мной практически в одиночку. Код можно увидеть на GitHub-е, на Хабре есть две статьи о том, что это и как работает: вводная статья и описание сделанных по результатам нагрузочных испытаний оптимизаций + вот этот пост.

В качестве дополнительных примеров: timertt (+ документация), so5extra (+ документация) -- эти проекты так же написанные мной самостоятельно.

Связаться со мной можно через eao197 на gmail тчк com. Если кому-то интересен профиль на LinkedIn, то вот.


Это сообщение повисит какое-то время вверху. Потом будет видно, имеет ли смысл пытаться дальше оставаться в C++.

[prog.c++.wtf] Один из самых странных паттернов в коде, с которым доводилось сталкиваться...

В течении последнего года, может быть чуть меньше, регулярно стал натыкаться в разных кодовых базах на паттерны вроде вот такого (внимание на тело цикла for):

bool does_contain_apropriate_item(
   const item_container & items,
   const search_criteria & search_params)
{
   for(const auto & i : items) {
      if(!does_meet_coditions(i, search_params)) {
         continue;
      }

      return true;
   }

   return false;
}

Зачем нужен continue в цикле и почему нельзя сразу написать:

bool does_contain_apropriate_item(
   const item_container & items,
   const search_criteria & search_params)
{
   for(const auto & i : items) {
      if(does_meet_coditions(i, search_params))
         return true;
   }

   return false;
}

Большая и неразрешимая для меня загадка.

Возможно, выросло поколение, которое лояльно относится к break/continue в циклах. А может уже и не одно поколение.

PS. Почему не используется в таких случаях std::find_if -- это отдельный вопрос. Местами не такой простой, как может показаться. Тем более, что я привел не реальный фрагмент кода, а общую схему того, что вижу в последние месяцы регулярно. Детали же часто сильно отличаются, иногда еще нужен и индекс найденного элемента, так что на практике std::find_if не всегда такая уж удобная замена.

вторник, 2 апреля 2024 г.

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

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

Фильмы

Холоп 2 (2023). Вполне на уровне первого фильма. Но, как раз это немного и разочаровывает: как все предсказуемо, хотелось бы, чтобы авторы чем-то удивили. А удивили разве что масштабной батальной сценой в финале. Вот это было здорово.

Дом у дороги (Road House, 2024). Если раньше смотрели "Дом у дороги" с Патриком Суэйзи от 1989-го, то современный ремейк будет "немного предсказуемым". Но глянуть его можно хотя бы ради красочно поставленных рукопашных сцен.

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

Ангел мести (Fatum, 2023). Посредственно, но смотрибельно. Хотя и далеко не шедевр.

Анатомия падения (Anatomie d'une chute, 2023). Если не жалко потратить 2.5 часа своего времени на то, чтобы узнать за что же именно был вручен "Оскар", то можно и глянуть. Но меня фильм не зацепил: ни история сама по себе, ни то, как она была рассказана, ни игра актеров. А уж работа оператора как и вообще раздражала, как будто телеспектакль с советского ТВ посмотрел.

Молчание ангелов (Englemageren, 2023). Пока смотришь, то кажется, что бюджетно, но более-менее смотрибельно. Но вот финал ошарашивает таким маразмом, что становится жалко потраченного на просмотр времени.

Три мушкетера: Миледи (Les Trois Mousquetaires: Milady, 2023). Как по мне, так редкостная дрянь. Категорически не советую тратить на это "кино" свое время.

Сериалы

Медиатор (третьий сезон, 2024). Если первые два сезона понравились, то можно смело смотреть и третий. Мне зашло. На фоне других современных российских (да и не только) сериалов, так прям отдохновение.

Джентльмены (The Gentlemen, первый сезон, 2024). Пара первых серий и пара последних, как по мне, так отличные. В середине ну такое себе, можно было бы и подсократить хронометраж. Но в целом посмотрел с удовольствием.

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

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

Halo (второй сезон, 2024). Смотреть можно разве что тем, кому понравился первый сезон. Мне первый сезон не то, чтобы понравился, но просто захотелось посмотреть что-то из жанра "боевой фантастики". Местами норм, местами скучно. Ну и везде какая-то бессмыслица происходит, как по мне :)

Трасса смерти (первый сезон, 2017). Бодрый сериал, динамичный. Но в финале начался настоящий парад "роялей в кустах", а завершилось все настолько невнятно, что я так и не понял, кто же там был главным злодеем и вообще что все это было...

Не смог осилить

Костолом (Ruthless, 2023). Какая-то унылая и бюджетная попытка показать ещё одного "боевого пенсионера", в одиночку расправляющегося с кучей бандитов. Уж лучше пересмотреть первую часть "Заложницы".

Уцелевший (Warhorse One, 2023). С самого начала какой-то маразм + ну какой-то совсем уж ущербный уровень компьюетрной графики, так что я не смог выдержать и пятнадцати минут.

воскресенье, 31 марта 2024 г.

[prog.c++] Оказывается, для VC++ недостаточно ключа -std для того, чтобы получить актуальное значение __cplusplus

Если вы, как и я, думали, что указав компилятору ключ -std:c++20 вы автоматически получите корректное значение __cplusplus, то вы заблуждаетесь :(

Кроме ключа -std:c++20 нужно еще и ключ -Zc:__cplusplus указать. Цинк:

$ cat t.cpp
#include <iostream>

int main() {
        std::cout << __cplusplus << std::endl;
}

$ cl -EHsc -nologo -std:c++20 t.cpp
t.cpp

$ ./t.exe
199711

$ cl -EHsc -nologo -std:c++20 -Zc:__cplusplus t.cpp
t.cpp

$ ./t.exe
202002

Рецепт найден здесь.

Что тут остается сказать кроме "Вот уроды!"...

среда, 27 марта 2024 г.

[prog.thoughts] "Универсальный" против "специализированного" на примере из SObjectizer-а

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

В SObjectizer агенты подписываются на сообщения: если подписка есть, то сообщение до агента может дойти, а если подписки нет, то сообщение точно не дойдет. А раз есть подписки, то их нужно как-то хранить. Соответственно, возникает простой вопрос: "Как хранить?"

Фокус в том, что заранее неизвестно, сколько у агента будет подписок. Может быть две, может быть двадцать две, может быть две тысячи и двадцать две. А может и двести тысяч. Ну мало ли. Никто же не запрещает 😉

Как по мне, так это означает, что невыгодно в агенте хранить подписки одним и тем же способом вне зависимости от количества этих самых подписок. Ведь нет контейнеров, которые бы отлично работали бы при любом количестве элементов. Так, если у нас всего две подписки, то hash-таблица избыточна, как и бинарное дерево поиска (где каждый узел -- это отдельный объект в динамической памяти). А если у нас 100500 подписок, да они еще и активно создаются/уничтожаются, то непрерывный вектор здесь не вариант, т.к. вставки в середину (как и удаления из середины) дороги.

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

Появление же этого интерфейса не может не сказаться на производительности. Ведь у нас появляется дополнительная косвенность. Плюс у самих реализаций subscription_storage могут быть какие-то дополнительные накладные расходы...

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

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

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

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

ЗЫ. У SObjectizer-а давеча состоялся очередной релиз. Как раз была добавлена еще одна реализация subscription_storage. И таки дошли руки сделать описание этой штуки в Wiki-проекта.

среда, 20 марта 2024 г.

[prog.cmake] Попытался заглянуть в документацию по CMake. Пригорело. Нехило так пригорело :(

Я вот правда не понимаю, как этот кусок говна умудрился стать стандартом де-факто. И еще я не понимаю вот чего: нам, си-плюс-плюсникам, что, мало сложностей и несуразностей в самом C++? Мы так мало набили шишек на граблях C++, что радостно взялись скакать еще и по граблям CMake? Не, реально?

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

PS. Пожалуйста, не нужно спрашивать у меня чем заменить CMake. Уже почти 20 лет пользуюсь собственной системой сборки, написанной на Ruby. Когда-то даже прикладывал усилия чтобы продвинуть свой лисапед в массы, но не преуспел. На этом считаю свой долг по улучшению C++ной экосистемы полностью выплаченным.

вторник, 19 марта 2024 г.

[prog.c++.kill'em-all] Еще пример C++ного кода от которого у меня изрядно подгорает

Вот не нужно писать кроссплатформенный код вот так:

enum class status { not_started, started, shutting_down, stopped };

#if defined(PLATFORM_WINDOWS)
[[nodiscard]] const wchar_t * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return L"not_started";
    case status::started: return L"started";
    case status::shutting_down: return L"shutting_down";
    case status::stopped: return L"stopped";
  }
}
#else
[[nodiscard]] const char * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return "not_started";
    case status::started: return "started";
    case status::shutting_down: return "shutting_down";
    case status::stopped: return "stopped";
  }
}
#endif

вот не надо, пожалуйста.

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

Сделайте хотя бы так:

// Эта кухня должна жить в отдельном заголовочном файле.
#if defined(PLATFORM_WINDOWS)
  using platform_char_type = wchar_t;
  #define STRING_LITERAL(str) L##str
#else
  using platform_char_type = char;
  #define STRING_LITERAL(str) str
#endif

enum class status { not_started, started, shutting_down, stopped };

[[nodiscard]] const platform_char_type * to_str(status st) noexcept {
  switch(st) {
    case status::not_started: return STRING_LITERAL("not_started");
    case status::started: return STRING_LITERAL("started");
    case status::shutting_down: return STRING_LITERAL("shutting_down");
    case status::stopped: return STRING_LITERAL("stopped");
  }
}

четверг, 14 марта 2024 г.

[prog.flame] Самодокументирующися код против документированного, наглядно

Намедни в LinkedIn поиронизировал на счет "самодокументирующегося кода". В очередной раз 😎

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

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

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

Под катом оригинальный фрагмент с комментариями. Такими, какими они и были написаны при разработке. Но сперва этот же фрагмент, но вообще без комментариев. Именно так "самодокументирующися" код и выглядит, по моему (не)скромному опыту.

class DefaultThreadPoolScheduler final : public Scheduler
{
   ...

private:
   struct WorkerData
   {
      std::mutex _lock;
      std::condition_variable _wakeupCondition;

      Scheduler::TaskUniquePtr _taskToRun;

      bool _shutdownInitiated{ false };
   };

   using WorkerDataContainer =
      std::vector<std::reference_wrapper<WorkerData>>;

   std::latch _allWorkersStartedLatch;

   std::mutex _lock;

   TasksContainer _tasksQueue;

   ThreadPool _threadPool;

   WorkerDataContainer _availableWorkers;

   bool _shutdown{ false };
};

void DefaultThreadPoolScheduler::doWork() noexcept
{
   WorkerData thisWorkerData;

   {
      std::lock_guard schedulerLock{ _lock };
      _availableWorkers.push_back(std::ref(thisWorkerData));

      _allWorkersStartedLatch.count_down();
   }

   bool shutdownIntitiated{ false };
   while( !shutdownIntitiated )
   {
      std::unique_lock workerLock{ thisWorkerData._lock };

      if( TaskUniquePtr taskToRun = std::move(thisWorkerData._taskToRun); !taskToRun )
      {
         shutdownIntitiated = thisWorkerData._shutdownInitiated;
         if( !shutdownIntitiated )
         {
            thisWorkerData._wakeupCondition.wait(workerLock);

            shutdownIntitiated = thisWorkerData._shutdownInitiated;
         }
      }
      else
      {
         workerLock.unlock();
         taskToRun->run(Scheduler::RunCondition::Normal);
         completeTaskThenTryGetNext(std::move(taskToRun), thisWorkerData);
      }
   }
}

понедельник, 11 марта 2024 г.

[prog.c++] Первая попытка написать собственный концепт

В проекте для одного из клиентов уже используется C++20, так что есть возможность на практике прикоснуться к некоторым фичам двадцатого стандарта. Вроде ranges и concepts.

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

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

В общем, смысл в том, что есть интерфейс диспетчера задач Scheduler и есть интерфейс отдельной задачи Task:

class Scheduler
{
public:
   class Task
   {
   public:
      enum class Status { Normal, Deleted };

      virtual ~Task() = default;

      virtual void run(Status) = 0;
   };

   using TaskUniquePtr = std::unique_ptr<Task>;

   virtual void push(TaskUniquePtr task) = 0;
};

Существует несколько реализаций Scheduler-ов. В качестве же Task-ов планировалось использовать лямбда-функции, которые должны оборачиваться в реализацию интерфейса Scheduler::Task. Т.е. вместо того, чтобы делать как-то так:

class FirstTask final : public Scheduler::Task
{
  ... // Какие-то внутренности.
public:
  ... // Какой-то конструктор.

  void run(Status status) { ... /* Какие-то действия */ }
};
...
scheduler.push(std::make_unique<FirstTask>(...));

предполагалось делать как-то так:

make_task(scheduler,
  [...](Scheduler::Task::Status status) {... /* Какие-то действия */ });

И вспомогательный шаблон функции make_task должен был обернуть лямбду в некий класс-наследник Task. Что-то вроде:

template<typename H>
class LamdaAsTask final : public Scheduler::Task
{
   H _handler;

public:
   explicit LamdaAsTask(H handler) : _handler{std::move(handler)} {}

   void run(Status condition) override
   {
      _handler(condition);
   };
};

template<typename H>
void make_task(
   Scheduler & scheduler,
   H && handler)
{
   scheduler.push(std::make_unique<LamdaAsTask<H>>(std::forward<H>(handler)));
}

Но ситуация была чуть сложнее. Нужно было использовать разные фабрики для Task. Т.е. в make_task передавались не только лямбда и ссылка на Scheduler, но еще и некая фабрика. Именно фабрика брала лямбду и возвращала указатель на новый Task. Т.е. шаблон make_task выглядел так:

template<typename Factory, typename H>
void make_task(
   Scheduler & scheduler,
   Factory && factory,
   H && handler)
{
   scheduler.push(factory(std::forward<H>(handler)));
}

И место для улучшения кода я видел в том, чтобы ограничить параметры шаблона make_task концептами.

Вырисовывалось два концепта.

Первый концепт описывает лямбду-обработчик. Ну типа ее можно вызвать с нужным параметром и она возвращает void.

template<typename H>
concept Handler_C = requires(H h)
{
   { h(Scheduler::Task::Status::Normal) } -> std::same_as<void>;
};

Второй концепт описывает фабрику Task-ов:

template<typename F, typename H>
concept TaskFactory_C = requires(F f)
{
   requires Handler_C<H>;

   { f(H{}) } -> std::convertible_to<Scheduler::TaskUniquePtr>;
};

Т.е. этот концепт зависит от параметра H, который описывает лямбду. Эта лямбда должна удовлетворять требованиям концепта Handler_C. А фабрика сама по себе должна поддерживать вызов operator() с передачей туда лямбды H и возвратом чего-то приводимого к TaskUniquePtr.

Благодаря таким концептам make_task приняла вид:

template<Handler_C H, TaskFactory_C<H> Factory>
void make_task(
   Scheduler & scheduler,
   Factory && factory,
   H && handler)
{
   scheduler.push(factory(std::forward<H>(handler)));
}

В общем, я получил то, что хотел.

Под катом полный код автономного примера, который всю эту кухню демонстрирует.

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

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

четверг, 7 марта 2024 г.

[work.culture] Использование жестких, на грани грубости, оценок в работе

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

Ну т.е. использование фраз вроде "получилось откровенное говно" или "вы предлагаете полную херню" -- это было вполне себе обыденно. И не могу сказать чтобы это сильно людей напрягало. Да, временами это воспринималось персонально, но обычно люди быстро понимали, что здесь нет наезда на них конкретно (а кто не понимал, тот находил себе работу поспокойнее).

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

Так вот о глобализации. Она же сказывается. И, есть ощущение, что традиции толерантности проникают и к нам. И уже даже в русскоязычных коллективах начинают использовать эвфемизмы дабы никого не обидеть. Т.е. там, где в середине 90-х просто бы сказали "код херовый", сейчас уже что-то вроде "возможно, в нескольких местах к качеству есть вопросы".

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

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

Это по типу того, как мы в экстремальных условиях переходим на русский матерный. Ну реально же лучше работает 😉

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

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

Во-вторых, использование мата в общении. Не буду ханжой и не стану утверждать, что мат вообще не употребляю. Когда случается звиздец, сложно назвать происходящее как-то иначе. Но!

Мат (если без него не обойтись) должен использоваться только в исключительных случаях.

И ни в коем случае нельзя использовать мат в отношении подчиненных. Да, я понимаю, что временами очень тяжело удержаться от вопроса "ты что долбо*б?" или "а не ох*ел ли ты?", но нужно. Да и клиентов, в их отсутствие, так же лучше матами не обкладывать. И не только потому что везде есть уши 🙂

Т.е. если мат используется эпизодически и к месту, то это еще терпимо. Хотя лучше бы вообще без... Но жизнь есть жизнь.

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

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


В общем, к чему это я?

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

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

понедельник, 4 марта 2024 г.

[prog.flame] Пример серьезного, на мой взгляд, просчета в API библиотеки libconfig

Впервые довелось столкнуться с библиотекой libconfig. Довольно таки популярной, насколько я могу судить. Тем удивительнее было обнаружить там описанный ниже косяк.

Чтобы получить значение целочисленного параметра нужно использовать функцию config_setting_get_int:

int config_setting_get_int (const config_setting_t * setting)
long long config_setting_get_int64 (const config_setting_t * setting)

These functions return the value of the given setting. If the type of the setting does not match the type requested, a 0 value is returned.

Т.е. если мы пытаемся получить значение параметра, а нам возвращают 0, то этот ноль может означать:

  • что значение не получено, т.к. оно имеет другой тип. Например, вместо my_param=0 задано my_param="0" или my_param="zero";
  • что значение получено и это таки ноль. Просто ноль.

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

Аналогичная проблема есть и с семейством функций config_lookup_int/config_lookup_int64 и прочих вариантов lookup-чего-то-там. Эти функции возвращают CONFIG_FALSE и в случае, если параметр вообще не был найдет, и в случае, если был найден, но содержит не тот тип (например, строка вместо числа).

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

Т.е. получив 0 из config_setting_get_int или CONFIG_FALSE из config_lookup_int у меня нет возможности разобраться с тем есть ли ошибка и, если есть, какая она.

Хотя, как по мне, избежать этой проблемы можно было бы очень просто, если бы у config_setting_get_int был другой формат:

int config_setting_get_int(
  // Где искать значение.
  const config_setting_t * setting,
  // Куда помещать значение.
  int * value_receiver)

И возвращаемое значение означало бы признак успешности, вроде такого: 0 -- все OK, -1 -- значение имеет другой тип, -2 -- значение слишком большое, чтобы уместиться в int и т.д.

Очевидная, вроде бы, вещь. Но почему-то не сделанная... 🙁

суббота, 2 марта 2024 г.

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

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

Фильмы

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

Плохие парни (Bad Hombres, 2023). Откровенно бюджетный фильм, но за развитием событий в котором интересно следить. Могло бы получиться на удивление неплохо если бы откровенно унылым финалом авторы не спустили все свои предыдущие усилия в унитаз.

Территория зла (Land of Bad, 2024). Очень двойственные впечатления: с одной стороны картинка шикарная. Но с другой постоянно преследует ощущение "нам втирают какую-то дичь". По итогу ощущения негативные, можно пройти мимо этого фильма.

Сериалы

Месье Спейд (Monsieur Spade, первый сезон, 2024). Очень красиво снято. И Клайв Оуэн, как по мне, отлично воплотил образ частного сыщика из детективов Дешела Хэммита. Но в целом сериал оставил посредственные впечатления. Слишком много внезапных флешбэков, слишком много розовых соплей, слишком неспешное повествование на протяжении всего сериала, но при этом очень скомканные и кульминация с развякой (толком не понимаешь что же именно и из-за чего произошло).

Кошка (первый сезон, 2023). Ну такое себе. Мне гораздо интереснее было смотреть на виды Калининграда, в котором события развивались, чем за самими событиями. Так что не советую.

Джек Ричер (Reacher, второй сезон, 2023). Первые семь серий были вполне себе в духе первого сезона. Но в последней серии авторы устроили такую муть, что убили вообще все положительные впечатления. В итоге сильно не понравилось.

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

Настоящий детектив (True Detective, четвертный сезон, 2024). Это не детектив, а редкой нудятины муть про страдания "сильных и независимых женщин" (c), да еще и чрезмерно сдобренная мистикой. Категорически не рекомендую.

Фильм вне категории

Звук свободы (Sound of Freedom, 2023). История, рассказанная в фильме поражает и, конечно же, не может оставить равнодушным. Но вот сам фильм, именно как фильм, мне показался откровенно слабыми и не раскрывающим весь ужас данной темы. Поэтому как-то оценивать его в целом не берусь.

четверг, 29 февраля 2024 г.

[prog.c++.flame] Вот пример того, что мне сильно не нравится в C++

Попалась мне несколько дней назад старая статья на тему хитрых трюков в C++: "NON-CONSTANT CONSTANT-EXPRESSIONS IN C++". Да, я знаю, статья не новая, но вот такой я медленный газ... 🙁

В статье показывается некий трюк, который типа должен позволить сделать так:

int main () {
  constexpr int a = f ();
  constexpr int b = f ();

  static_assert (a != b, "fail");
}

Т.е. два вызова одной и той же constexpr-функции в compile-time должны дать разный результат.

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

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

Хочется привести такую аналогию: возможно вы замечали, что временами при вставке штепселя в электрическую розетку возникает искра (и при извлечении такое случается). Иногда искр меньше, иногда больше. Насколько я знаю, этот эффект физиками давно изучен и он лежит в основе электроискровой резки металлов. Ну да не суть. Суть в том, что вот кто-то обнаружил этот эффект и начал экспериментировать: штепсели с разным диаметром контактов, скорость вставки/извлечения, угол под которым все это выполняется и т.д. А потом написал статью, что типа если вы будете делать вот так, то у вас гарантированно будут искры и искры эти будут наиболее яркими. А потом кто-нибудь еще и попытается извлечь какую-нибудь "пользу" из такого "открытия"?

Нормальные люди прочитав о таком, скорее всего, просто покрутят пальцем у виска.

Тогда как в мире C++ вот такое вот "открытие" привлекает внимание и ведет к продолжению подобных поисков и экспериментов. И это считается нормальным. Более того, такие вещи обсуждаются. Возможно, кто-то даже решается применить что-то подобное в своей работе. А потом это вырастет в какую-то продвинутую технику программирования на C++, по типу случайно открытого в середине 1990-х метапрограммирования не шаблонах...

Мне это не нравится. Все-таки хочется, чтобы С++ развивался в сторону удобного, практичного и предсказуемого инструмента, где для решения задач используются специально спроектированные возможности языка программирования, а не это вот все 🙁

Но самый цимес обнаружился при попытке проверить представленное в статье решение 😅

Оказалось, что само решение (в коротком его варианте), работает только в VC++.

Под GCC работает только то длинное решение, которое типа должно обходить баг в clang-е. А вот короткое решение не работает.

Тогда как в clang-е не работает вообще ни короткое, ни длинное.

Ну и, спрашивается, нахера весь этот огород было городить, если он "работает" только на одном из компиляторов? Так и хочется вспомнить старое, но актуальное: "Не выёбывай умничай" 🤬

Павбывавбы.

понедельник, 12 февраля 2024 г.

[prog.multithreading.bugs] Повезло столкнуться с собственным багом в многопоточном коде

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

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

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

Особо доставили два момента:

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

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

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

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

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

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

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

По недосмотру в коде не оказалось повторного захвата мутекса после того, как текущая нить дождалась своего события и проснулась. Поэтому обновление контейнера было уже не thread-safe 🥴

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

В общем, целый ряд счастливых случайностей:

  • сперва я очень удачно сгенерировал "правильную" последовательность запросов которая привела к тому, что две рабочие нити проснулись в одно время;
  • затем повезло с тем, что при перезаписи std::map-а из разных потоков не образовался какой-то мусор из-за чего бы программа могла бы упасть с segmentation fault;
  • и все это случилось когда в программе еще оставались отладочные печати, благодаря которым на консоль сбрасывались дампы с информацией о текущих запросах;
  • ну и каким-то чудом в этих самых дампах я заметил то, что у ряда запросов статус оказался "в работе", а не "в ожидании".

Короче говоря, без везения в поиске багов в многопоточке не обойтись 😎

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

Сам я себя ни в коем случае специалистом по многопоточному программированию не считаю, мне тупо не хватает мозгов, чтобы моделировать все то многообразие сочетаний событий, которое может возникнуть в многопоточном коде. Я поэтому-то SObjectizer-ом и занимаюсь, чтобы свести работу с многопоточностью к минимуму. Поэтому в моем многопоточном коде баги были, есть и будут. Куда же без них 😉 Главное, чтобы они вовремя наружу вылазили, под присмотром 🤣


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

суббота, 10 февраля 2024 г.

[prog.multithreading] Нужна помощь в поиске названия для примитива синхронизации, похожего на std::latch

Мне тут потребовался примитив синхронизации, в чем-то похожий на добавленный в C++20 std::latch. Но с важным отличием: в `std::latch` нужно в конструкторе указывать значение счетчика. А в моем случае это количество заранее точно неизвестно.

Грубо говоря, сценарий использования `std::latch`: есть тред A, который ждет, пока N тредов B(i) сделают кусок работы. Тред A засыпает на `wait`, каждый тред B(i) рано или поздно вызывает `count_down` и когда это сделают все треды B(i), тред А проснется.

Все это отлично работает пока N известно заранее.

В моем же случае тред С создает сперва тред A, а затем начинает создавать треды B. И тред A точно не знает, сколько именно C создаст тредов B. Просто в какой-то момент треду A нужно будет дождаться пока запущенные треды B завершат свою работу. Для чего каждый тред B сперва инкрементирует счетчик, а затем декрементирует. Треду же А достаточно дождаться обнуления этого счетчика.

Сделанный для этих целей простой вариант "барьера" можно увидеть под катом.

Используется приблизительно следующим образом:

// Это все внутри треда C.
meeting_room_t completion_room;

std::thread thread_a{[&]() {
  ... // что-то делает.
  // Нужно дождаться пока треды B завершат свою работу.
  completion_room.wait_then_close();
  ... // еще что-то делает.
}};

// Создаем треды B.
std::vector<std::thread> threads_b;
while(some_condition()) {
  threads_b.push_back([&completion_room]() {
    completion_room.enter(); // Увеличили счетчик.
    ... // что-то делает.
    completion_room.leave(); // Уменьшили счетчик.
  });
  ... // какие-то еще действия.
}

// Осталось дождаться завершения работы.
for(auto & t : threads_b) t.join();
thread_a.join();

Возникла сложность с названием для такого примитива синхронизации.

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

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

Upd. Похоже, что такая штука назвается rundown: Run-Down Protection. Большое спасибо Константину за наводку.

Текущая реализация meeting_room_t:

вторник, 6 февраля 2024 г.

[prog.c++.kill'em-all] C++ный код, от которого у меня изрядно подгорает

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

error_code use_resource(resource_id res_id) {
  if(auto r = first_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    dispose(res_id);
    return r;
  }
  ...
  dispose(res_id);
  return error_code::ok;
}

Думаю, что несложно догадаться, что именно триггерит: это обилие вызовов dispose.

Я могу простить тот факт, что в use_resource передается голый дескриптор ресурса, а не какая-то RAII-обертка вокруг него.

Ну мало ли, бывает. Может эта функция вообще как extern "C" описана и предназначена для того, чтобы ее вызывали из Си-шного кода. Или же это часть древнего проекта и первоначально use_resource была написана еще в конце 1980-х, а сейчас ее просто дорабатывают не имея возможности поменять все 100500 мест в старой кодовой базе, где она вызывается именно вот так.

Но блин, почему нельзя сделать RAII обертку уже внутри use_resource?

Хотя бы подобным образом:

error_code use_resource(resource_id res_id) {
  struct resource_disposer {
    resource_id m_id;
    resource_disposer(resource_id id) : m_id(id) {}
    ~resource_disposer() { dispose(m_id); }
  } disposer(res_id);

  if(auto r = first_operation(res_id); r != error_code::ok) {
    return r;
  }
  if(auto r = second_operation(res_id); r != error_code::ok) {
    return r;
  }
  ...
  return error_code::ok;
}

Причем реализация такого `resource_disposer` -- это вообще C++98. Таким подходом можно пользоваться уже больше двадцати пяти(!!!) лет без оглядки на версию компилятора. В современном C++ можно было бы найти еще несколько способов достижения той же самой цели (хотя бы finally из GSL), более лаконичных.

На эту тему подобной "очистки" ресурсов я уже неоднократно писал. Вот, например, от 2015-го года (уже почти десять лет как!!!): раз и два. Но, как я смотрю, время идет, а криворуких программистов недоучек меньше не становится.

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

Вероятно, C++ программистов нужно начинать учить с идиомы RAII. А уже все остальное -- потом.

Ну а Си-программистов, по аналогии, нужно начинать учить с идиомы goto err (или goto cleanup). Даже не смотря на то, что goto -- это зло. Как и чистый Си, впрочем ;)


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

понедельник, 5 февраля 2024 г.

[prog.c++] Захотелось в C++ странного (на тему транзитивной константности)...

Недавно столкнулся с задачей, в которой было бы хорошо иметь транзитивную константность в C++.

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

Что такое транзитивная константность?

Представим себе, что у нас есть:

class Foo {
public:
  void foo(); // Это не-const метод.
  void foo2() const// А это уже const-метод.
};

class Bar {
  Foo * m_foo; // Это не-const указатель!
public:
  ...
  void bar2() const { // Это const-метод, в котором мы не можем присвоить m_foo новое значение.
    m_foo->foo(); // Но зато можем вызвать не-const метод для m_foo.
  }
};

Foo foo;
const Bar bar{&foo};
bar.bar2(); // Этот вызов может изменить foo.

Из-за того, что в C++ константность не транзитивна, то в const-объекте bar можно иметь не-const указатель на foo и в const-методе Bar::bar2 можно поменять объект foo.

Если бы константность была транзитивной, то в Bar::bar2 указатель Bar::m_foo автоматически бы стал константным и вызвать в Bar::bar2 не-const метод Foo::foo у нас уже не получилось бы.

Поскольку в С++ транзитивной константности нет, то я было попробовал сделать ее вручную. По типу чего-то такого:

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ...
  [[nodiscard]] T * get() { return m_ptr; } // Не-const.
  [[nodiscard]] const T * get() const { return m_ptr; } // Уже const.
};

Это позволяет получить транзитивную константность в простом случае:

class Bar {
  AutoConstPtr<Foo> m_foo; // Это уже не raw pointer.
public:
  ...
  void bar2() const {
    m_foo.get()->foo(); // А вот здесь будет ошибка компиляции!
  }
};

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

Но, к сожалению, это не сработало на практике. Например, из-за вот таких случаев:

void ProcessItems(const std::vector<AutoConstPtr<Foo>> & items) {
  for(auto p : items) {
    p.get()->foo(); // Упс!
  }
}

Фокус в том, что p -- это будет копия AutoConstPtr<Foo>. Не-const копия. Следовательно, для p будет вызываться не-const версия get. Следовательно, будет возвращаться не-const указатель на Foo. Следовательно, можно вызывать не-const методы Foo, т.е. модифицировать Foo. И это в ситуации, когда исходно у нас были как раз константные указатели на Foo (ведь у нас const-ссылка на вектор указателей).

Вот таким вот незамысловатым образом красивая идея накрылась медным тазом. Абыдна, да 🙁

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

Ведь сейчас мы пишем что-то вроде:

MyClass::MyClass(const MyClass & other) {...}

и понимаем, что это конструктор копирования. Но не понимаем, какой именно экземпляр MyClass при этом конструируется. Т.е.:

MyClass source{...};

MyClass copy1{source}; // Вызов конструктора копирования.
const MyClass copy2{source}; // Вызов того же самого конструктора копирования.

А что, если бы мы могли добавлять const и к конструктору?

template<typename T>
class AutoConstPtr {
  T * m_ptr;
public:
  ... // Здесь какие-то другие конструкторы.
  AutoConstPtr(const AutoConstPtr &) = delete// Нельзя построить не-const из const.
  AutoConstPtr(const AutoConstPtr & other) const // Тут все OK.
    : m_ptr{other.m_ptr}
  {}
  ...
};

Тогда бы не получилось бы скомпилировать конструкцию:

for(auto p : items) ...

потому что нельзя построить не-const объект AutoConstPtr из const-объекта.

А вот так бы получилось бы:

for(const auto p : items) ...

Вот такая вот странная фантазия.

Но это реально фантазия, т.к. даже если бы была возможность помечать конструкторы как const, то непонятно было бы что делать вот с такими ситуациями:

const auto std::vector<AutoConstPtr<Foo>> & source = ...;
std::vector<AutoConstPtr<Foo>> selected;
std::copy_if(source.begin(), source.end(),
    std::back_inserter(selected),
    [](const auto & item) { return ...; });

Так что, возможно, идея транзитивной константности в принципе не для C++.

четверг, 1 февраля 2024 г.

[life.cinema] Очередной кинообзор

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

Фильмы

День мертвых (2021). Отличный пример кино, в котором минимум персонажей, минимум событий, а все держится на разговорах, но при этом следить за происходящим интересно. Хотя, наверняка, зайдет этот фильм далеко не всем.

Общество снега (La sociedad de la nieve, 2023). Впечатляющая история, снято все красиво, подача материала необычная... Но вот чего-то мне сильно не хватило. Не шедевр, к сожалению. А очень жаль. Тем не менее, имеет смысл смотреть.

Семейный план (The Family Plan, 2023). На удивление неплохо. Вполне можно посмотреть когда хочется отключить мозги и развлечься.

Догмен (Dogman, 2023). Если смотреть на этот фильм как на фэнтезино-фантастическую картину, вроде "Джокера", то в рамках этого жанра еще ничего, вполне смотрибельно.

Каменщик (The Bricklayer, 2023). Ну такое себе, на троечку. Хотя есть там что-то от духа боевиков 1980-х годов.

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

Озеро диких гусей (Nan fang che zhan de ju hui, 2019). Сам фильм мне не зашел, но в чем-то это оказалась любопытная картина, т.к. очень уж сильно отличается от европейского, не говоря уже про американское, кино.

Мятежная Луна, часть 1: Дитя огня (Rebel Moon - Part One: A Child of Fire, 2023). Попытка Netflix-а заполучить свою фэнтезийно-космическую франшизу. Получилась редкая муть (что-то по типу Восхождение Юпитер). Только, в отличии от "Восхождения Юпитер" здесь, помимо всего прочего, меня еще и визуальная составляющая раздражала (как и в предыдущей большой работе Зака Снайдера).

Сериалы

Медленные лошади (Slow Horses, третий сезон, 2023). Бодренько и динамично. Мне понравилось.

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

Мертвый сезон (Hors Saison, первый сезон, 2022). Ну такое себе. В принципе, глянуть можно, но слишком часто придется восклицать "да что за фигня?!"

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

Не смог досмотреть

Легенда о самбо (2022). Смог осилить всего минут двадцать. Показалось, что это редкостное говно не смотря на местами красочную и качественную картинку.

пятница, 26 января 2024 г.

[prog.c++] Интересное чтиво про strict aliasing rule...

...лежит здесь: What is the Strict Aliasing Rule and Why do we care?

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

Из того, что стало откровением и открытием лично для меня (применительно к C++):

  • в C++ содержимое другого объекта можно просматривать путем каста к char, unsigned char или std::byte. Т.е., грубо говоря, вы всегда можете сделать reinterpret_cast<char *>(other_pointer). Но это не распространяется на signed char. И отдельная история с std::int8_t/uint8_t: эти типы могут быть простыми синонимами для char и unsigned char, а могут быть и отдельными, самостоятельными типами (в таком случае на них правило char/unsigned char/std::byte не распространяется);
  • оказывается, современные компиляторы могут понять, что делает std::memcpy и избавиться от реального вызова std::memcpy. Например, вот такой корректный способ получить float из int-а (при условии их одинаковых размеров):

    int src = ...;
    float dst;
    std::memcpy(&dst, &src, sizeof(src));

    в случае умного компилятора будет просто помещать в dst нужно значение без вызова memcpy;

  • не знал раньше про common initial sequence. А это, как выяснилось, может стать архиважной штукой при работе с union:

    struct A { char type; ... };
    struct B { char type; ... };
    struct C { char type; ... };
    union U { A a; B b; C c; };
    
    U u;
    u.a = ...;
    if(u.b.type == ...) // (1)
    {...}

    Обращение к u.b в точке (1) легально не смотря на то, что U::b -- это неактивный в данный момент элемент union-а.

понедельник, 22 января 2024 г.

[prog.c++] Оказывается, в современном C++ параметры шаблона с дефолтными значениями можно располагать в начале списка параметров шаблона...

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

#include <iostream>

struct no_size_limit {
    static bool is_size_valid(std::size_t /*size*/) {
        std::cout << "no_size_limit::ensure_valid_size" << std::endl;
        return true;
    }
};

template<typename Size_Limiter=no_size_limit, typename... Args>
void f(Args && ...args) {
    if(Size_Limiter::is_size_valid(sizeof...(args))) {
        std::cout << "processing of args" << std::endl;
    }
    else
        std::cout << "ignoring args" << std::endl;
}

template<std::size_t N>
struct at_least {
    static bool is_size_valid(std::size_t size) {
        std::cout << "at_least<" << N << ">::ensure_valid_size" << std::endl;
        return (N <= size);
    }
};

int main() {
    f(12345);
    f<at_least<3>>(12345);
    f<at_least<5>>(123);
}

Цынк

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

Приятно.


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

вторник, 16 января 2024 г.

[linux] Если вам потребовался ArchLinux в Docker с пакетом из AUR...

...то вот эти ссылки могут оказаться полезны. По крайней мере мне помогли.

Arch_User_Repository. Официальная информация о том, что такое AUR и как ставить пакеты из AUR. Имеет смысл просмотреть хотя бы по диагонали, чтобы понимать, что к чему и почему.

Testing our package build in the Docker world. В принципе, основная статья, в которой вроде бы все собрано воедино в нормальном, лаконичном и более менее понятном виде.

Testing an Arch Linux package in Gitlab CI. Статья не совсем про Docker, но мне она оказалась наиболее полезна, т.к. там расписывается что и зачем делается.

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

ЗЫ. Т.к. после экспериментов с Docker-ом остается куча всяких устаревших (и не очень) образов, то найти простые способы поудалять лишние Docker-овские images и containers можно здесь: How To Remove Docker Images, Containers, and Volumes. Например:

$ docker image prune
$ docker rmi $(docker images -a -q)
$ docker rm $(docker ps -a -f status=exited -q)
$ docker stop $(docker ps -a -q)
$ docker rm $(docker ps -a -q)

среда, 10 января 2024 г.

[prog.c++.wtf] Публичный член приватного вложенного типа?

Еще одно открытие для меня в языке C++, которым пользуюсь уже больше 30 лет:

class Outer {
    struct Inner {
        int m_a{};
        int m_b{};
    };
public:
    Inner m_i;
};

int main()
{
    Outer o;
    o.m_i.m_a = 3;
    o.m_i.m_b = 4;
}

Оказывается, так можно. Цынк.

Что меня выморозило в этом примере: мы же класс Inner сделали закрытым вложенным классом для Outer. Т.е. вроде как, по логике вещей, классом Inner могут пользоваться только сам Outer и его друзья.

Но ничего нам не запрещает объявить в Outer публичный член Outer::m_i приватного, вроде бы, типа Outer::Inner. И любой желающий может работать с таким объектом приватного типа Outer::Inner.

Впервые с таким столкнулся. Я, честно говоря, ожидал, что компилятор не должен позволить объявить публичный Outer::m_i. Но, в очередной раз, ошибся 🥴


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.

вторник, 9 января 2024 г.

[prog.c++] Оказывается, в современном C++ нельзя взять и сложить std::string с std::string_view...

На пятый год работы с C++17, в котором std::string_view появился, "Зоркий глаз" (т.е. я) заметил, что в C++ пока нет версии operator+ для случая std::string и std::string_view :(

Поэтому ни в C++17, ни в C++20, ни, подозреваю, в C++23, не получится написать так:

std::string f(std::string_view a, std::string_view b) {
  using namespace std::string_view_literals;
  return std::string{"Expected value: "} + a + ", actual value: "sv + b;
}

Но есть пропозал. И, может быть, нам повезет и в C++26 эта фича в языке таки появится. А может только в C++29...

Если честно, то я, мягко говоря, в шоке.


На правах саморекламы: изобретаю велосипеды для себя, могу изобретать и для вас.