Показаны сообщения с ярлыком Подсмотренное. Показать все сообщения
Показаны сообщения с ярлыком Подсмотренное. Показать все сообщения

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

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

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

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

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

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

g++ -D MY_LIB_DEFAULT_N=10240 ...

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

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

namespace my_lib {

constexpr tmp_array_size = MY_LIB_DEFAULT_N;

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

/* namespace my_lib */

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

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

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

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

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

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

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

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

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

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

namespace my_lib {

...

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

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

/* namespace my_lib */

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

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

namespace my_lib {

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

/* namespace my_lib */

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


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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

[life] Пара-тройка вредных советов о том, как создать дискомфорт вашим соседям по больничной палате

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

Заранее приношу свои извинения за использование обсценной лексики, но здесь как в анекдоте про прачечную, никак не обойтись... :(

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

[prog.c++] Интересная постановка вопроса: если люди испытывают сложности с многопоточностью, то не забанить ли многопоточность совсем?

Этот вопрос всплыл на днях на r/cpp. Позволю себе процитировать значимую часть поста с reddit-а без перевода:

Hi all,

Had an interesting discussion today and would appreciate some advice.

Multithreaded code can be especially tricky for medium to junior level developers to write correctly. When a mistake is made, it can result in difficult to find bugs.

If the application you are working on needs to be very reliable (but isn't safety critical), what would you recommend for medium/low experience c++ teams?

Assume that the application will need to do 4 things at once and can't use state machines or coroutines. The various "threads" need to regularly exchange less than 10 KB of data a second.

Do you ban threads?

A few approaches come to mind.

#1 train the team to know the dangers and let them use threads with best practices. More experienced (and paranoid/diligent) developers carefully audit.

Any suggestions for books/resources for this team?

#2 and/or use a tool or technique to detect concurrency issues at compile time or during test? Thread sanitizer? cppcheck? ???

#3 ban threads and force concurrency to be implemented with multiple processes. 4 processes each with 1 thread. The processes will communicate with some form of IPC.

Т.е. смысл в том, что для разработчиков уровня middle/junior написание мультипоточного кода зачастую оказывается слишком сложным. И если приложение должно быть надежным, то возникает вопрос: как же быть? Может быть проще вообще запретить многопоточность в пользу многопроцессности? А если не запрещать, то что? Учить людей? Использовать какие-то инструменты для тестирования и анализа корректности кода?

Признаться, комментарии на reddit-е к этому посту я не читал, только просмотрел мельком и не увидел того, чего хотел 🙁

Поэтому выражу эмоции в этом блог-посте.

Хватить себя обманывать -- писать низкоуровневый многопоточный код сложно не только middle/junior-ам, но и senior-ам. Не устану повторять, что многопоточность на голых нитях, mutex-ах, condition_variable и, ниприведихоспади, atomic-ах -- это пот, боль и кровь.

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

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

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

Если нужно, значит наш путь лежит в многопоточность. Если не нужно, то выдыхаем и не паримся (до поры до времени).

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

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

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

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


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

  • обеспечение надежности работы в условиях ненадежности используемых программных компонент. Например, мы вынуждены полагаться на стороннюю библиотеку, которая время от времени падает и роняет весь процесс, в котором ее используют. Библиотека не наша, мы не можем "довести ее до ума", можем только минимизировать причиняемый ею ущерб;
  • обеспечение надежности работы в условиях ненадежности внешнего оборудования и/или внешних сервисов. Например, к компьютеру подключено устройство, которое может зависнуть. И единственный способ вернуть его к жизни -- это убить процесс, который общался с устройством, переинициалировать устройство и начать работать с ним заново. Аналогично и со сторонними сервисами, с которыми мы можем общаться через HTTP(S) или еще какую-то форму IPC. Бывает, что такой сервис набирает от нас N запросов и перестает подавать признаки жизни до тех пор, пока все эти N запросов не будут принудительно прекращены;
  • возможность жесткого прерывания длительных операций. Например, какой-то математический расчет, который может занимать часы и который не так-то просто прервать "изнутри". Но вот если вынести этот расчет в отдельный процесс, то этот процесс легко "прибить" в случае необходимости;
  • простота и удобство реконфигурации "на лету". Иногда работающий в режиме 24/7 сервис нужно переконфигурировать без его останова, но из-за внутренней кухни и/или особенностей использованных в нем библиотек, сделать такую переконфигурацию затруднительно.

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

  • в Unix-ах нужно избегать возникновения зомби-процессов;
  • после принудительно убитого дочернего процесса могут оставаться различные следы жизнедеятельности (в виде .tmp-файлов, которые не были вовремя удалены), которые нужно подчищать;
  • если взаимодействие идет через shared-memory, то нужно как-то определить а доверяем ли мы текущему содержимому блока разделяемой памяти или же внезапно умерший дочерний процесс оставил там какой-то мусор...

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

Но аргументы "за" такой переход точно не должны быть из категории "однопоточное программирование проще многопоточного".


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

четверг, 12 декабря 2024 г.

[prog.flame] Иногда просто поражаешься трудолюбию программистов

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

void data_holder::validate_data()
{
  std::cout << "*** Data validation: 1/15 (checking) ***" << std::endl;
  check();
  std::cout << "*** Data validation: 2/15 (sorting) ***" << std::endl;
  sorting();
  std::cout << "*** Data validation: 3/15 (deduplication) ***" << std::endl;
  deduplicate();
  std::cout << "*** Data validation: 4/15 (normalization) ***" << std::endl;
  normalize();
  ... // И так еще несколько строк пока не будет сделан шаг 15 из 15.
}

Вот честно, удивлен такому трудолюбию, т.к. меня бы быстро задолбало выписывать однотипные строки печати в std::cout с инкрементом значений в них. Особенно с учетом того, что количество этих шагов увеличивается по мере развития проекта :)

Так что я бы чуть ли не сразу написал бы что-то вроде:

void data_holder::validate_data()
{
  constexpr int total_steps = 15;
  auto inform = [step = int{1}](const char * name) mutable {
    std::cout << "*** Data validation: " << step << "/" << total_steps
      << " (" << name << ") ***" << std::endl;
    ++step;
  };

  inform("checking");
  check();
  inform("sorting");
  sorting();
  inform("deduplication");
  deduplicate();
  inform("normalization");
  normalize();
  ... // И так еще несколько строк пока не будет сделан шаг 15 из 15.
}

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

Ну и да, исходный пример заставляет вспомнить народную мудрость "простота хуже воровства".

вторник, 15 октября 2024 г.

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

Давайте посмотрим на структуру данных B:

struct A {};

struct B : public A {
    A _first;
};

Какой у нее размер? С учетом того, что даже у пустой структуры размер будет, как минимум, 1 байт. И что в C++ есть empty base class optimization.

То, что B наследуется от пустого A благодаря empty base class optimization не увеличивает размер B. Т.е. размер B будет определяться размером поля B::_first, а это один байт. Следовательно, sizeof(B) должен быть равен единице.

На самом деле нет.

Дело в адресах для объекта B и для его поля B::_first:

B b;
A * p1 = &b; // Это легально т.к. B есть A благодаря наследованию.
A * p2 = &b._first;

assert(p1 != p2);

Объект типа B является объектом типа A из-за наследования. Соответственно, адрес объекта типа B будет и адресом объекта типа A.

Внутри типа B есть еще один объект типа A. И у него так же есть свой адрес.

При этом объект B и его поле B::_first -- это два разных объекта типа A.

А в C++ у двух разных объектов одного типа не может быть одинаковых адресов.

Поэтому в показанном примере компилятор не может применить для типа B оптимизацию пустой базы, что и добавляет специальное выравнивание для поля B::_first, чтобы эти адреса оказались разными. Тем самым увеличивая размер B.

Убедиться в этом можно на wandbox - цынк

Информация об этой особенности C++ найдена здесь.

PS. Какой практический смысл у этой информации? Вероятно, никакого. Но это лишняя иллюстрация того, сколько же разных нюансов приходится учитывать при развитии C++.

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

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

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

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

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

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

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

template <class T>
struct PositionWrapper {};

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

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

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

четверг, 1 августа 2024 г.

[prog.c++.wtf] Еще немного про самый странный паттерн в коде

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

Вроде бы понял, в чем смысл. С обычными return-ами часто пишут так:

int some_func(some_arg & arg) {
  if(!is_valid(arg))
    return -1;

  if(!approriate_state(arg))
    return -1;

  if(!some_another_condition(arg))
    return -1;

  ... // Ну а здесь уже какие-то действия.
}

Похоже, этот подход пытаются переложить на тело цикла, только вместо преждевременных return-ов используют continue:

for(auto & item : collection) {
  if(!is_valid(item))
    continue;

  if(!appropriate_state(arg))
    continue;

  if(!some_another_condition(arg))
    continue;

  ... // Ну а здесь уже какие-то действия.
}

Понять-то я понял, а вот принять не получается 🙁

Во-первых, дело в том, что в реальном коде все это выглядит не так опрятно и понятно, как в моих псевдопримерах. Между if-ами, зачастую, еще какие-то действия выполняются, а сами if-ы могут быть и вложенными, и кроме continue еще встречаются и break, и даже return...

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

Во-вторых, недавно довелось несколько циклов преобразовать в лямбды, которые передаются во что-то типа `std::ranges::for_each`, т.е. было:

for(const auto & item : detect_collection_for_processing()) {
  ... // Тут обработка с continue/break-ами.
}

а стало:

for_all_ready_to_processing_items(token, [&](const auto & item) {
  ... // Тут обработка, но уже без continue/break.
});

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

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

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


Не хочу создать впечатление, что в моем коде только правило "единственного return-а" и ничего больше.

Это далеко не так.

И break-и использую, и несколько return-ов из функций. В том числе, бывает, сочетаю в теле цикла и break, и return.

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

А вот continue в моем коде, действительно, найти тяжело. Практически не использую. Чего и вам желаю 😎

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

[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 не всегда такая уж удобная замена.

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

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

понедельник, 14 августа 2023 г.

[prog.c++] Вот как раз для таких вот задач SObjectizer и бывает полезен

В качестве наглядного примера: свежий вопрос на LOR-е под названием "Приоритетная очередь".

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

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

понедельник, 24 июля 2023 г.

[work.pr] Прикольно: знакомство с SObjectizer как дополнительный плюс

Не скрою, приятно увидеть такое в чужой вакансии:

Что называется, пустячок, а приятно. А может и не пустячок, а проявление старой мудрости "вода камень точит"

вторник, 2 мая 2023 г.

[management] Наглядная иллюстрация для начинающих разработчиков, мечтающих вырасти в настоящих менеджеров

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

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

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

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

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

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

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

А вот на RSDN-е давеча нашлось хорошее описание типичной ситуации, с которой приходится сталкиваться менеджеру программистов:

На моём уровне — собираю статистику по команде. Кто сколько items закрыл, средний размер item, как быстро закрывается в среднем каждым из членов команды и т.д и т.п. ... Поднимается вопрос, что имярек делает всё в N раз медленнее других, требует много внимания, снижает среднюю продуктивность команды, не прогрессирует последние M месяцев. Идёшь к владельцу продукта, показываешь статистику, даёшь конкретные примеры того, как он тебя отвлекает от твоих очень важных дел на всякую мелочь, предлагаешь перевести его куда-то в другую команду, где не требуется самостоятельно искать решения, если есть куда переводить и просишь разрешения на найм другого. При этом подумай о том, есть ли способ исправить ситуацию перед тем, как это делать. Потому, что владелец продукта может задать вопрос, а можно ли это как-то исправить, ибо замена работника — удовольствие дорогое. Ну, или как вариант, обнаружь, что имярек ничем от других по производительности не отличается и это просто субъективное ощущение. Если субъективное и только у тебя (посоветуйся с лидами и теми разрабами, с которыми у тебя полностью доверительные отношения — т.е. теми, кто тебе доверяет настолько, чтобы обсуждать деликатные вопросы открыто), то забей на эти ощущения и пусть работает. Если у всех — то это уже мораль команды и к владельцу идёшь уже с именно с акцентом на это.

У нас было два показательных случая с onshore FTE.

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

Второй случай — человек вообще не коммитит нифига, ссылаясь на занятость на внедрении нашего продукта у клиента. На внедрении мне сказали, что и там он нихрена не делает. Сбор статистики, разговор с владельцем продукта, performance plan, перевод в другие команды. Расстанутся ли с ним по итогу или он возьмётся за ум и станет звездой в другой команде — это лично мне не так уж важно.

Т.е. скучный учет и контроль.

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

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

[prog.c++] Подсмотрел интересный трюк в докладе Ивана Чукича

Доклад "the expected outcome" с Meeting C++ 2022. Где-то на 10-й минуте докладчик демонстрирует простой шаблон класса для работы с кодами ошибок из API на чистом Си.

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

Вкратце этот простой шаблон выглядит так:

template<typename T, typename ValidationPolicy>
class Result {
private:
   T value_;

public:
   bool has_value() const;
   T value() const;
   T error() const;
};

Т.е., мы храним значение, возвращенное какой-то C-шной функций (например, функцией open из POSIX), и посредством ValidationPolicy можем проверить, указывает ли это значение на ошибку или же на нормальный результат.

Для разных типов представления ошибок можно реализовать свои ValidationPolicy. Например, NonZeroAsError для случая, когда ноль означает успешное завершение, а ненулевое значение -- код ошибки. Или MinusOneAsError для случая, когда значение -1 указывает на ошибку, а остальные значения -- это успешный результат. И т.д.

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

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

вторник, 24 сентября 2019 г.

[prog] Еще один наглядный пример зачем нужен SObjectizer и подобные фреймворки

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

есть таск приготовить пирог, он «верхний». в его реализации есть вызовы тасков: сходи в магазин за ингридиентами пирога, подготовь ингридиенты, испеки. вот эти три явно только последовательно могут быть выполнены.

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

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

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

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

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

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

ИМХО, полезность инструментов вроде SObjectizer, CAF, cpp-taskflow, Boost.Fiber и им подобных в том, что вы можете тупо взять и быстро набросать черновое решение вашей задачи, чтобы посмотреть что в принципе получается. Получается ли вообще. Или вообще не получается. Если более-менее получается, то что у вас с производительностью и другими показателями.

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

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

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

Делать свой велосипед можно только если вы четко, на 146%, убедились в том, что существующие чужие разработки вас совершенно не устраивают. И только если у вас есть ресурсы на это занятие. Причем не только на первоначальную реализацию, но и на последующую поддержку.

Так что, если вы не Yandex, не Kaspersky Lab, не Mail.ru и, уж тем более, не Google/Facebook/Amazon/Aliexpress, то не делайте свои велосипеды. Попробуйте сперва что-нибудь готовое. Благо, есть что попробовать.


Если у вас есть задача, но недостаточно опыта, чтобы понять, как к ней подступиться и что использовать для ее решения, то возьмите SObjectizer спросите у нас ;)

пятница, 20 сентября 2019 г.

[prog.facepalm] Никогда такого не было и вот опять: Ошибки аллокации достаточно бесполезно ловить в пользовательской программе.

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

Ошибки аллокации достаточно бесполезно ловить в пользовательской программе.

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

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

В-третьих, если вы пишете прикладную программу, близкую к системной в плане управления ресурсами (веб-сервер, сервер БД), то у вас и так весьма особенные нужды в выделении памяти. Часто используются пулы, маппинг файлов/анонимной памяти прямо от системы. Куча в первую очередь интересна "совсем прикладным" программам, которым нехватка ресурсов неинтересна.

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

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

Так что особого энтузиазма по поводу светлого будущего софтостроения я не питаю :(

пятница, 16 августа 2019 г.

[prog.flame] Ну не могу не утащить великолепную цитату в склерозник. Про RAII в виде создания процессов

Ну прекрасно же! Просто прекрасно. Ни добавить, ни убавить.

Просто Си не следует рассматривать отдельно от Unix. RAII там есть, просто имеет форму создания процессов. И техника эта богаче ООП по своим выразительным возможностям.

Взято отсюда. Это человек решил вступить в спор о том, чего не хватает в чистом С, в частности об отсутствии в C классов и RAII.

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

суббота, 4 мая 2019 г.

[work.flame] Увидел прекрасный список требований к соискателям. Не могу не прокомментировать

Вот этот пост в FB. Для тех, кому лень ходить в FB (или кого там нет, а такие счастливые люди, надеюсь, еще есть), процитирую его здесь полностью:

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

1. Способность разбираться (вот надо проанализировать кучу логов - человек найдет чем, даже если этого никогда не делал, разучит grep или научится виртуозно владеть Notepad++)
2. Отсутствие "нас этому не учили" позиции. И умение учиться. В современном мире, особенно когда ты делаешь что-то новое, никого никогда ничему не учили. Надо самому быть готовым учиться. И это нормально, когда рекламщик для своей работы изучает pandas, а программист - UX построения отчетов.
3. Личный тайм-менеджмент. Умение управлять своим временем, ресурсами, результатами. Чтобы без этих "ой, я увлекся" или "ой, я неправильно расставил приоритеты".
4. Грамотная и структурированная письменная речь. Это прямо must, мы живем в мире текстовых коммуникаций. Неспособность внятно выражать свои мысли в письменной форме - бич современного общества.
5. Понимание слова "результат". Это когда вы не считаете результатом "я кодил" или "я делал" или "я занимался". Когда вы считаете своим результатом - конечный результат, а не "свою маленькую делянку".
6. Амбиции. Я не понимаю и не люблю людей, которым все равно, чем они занимаются, лишь бы платили. Имхо это какой-то начальный уровень развития. Не понимаю и не люблю людей, которых не интересуют достижения команды, в которой они работают. У нас ежемесячное подведение итогов по компании - самое популярное мероприятие у сотрудников.
7. Здравый смысл. Не знаю как измерять. Но это общее понимание того, нафига ты что-то делаешь. Это частота задавания себе вопросов "а зачем?", "а как добиться того же самого, но попроще". Это когда ты не оправдываешь идиотизм ситуации или баги тем, что "так исторически сложилось" или "так было написано в ТЗ".
8. Способность "отвечать за базар". Грубая фраза, но очень важная. Это когда ты можешь человеку доверять. Потому что если он сказал "я разберусь", то ты уверен, что он не забудет и либо разберется, либо придет и скажет "ну слушай, там вот такая история, нужна помощь". И если он сказал "будет к вечеру", то к вечеру у тебя либо будет результат, либо до вечера ты узнаешь, что его не будет. Но уж точно тебе не придется писать на следующий день "ну что там?".
UPD 9. Отсутствие стеснения задавать вопросы. Когда у человека чувство собственного достоинства связано с тем, как он выглядит в глазах других, а не с реальными результатами - они стесняются задавать вопросы.

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

Ну и еще не могу не удержаться и не упомянуть, что у автора этого списка предшествующая запись в FB имеет вот такое содержание:

ОЧЕНЬ ищем iOS разработчика и QA инженера. Прям измучились в поиске людей с инженерной жилкой.

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

понедельник, 29 апреля 2019 г.

[prog.c++.flame] Посмотрел тут свежий доклад Саттера про более дешевые исключения для C++

Вот этот доклад с ACCU 2019:

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

среда, 26 декабря 2018 г.

[prog.humour] Открыл статью о Rust, увидел пример кода, закрыл статью о Rust-е ;)

Иногда мне кажется, что у наиболее активных растоманов какой-то стокгольмский синдром в терминальной стадии :) Вот, например, свежая статья о Rust-а на Хабре: "Так ли страшен Rust, как его малюют", автор которой, будучи C#-разработчиком, активно защищает Rust в различных хабрасрачах.

Так вот, автор сперва приводит пример кода на Rust-е:

let mut guess = String::new();

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

let guess: u32 = guess.trim().parse()
    .expect("Please type a number!");

println!("You guessed: {}", guess);

А потом говорит, что можно сделать существенно проще и приводит вот такой аналог:

let mut guess = String::new();
io::stdin().read_line(&mut guess)?;
let guess: u32 = guess.trim().parse()?;
println!("You guessed: {}", guess);

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

При том, что оба примера -- это попытка записать вот такой двустрочник на C#:

var number = int.Parse(Console.ReadLine());
Console.WriteLine($"You guessed: {number}");

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


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