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