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

вторник, 28 июля 2026 г.

[prog.thoughts] И еще одна мысль про влияние ИИ на программирование

Навеяно вот этой статьей: Английский вместо кода

Когда я приходил в профессию на программиста возлагались следующие задачи:

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

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

Никаких бизнес-аналитиков и тестировщиков в то время в наших палестинах еще не существовало как класса, появилось все это лет на 5-10 позже (хотя на Западе, как я слышал, к началу 1990-х все это уже было). Ну да не суть.

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

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

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

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

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

Как по мне, так это уже и не программирование вовсе.

понедельник, 27 июля 2026 г.

[prog.thoughts] Подумалось тут про ИИ и будущее программирования...

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

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

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

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

понедельник, 29 июня 2026 г.

[prog.thoughts] Начинаю думать, что бесстековые короутины в C++ следовало делать чуть иначе

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

Изначально я планировать только позволить пользователю описывать event-handler-ы, в которых можно было бы делать co_await. Но по мере продумывания способов реализации столкнулся с тем, что не имею на руках никаких механизмов запуска внешних по отношению к SO-5 короутин. А это нужно, чтобы проверить, что из event-handler-а можно дернуть, скажем, короутину из Asio, и когда она завершится, управление должным образом вернется event-handler-у.

И тогда появилась мысль сделать сперва возможность асинхронной работы с mchain-ами. Сейчас в SO-5 есть синхронные версии receive и select, а что, если предоставить их асинхронные версии? Тогда в event-handler-е можно было бы написать что-то вроде:

so_5::event_handler_task_t
some_agent::evt_some_handler( mhood_t<some_msg> cmd )
{
  auto result = co_await so_5::async_receive( so_5::from( test_chain ), ... );
  ...
}

Тогда отправляя (или не отправляя) сообщения в тестовый канал я бы мог тестировать поведение event-handler-ов.

Но стоило погрузиться в задачу создания асинхронных версий async_receive и async_select-а, как стало возникать подспудное подозрение, что с C++ными бесстековыми короутинами что-то не так. И я не говорю про их мудреность (это тема отдельного разговора). Было ощущение, что несмотря на заумность и гибкость C++ных короутин в них все-таки чего-то важного не хватает.

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

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

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

В Capy ее предлагают решать за счет введения специальной сущности -- Executor-а и за счет изменения интерфейса сущности Awaiter-а: метод await_suspend для Awaiter-а вместо одного аргумента получает два:

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env);

И во втором параметре передаются вещи, которые могут потребоваться короутине (и ее дочерним короутинам): executor, stop_token, allocator.

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

Это как раз то, чего мне не хватало для реализации асинхронных версий receive/select. Я уже сам пришел к мысли о том, что в асинхронный receive нужно передавать какой-то coro_scheduler, который будет отвечать за то, чтобы возобновить приостановленный receive/select именно там, где это разрешено. Например, если receive вызывается из event-handler-а, то это могло бы выглядеть так:

auto result = co_await so_5::receive(
  so_5::from( test_ch ).resume_by( this->so_coro_scheduler_for_this_agent() )...,
  ... );

А если mchain используется вне агентов, то может быть что-то вроде:

so_5::cpp_coro::this_thread_scheduler_t coro_scheduler;
coro_scheduler.sync_wait(
  [&coro_scheduler, &ch]() -> so_5::cpp_coro::async_receive_task_t {
    co_await so_5::receive(
      so_5::from( ch ).resume_by( coro_scheduler )..., ...);
  } );

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

Но, повторюсь, и подход Capy, и мои попытки придумать некий условный coro_scheduler для SObjectizer-а -- это попытки прикрутить решение сбоку посредством синей изоленты.

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

Например, чтобы обращение к co_await можно было параметризовать. Скажем, передавать executor/scheduler непосредственно в co_await:

co_await(executor) some_task();

И чтобы этот executor передавался бы параметром в await_resume. Может быть в виде ссылки на некоторый специальный объект environment, как это делается в Capy.

Есть у меня подозрение, что с такой явной передачей executor-ов в co_await, код с короутинами и писать, и читать было бы гораздо проще.

PS. Раз уж заговорил про попытку добавить поддержку короутин в SO-5, то слегка обозначу текущий статус. Пока до чего-то работающего еще далеко. Пытаюсь двигаться от простого к более сложному. Сперва хочу попробовать сделать асинхронную версию receive/select. Чтобы с ее помощью перейти к поддержке event-handler-ов в виде короутин. Затем, возможно, попробую погрузиться еще глубже, чтобы разрешить короутины в роли обработчиков входящих сообщений в receive/select (если это вообще возможно). Работы много, ресурсов мало, движется все очень медленно. Поэтому я сам в течении ближайших пару месяцев никаких значимых результатов не жду.

пятница, 8 мая 2026 г.

[prog.c++] Хочется странного, но теперь уже от std::map

В std::map начиная с C++17 есть отличный метод try_emplace. Он особенно хорош, когда конструирование mapped_type очень дешевое. Например, когда в качестве ключа у нас int, а в качестве значения -- структура с несколькими int-ами внутри. Тогда получается эффективно: попробовали вставить, если ключа в map еще нет, то из параметров сконструировали mapped_type и добавили в map новое значение. А если же в map ключ уже есть, то передача в try_emplace нескольких int-ов как параметров для конструктора mapped_type -- это копейки, на которые во многих случаях можно просто не обращать внимания.

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

Если мы напишем что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::make_unique<heavy_object>(...));

то это явная пессимизация -- ведь нам придется создавать heavy_object при каждом обращении к try_emplace, даже если ключ в map уже есть.

Я вижу два стандартных пути выхода из этой ситуации.

Во-первых, мы можем пойти классическим способом, через find с последующим emplace, если find завершился неудачно:

auto it = my_map.find(key);
if(it == my_map.end()) {
  it = my_map.emplace(key, std::make_unique<heavy_object>(...)).first;
}
// Теперь it указывает на объект внутри std::map.

Но здесь плохо то, что для вставки объекта поиск по std::map нужно будет делать дважды.

Во-вторых, в try_emplace можно передать пустой unique_ptr, а сам объект создать уже после вставки. Т.е. что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  it->second = std::make_unique<heavy_object>(...);
}

Но здесь плохо то, что нам нужно позаботиться об exception safety, ведь вызов make_unique может бросать исключение. И самое худшее, что можно сделать, это написать что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  try {
    it->second = std::make_unique<heavy_object>(...);
  }
  catch(...) {
    // Удаляем только что вставленный пустой указатель.
    my_map.erase(it);
    throw;
  }
}

Гораздо лучше было бы иметь что-то вроде scope(failure) из D. Что-то вроде:

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  auto guard = at_failure(
    // Эта лямбда будет вызвана если выход из скоупа произойдет
    // из-за исключения.
    [&it, &my_map]() {
      my_map.erase(it);
    });
  it->second = std::make_unique<heavy_object>(...);
}

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

auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{});
if(inserted) {
  // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно
  // это исправить.
  // Защищаемся от исключений.
  do_with_rollback_on_exception(
    // Что хотим сделать.
    [&it, ...]() {
      it->second = std::make_unique<heavy_object>(...);
    },
    // Эта лямбда будет вызвана если первая лямбда бросит исключение.
    [&it, &my_map]() {
      my_map.erase(it);
    });
}

Но все эти приседания были бы не нужны, если бы был вариант try_emplace, который бы принимал не аргументы для конструктора mapped_type, а фабрику, которая может породить экземпляр mapped_type для вставки:

auto [it, inserted] = my_map.try_emplace_with_factory(key,
  // Эта лямбда будет вызвана, если объекта в map нет.
  [...]() {
    return std::make_unique<heavy_object>(...);
  });

К сожалению, такого варианта try_emplace_with_factory в std::map нет.

PS. Вышесказанное относится и к std::unordered_map.


Upd. В обсужении на LinkedIn посоветовали обходной маневр вида:

#include <map>
#include <string>

class simple_data {
    std::string _data;
public:
    simple_data(const char * s) : _data{ s }
    {}
};

class data_holder {
    std::string _data;
public:
    template<typename... Args>
    data_holder(Args && ...args) : _data{ std::forward<Args>(args)... }
    {}
};

template<typename F>
struct deferred {
    F _f;

    template<typename T>
    operator T() { return _f(); }
};

int main()
{
    std::map<int, simple_data> m1;
    m1.try_emplace(0, deferred{ []{ return simple_data{ "Hello, world" }; } });

    std::map<int, data_holder> m2;
//    m2.try_emplace(0, deferred{ []{ return std::string{ "Hello, world" }; } });
    m2.try_emplace(0"Hello, world");
}

Но не для всех случаев он будет работать. В частности для data_holder-а не сработает, т.к. у data_holder-а есть шаблонный конструктор (цынк).

понедельник, 23 июня 2025 г.

[prog.c++] Есть ли теперь смысл при разработке C++ библиотек придерживаться не самых свежих стандартов?

В заголовок поста вынесен вопрос, который занимает меня в последние пару-тройку лет. A последний год так точно.

Поясню в чем дело.

В C++ на протяжении десятков лет есть специфическая картина: существует официальный С++ на "бумаге", т.е. описанный в соответствующем стандарте, будь то С++98, С++11 или C++23, и есть реальный C++, доступный конкретному разработчику в конкретном компиляторе. И, как правило, в имеющихся в нашем распоряжении компиляторах далеко не все фичи из самого последнего официального стандарта реализованы.

Эта картина особенно важна при разработке кросс-платформенных библиотек. Если ты хочешь, чтобы твою библиотеку использовали разные люди в разных проектах и на разных платформах, то ты вынужден занижать версию стандарта. Например, вместо C++23 (который вроде как уже два года нам доступен) делать библиотеку на C++17 или даже C++14.

При разработке софта для конечного пользователя ситуация, зачастую, гораздо проще: очень часто софт пишется под конкретную платформу и вполне можно заложиться на конкретный компилятор. Но с разработкой библиотек не так. В проекте X библиотека может работать уже в режиме C++23, тогда как в проекте Y -- все еще в C++17.

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

Но после C++20 смиряться все сложнее.

C++11 стал совсем другим языком в сравнении с C++98. Два следующих стандарта, C++14 и С++17, инкрементально улучшали C++, но не могу сказать, что они переводили язык на какой-то принципиально другой уровень (даже не смотря на такие крутые фичи C++17 как structured binding и if constexpr). А вот начиная с C++20 все принципиально поменялось:

  • C++20 добавил концепты и operator spaceship (про модули промолчу ибо не пробовал);
  • С++23 добавил deducing this. Не смотря на то, что (на мой взгляд) сделали это через одно место (и можно было бы по-другому), но таки важную реальную проблему эта фича решает;
  • С++26 добавляет compile-time рефексию и, я очень надеюсь, контракты.

Все это вместе, не побоюсь этого слова, делает из C++ совсем другой язык в такой же степени, как C++11 после C++98. Если не в большей.

И вот глядя на все это великолепие в свежих стандартах С++ я не могу найти для самого себя ответ на вопрос: а зачем на при разработке своих библиотек оставаться на C++17/14/11?

Вот реально.

Ладно бы нам за наши OpenSource проекты платили. Но этого нет. И RESTinio, и SObjectizer приносят нам деньги не напрямую, а приводя к нам клиентов через репутацию. Зачастую новые фичи в тот же SObjectizer добавляются "just for fun" -- просто что-то выглядит интересным или представляет из себя вызов, поэтому делаешь это ради получения удовольствия.

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

Поэтому чем дальше, тем больше мыслей о том, чтобы в следующем году перевести SObjectizer сразу на C++26. На счет RESTinio ситуация посложнее, но если представиться возможность плотно поработать над RESTinio, то и там тоже можно будет сразу же брать С++26.

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

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

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

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

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

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

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

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

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

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

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

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

f(-1);

или так:

f(1.2);

или так:

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

и даже так:

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

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

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

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

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

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

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

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

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

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

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


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

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

вместо:

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

пятница, 4 апреля 2025 г.

[prog.c++] Нормально на C++ программируют лишь параноики?

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

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

Проблема, однако, в том, что C++ таким высокоуровневым языков не является.

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

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

Или, что часто происходит в моей практике, люди забывают (или не знают?) про exception safety. Как результат, если вылет исключения не приводит к немедленной катастрофе, то уж утечку ресурсов или нарушение инвариантов множества объектов вызывает точно. Далеко не все программисты, к сожалению, привыкли к повсеместному RAII. А обеспечение strong exception safety обходится не бесплатно в плане сроков написания кода (особенно если это дело еще и покрывать тестами).

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

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


На самом деле проблема не в "современном C++", а именно в C++ безотносительно его версии.

Как по мне, так старый и недобрый "Си с классами" являлся еще большим рассадником багов и внимания к мельчайшим деталям при программировании на C++98 требовалось даже больше, чем сейчас.

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

Тогда как код на современном C++, с какими-нибудь ranges и coroutines может производить обманчивое впечатление того, что C++ таки вошел в одну когорту с Java, C#, Scala и, может быть, даже Python с JavaScript. Что есть опасное заблуждение.

Да, на C++ можно писать высокоуровневый код, особенно при наличии хороших прикладных библиотек. Только вот возможность нечаянно отстрелить себе ногу никуда не делась. Что принципиально отличает C++, даже в самых его свежих стандартах, от Java/C#/Python/JavaScript.

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

PS. Нахожусь при мнении, что при программировании на чистом Си параноить приходится гораздо меньше. Потому, что там нет исключений. И нет неявно вызываемых конструкторов (например, когда из строкового литерала внезапно возникает экземпляр std::string). Поэтому в мире чистого Си при использовании идиомы goto cleanup чувствуешь себя намного спокойнее.

PPS. Нормально программировать на C++ вполне себе возможно, сколько бы не пытались лаять хейтеры и ниасиляторы.

четверг, 3 октября 2024 г.

[prog.c++] В std::vector не хватает вот какой штуки...

Интересно, много ли C++ программистов задумывается о том, а как растет std::vector, когда мы в него добавляем элементы через push_back и не имеем возможности сделать предварительно reserve?

А ведь рост размера std::vector может существенным образом повлиять на расход памяти.

Предположим, что реализация std::vector в вашей стандартной библиотеке использует коэффициент 1.5 для увеличения размера вектора. Т.е. вы делаете push_back в полный вектор и опа! Ваш вектор увеличился в полтора раза. Была емкость на 1000 элементов, а стала на 1500. А использоваться оттуда будет 1001.

А если реализация стандартной библиотеки использует коэффициент 2, то дела еще хуже.

Сильно негативно это начинает проявляться на векторах размером в миллион-два-три и т.д. Если у вас в программе таких векторов не один и не два, то вы с удивлением для себя сможете обнаружить, что в этих векторах где-то 1/5 или 1/4, а то и 1/3 объема не занято. Что в сумме может дать десятки мегабайт впустую потраченной памяти.

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

А то, что делается вручную, легко забыть или же сделать неправильно 😣

Поэтому мне лично не хватает возможности задать для std::vector какую-то собственную политику роста. Что-то типа:

struct my_growth_policy {
  [[nodiscard]] std::size_t operator()(std::size_t current_capacity) {
    return ... // здесь какие-то вычисления.
  }
};

using my_vector = std::vector<my_data, std::allocator<my_data>, my_growth_policy>;

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

Но, боюсь, в стандартном std::vector такого не будет никогда.

пятница, 30 августа 2024 г.

[job.flame] Откуда такой акцент на софт-скиллз в последние годы?

Позволю себе немного пофлеймить в теме, в которой не разбираюсь (с другой стороны, а разве можно как-то иначе? 😉)

Когда я начинал работать, ни о каких софт-скиллах и речи не было. Это, по моим ощущениям, вообще тема текущего десятилетия. А реальный акцент на этих самых софт-скиллах делается в последние 3-4 года.

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

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

А давеча посмотрел случайно два интервью с нейробиологом Вячеславом Дубыниным:

ЛЮДИ ТУПЕЮТ! Причина - не алкоголь или никотин. Вячеслав Дубынин.

Вячеслав Дубынин про поиски себя, выгорание, аффирмации. Как работа влияет на мозг?

И у меня появилась другая версия. Практически медицинская 🙂

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

Мы с раннего детства проводили много времени в разного рода коллективах: будь то детский сад, школа, кружки, спортивные секции, пионерские и спортивные лагеря, не говоря уже про двор. Сплошной реал, никакой виртуальности. В котором навыки этого самого общения, как и навыки существования в рамках коллектива, прививались прямыми и незатейливыми способами. Уже к 4-5-м классам школы практически каждый на своей шкуре усваивал, что трепать языком нужно поменьше, за слова принято отвечать, да и получить в лыч за эти самые слова можно очень даже легко и непринужденно. А можно и не за слова, а просто потому, что оказался не в то время и не в том месте. Всякое бывало... В общем, прямые и незатейливые способы иногда оказывались настолько жесткими и суровыми, что не все вписывались в эти условия, но это уже тема другого разговора.

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

Что наводит меня на следующую мысль: а что если у молодого поколения, которое вместо игры в "войнюшку" всем двором, чатилось в соцсетях, просто оказались недостаточно развиты те самые нейросети для общения и социализации, которые естественным образом прокачивались у нас, старпёров, в нашем далеком уже детстве?

Т.е. тупо часть мозга, которая у поколения 1960-х, 1970-х и 1980-х была хорошо развита вот просто потому, что другого выхода-то и не было, у поколения 2000-х уже просто на недоразвитом (в биологическом смысле) уровне. И, грубо говоря, нынешние 20-летние не могут в человеческое общение на таком же уровне, на котором умели мы.

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

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


Ну ладно, эта медицинская версия слишком уж попахивает стариковским ворчанием по поводу никчемной молодежи. Так что вот еще одна.

Ранее (лет 25 назад) в найме роль HR была не столь существенна, как сейчас. А вот нонче, судя по постам в LinkedIn, практически только через HR.

Могут ли HR адекватно оценить хард-скиллы соискателей? Сильно сомневаюсь. Может и есть единицы таких продвинутых, но именно что единицы.

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

Но эта версия, честно говоря, слишком банальна. А потому и не интересна 😎

вторник, 4 июня 2024 г.

[prog.c++] Полагаю, что не нужно использовать bool-параметры в функциях и методах. Ну вот вообще не нужно.

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

do_something(target, true, false, true, logger);

Глядя на такой код более-менее понятно с первого взгляда что означают target и logger. А вот какой смысл несут true, false и еще раз true -- без заглядывания в документацию (если вам повезло и таковая есть вообще) или в реализацию do_something не скажешь.

Рекомендация, кстати говоря, здравая. Но, как по мне, слишком специализированная. Я воспринял ее в более общем виде: нужно избегать идущих подряд аргументов одного типа. Не важно bool это, int, double или string. Когда видишь что-то вроде:

prepare_data(data, 0, 0, 42, 122);

то без плотного знакомства с prepare_data все равно не обойтись.

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

prepare_data(data, default_x, defaut_y, default_w, default_h);

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

prepare_data(data, default_x, defaut_w, default_y, default_h);

Так что если у вас есть возможность пользоваться каким-то из вариантов strong typedef, то лучше им таки пользоваться. Отличная защита от опечаток. Да и в чужой код легче погружаться, когда там в прототипах функций вместо int-ов используются x_coord, y_coord, width и height.

Но вернемся к аргументам типа bool.

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

И вовсе не по тем причинам, которые когда-то описал Мартин Фаулер. А тупо потому, что читать чужой код, в котором раскиданы совершенно неинформативные true и false -- это то еще удовольствие:

auto it = data_cache.get(item_key, true);
...
it = data_cache.validate(it, false);

Когда видишь такой код впервые, то сразу спотыкаешься на true и false, пытаясь выяснить что они означают вообще.

Совсем другое дело, когда используются enum classes и именованные константы:

auto it = data_cache.get(item_key, cache_item_creation::create_if_missed);
...
it = data_cache.validate(it, cache_validation::dont_update_timestamp);

Так что с годами пришел к выводу, что в современном C++ c enum-class bool-аргументы не должны использоваться вообще.


PS. По поводу упомянутого выше Мартина Фаулера. Ну не убеждают меня его доводы. Хотя, если вы не будете использовать bool-параметры под влиянием аргументов Фаулера, то я возражать не буду ;)

среда, 22 мая 2024 г.

[prog.c++.fantasy] Даешь по два стандарта в год!

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

С C++11 эта история повторилась. Но вот с C++14 и C++17 все было уже гораздо, гораздо лучше. И я уже, честно сказать, привык к хорошему: вышел новый стандарт и в свежих версиях компиляторов "большой тройки" он уже практически весь поддержан.

Однако, C++20 вернул нас в старые и недобрые времена. Уже середина 2024-го года, а полностью и нормально C++20 вроде как нигде и не реализован.

Что в этом плохого лично для меня?

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

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

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

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

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

Не, реально.

Вот есть N предложений в работе. Какая-то часть из уже готова к включению в стандарт и просто ждет, пока же придет время фиксации этого стандарта. Так зачем ждать год-полтора-два, если уже есть готовые предложения? Давайте фиксировать по два стандарта в год. Например, весной -- C++24.1, осенью -- C++24.2. На следующий год весной будет C++25.1, а затем, осенью 2025-го, C++25.2 и т.д.

Как по мне, так хуже не станет.

Большие фичи, вроде модулей из C++20, могут (и будут) реализовываться годами. В 2024-ом году для меня нет разницы не реализованы ли модули из C++20 или же из C++23.2. Все равно же не реализованы.

Зато какая-нибудь мелкая фича, типа std::start_lifetime_as могла бы стать доступной для разработчиков в гипотетическом C++22.1, а не в С++23. Или вот в C++26 обещают добавить возможность обращаться к элементам parameter pack прямо по индексу. Так зачем ждать C++26, если можно включить ее в C++24.2?

Возможно, процесс стандартизации ISO сам по себе неторопливый и не может в принципе обеспечить принятие двух стандартов в год. Но лично у меня есть большие сомнения в том, что сейчас для C++ эта самая стандартизация ISO все еще важна.

Да, комитет по развитию C++ нужен. Спецификации языка, выпускаемые этим комитетом, нужны.

Но вот нужен ли на этих самых спецификациях штамп "ISO"? Сильно сомневаюсь. Возможно, в 1990-е развитие в рамках ISO имело смысл. Сейчас же его сложно разглядеть. А посему можно и задаться вопросом: а стоит ли сейчас цепляться за существующую модель стандартизации C++ или же выгоднее ее пересмотреть?

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

пятница, 27 октября 2023 г.

[prog.flame] Наглядная иллюстрация на тему "ушел рисовать каракули на бумаге"...

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

Дабы не повторяться, процитирую себя самого:

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

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

К сожалению, мой собеседник меня не понял (так уж показалось) и в итоге выдал вот такой пассаж:

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

А вчера я столкнулся с ситуацией, которая отлично иллюстрирует момент с "рисованием ничего не значащих каракулей на бумаге". Что и подтолкнуло к написанию этого поста.

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

Суть в том, что в свое время в RESTinio был добавлен механизм объединения обработчиков запросов в цепочки. Это что-то a la middleware из Express.JS (но именно что a la, т.е. по мотивам, но не один-в-один).

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

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

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

Три года назад ресурсов на решение этого вопроса не хватило.

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

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

Да, тот самый туман в голове с обрывками из отрывков разрозненных мыслей. Ну и каракули на бумаге, куда же без этого.

Есть ли у меня уверенность в том, что задача будет решена за неделю (а больше времени может и не быть)?

Нет, конечно.

Хочу ли я сделать первое, что придет в голову просто ради того, чтобы поставить галочку в списке фич RESTinio?

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

И что же получается?

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

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

Но, если результат вовсе не гарантирован, то что же толкает на поиск решения?

Во-первых, "есть такое слово: надо!" 😂 Поскольку продукт жив, то он должен пополняться новыми фичами. Пусть даже какие-то из них (пока) непонятно как сделать.

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

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

Особенно, почему-то доставил вот этот фрагмент из раздела про easy_parser:

// A parser for grammar:
//
// communicator = "port=" ("default" | port_params)
// port_params = '(' NUMBER ':' NUMBER ',' NUMBER ')'
//
struct port_params {
   unsigned short port_index_;
   unsigned int in_speed_;
   unsigned int out_speed_;
};

auto parser = epr::produce<port_params>(
   epr::exact("port="),
   epr::alternatives(
      epr::exact("default")
         >> epr::just(port_params{10u4096u4096u})
         >> epr::as_result(),
      epr::produce<port_params>(
         epr::symbol('('),
         epr::non_negative_decimal_number_p<unsigned short>()
            >> &port_params::port_index_,
         epr::symbol(':'),
         epr::non_negative_decimal_number_p<unsigned int>()
            >> &port_params::in_speed_,
         epr::non_negative_decimal_number_p<unsigned int>()
            >> &port_params::out_speed_,
         epr::symbol(')')
      ) >> epr::as_result()
   )
);

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

Кстати говоря, это стандартный C++14. Я знаю, что многим C++ категорически не нравится и многие убеждены, что C++ принципиально не подходит под написание eDSL. Но мне нравится то, что получилось.


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

четверг, 17 августа 2023 г.

[prog.c++.sobjectizer] Есть идея о том, как пользователь может реализовать собственные очереди сообщений для агентов

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

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

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

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

Свою текущую идею на этот счет я описал в разделе Discussions на GitHub: https://github.com/Stiffstream/sobjectizer/discussions/64

Если кому-то интересно дальнейшее развитие SObjectizer-а, то милости прошу ознакомиться. Там на плохом английском, да. Но при необходимости изложу все тоже самое на русском.

Главный вопрос для меня сейчас -- это интересно ли кому-нибудь данное направление?

Если кому-то интересно, то появляется смысл копать дальше.

Ну а если нет, то подожду удобного случая. Может со временем что-то еще лучше в голову придет.

вторник, 8 августа 2023 г.

[prog.c++] Тяжко это, не иметь готовых таймеров под рукой...

Это, фактически, продолжение вот этого поста. Т.е. опять маленькие дифирамбы SObjectizer-у 😉

Привыкаешь к тому, что в SObjectizer-е таймеры из коробки и даже не задумываешься несколько это бывает нужно. А вот когда нет SObjectizer-а, а требуется выполнять какое-то действие раз в секунду или хочется проверить результат какой-то операции через 250ms... И грусть-пичаль 😭

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

суббота, 29 июля 2023 г.

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

Зафиксирую в склерозник этот "а ля план действий". На случай, если придется искать работу C++ разработчиком ;)

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

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

Отдельно, в качестве лакмусовой бумажки, можно будет, пожалуй, задать такой вопрос: "А используете ли вы в C++ном коде приведения типов в стиле Си? Если да, то чем это объясняется и на основании каких критериев делается выбор в пользу такого приведения?"

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


Прекрасно понимаю, что в крупные компании с формализованными процессами и стандартизированными процедурами отбора (типа "Тинькова" или "Яндекса") я никогда не попаду с таким поведением. Но так сомневаюсь, что в таких организациях смогу продуктивно работать. Так что ну не судьба, да и ладно.

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

пятница, 14 июля 2023 г.

[prog.c++] Тяпничное, излишне эмоциональное, о наболевшем...

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

  • приложения для конечного пользователя, где нужно было бы искать сторонние готовые библиотеки, и затем комбинировать их чтобы решить прикладную задачу, за 300k RUB, или
  • библиотеки, закрывающей какую-то предметную область (условно: MQTT, UPnP, TURN и т.д., и т.п.) за 150k RUB

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

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

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

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

И да, озвученное выше можно считать публичной офертой 😉


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

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

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

Фраза эта врезалась мне в память и чем больше мне приходится разбираться с чужим кодом, тем чаще ее вспоминаю и все чаще задаюсь вопросом: "Если везде отличные C++ специалисты, то почему я постоянно сталкиваюсь с говнокодом?"

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


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

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

Во-первых, есть большой соблазн спросить: "А вы мой код на GitHub-е смотрели? Ну и как впечатления? Все еще есть необходимость спрашивать меня про виртуальный деструктор? Чего не хватило, что не понравилось?"

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

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

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

Но, блин, почему-то постоянно в глаза бросаются вещи, вроде вот таких:

class Manager {
  using WorkerMap = std::map<std::string, WorkerSharedPtr>;
  WorkerMap workers_;
  ...
  void addWorker(const std::string & id, WorkerSharedPtr worker) {
    // An old worker with the same ID has to be removed.
    auto old_worker_it = workers_.find(id);
    if(old_worker_it != workers_.end())
      workers_.erase(old_worker_it);
    ...
  }
  ...
};

Ведь можно было обойтись всего одной строчкой:

// An old worker with the same ID has to be removed.
workers_.erase(id);

Как бы и особого криминала нет, и вызов std::map::erase(const Key &) наверняка раскрывается под капотом в эти самые три строчки. Но блин, зачем на ровном месте объем кода раздувать? 🙁

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

четверг, 25 мая 2023 г.

[prog] Ошибочность ощущения что стал меньше ошибаться :)))

Когда я начал программировать, а было это уже давным-давно, то главным впечатлением было "как же много мы, люди, ошибаемся". Про древнее высказывание "Errare humanum est" уже тогда был наслышан, но не подозревал, насколько это верно :)

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

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

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

Сразу же приходит понимание, что это самое ощущение есть не что иное, как очередная ошибка.

Errare humanum est, короче говоря.

Так и живем ;)

пятница, 12 мая 2023 г.

[prog.c++] Хочется странного: особое отношение C++ного компилятора к структурам, объявленным как extern "C"

Я тут давеча в текущем проекте обновлялся с FFMPEG 4.4 на 5.1 и столкнулся с необходимостью использовать новый FFMPEG-шный тип AVChannelLayout.

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

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

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

Побитового копирования.

Что недопустимо для структур с владеющими указателями внутри.

И вот чтобы не наступать на грабли непреднамеренного случайного копирования одного экземпляра AVChannelLayout в другой мне захотелось странного: если C++ный компилятор видит структуру, которая объявлена как extern "C", то пусть он не генерирует для них конструктор и оператор копирования по умолчанию, а объявляет их как delete.

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

PS. Вообще, любопытно было столкнуться с типом вроде AVChannelLayout в C++ном коде. Если найду силы, то попытаюсь описать свои приключения/впечатления в отдельном посте. Но не обещаю, к сожалению. Будем посмотреть. Upd: вот и продолжение.