суббота, 13 сентября 2025 г.

[prog.c++] Наткнулся на любопытное с std::ranges::views::iota

Вчера мое утро началось с того, что под Linux-ом компилятор GCC 13 отказался компилировать код вроде вот такого (это не реальный код, а минимизированая версия на которой проблема воспроизводится):

templatetypename T, typename Range >
[[nodiscard]]
T
make_from( const Range & r )
{
   return T{ r.begin(), r.end() };
}

int main()
{
   std::string src{ "Hello, World" };
   auto v = make_from< values_container >(
         std::ranges::views::iota( 0u, src.size()) );

   std::cout << v.front() << " - " << v.back() << std::endl;
}

Изначально код был написан под другую платформу с более свежим С++ным компилятором, поэтому там make_from был реализован иначе, проблем с компиляцией не было. А вот под уже довольно древним по современным меркам GCC возникли проблемы: компилятор не видел у values_container конструктора, который бы получал два итератора.

Конкретно в моем случае в качестве values_container был folly::fbvector, но это не суть. Вполне мог бы быть и std::vector, проблема была бы точно такой же.

Ошибка несколько обескуражила, т.к. я точно знал, что у values_container есть конструктор, принимающий итераторы first и last.

Но с этим-то удалось разобраться достаточно быстро: first и last должны быть одного типа. А в моем случае объект-рэндж, возвращенный вызовом iota, имел разные типы для begin() и для end(). Соответственно, раз типы итераторов разные, то и компилятор не может найти подходящий конструктор у values_container.

Ну OK, раз типы у итераторов разные, то можно к этой ситуации приспособить реализацию make_from. Например, написать ее так:

понедельник, 1 сентября 2025 г.

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

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

Фильмы

F1 (2025). Мне понравилось. Не удивлюсь, если окажется, что это лучший фильм из просмотренных в 2025-ом.

Последний выстрел (Dead Shot, 2023). Простенько, но атмосферно. Нормально рассказанная история, за которой интересно следить, но не шедевр.

Эники-Беники (Eenie Meanie, 2025). Странное кино: начиналось как дурацкая молодежная комедия, закончилась же серьезной драмой с кучей трупов.

Долина теней (Valle de sombras, 2023). Это не "криминал", как заявлено в описании, а драма. Красиво снятая, но скучноватая.

Мир Юрского периода: Возрождение (Jurassic World: Rebirth, 2025). Скучно и предсказуемо, ничего не цепляет. Местами очень красиво снято и нарисовано, местами очень заметно, что съемки велись на фоне зеленого экрана. Но в целом сойдет, чтобы скоротать семьей вечер перед экраном.

Ограбление (The Pickup, 2025). Очень и очень посредственно. Можно глянуть только если больше ну вот совсем ничего нет.

Сериалы

Чужие деньги (первый сезон, 2025). Местами возникало ощущение "нам втирают какую-то дичь". Местами было вполне нормально. Развязка понравилась. Буду ждать второй сезон.

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

Бабочка (Butterfly, первый сезон, 2025). Тупенько, но бодренько. Пожалуй, главное достоинство -- это короткий сериал.

В глуши (Untamed, первый сезон, 2025). Затянуто. Есть ощущение, что все события можно было бы уместить если не в две серии, так в три точно.

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

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

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


Специализированный mutex, который в сложившейся терминологии C++ной библиотеки можно было бы назвать как recursive_shared_timed_mutex.

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

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

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


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

В моем представлении версионированные структуры -- это такие, в которых каждый пользователь видит свою независимую версию этой самой структуры.

Чтобы было понятнее, давайте вообразим себе std::map<int, std::string>, с которым несколько нитей работают одновременно. Первая нить читает значения по ключам 0, 1, 2, 3, 4, 5, вторая нить читает значения по ключам 4, 5, 6, 7, 8, а третья нить изменяет значение для ключа 0, удаляет значение для ключа 5 и добавляет значения с ключами 9, 10 и 11.

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

Такой механизм работы означает, что третья нить не может просто так сохранить свои изменения. Требуется специальная операция commit, после успешного выполнения которой версия от нити №3 сможет заменить собой основную версию map.

При этом нити №1 и №2 все равно не будут видеть новую версию, поскольку они работают с map через "старую" ссылку. Чтобы перейти на актуальное содержимое map им нужно обновить эту ссылку у себя, т.е. явным образом обратиться к map с просьбой дать ссылку на последнюю актуальную версию.

Может оказаться так, что кроме нити №3 модификации в map сейчас вносит и нить №4. И свой commit четвертая нить может выполнить еще до того, как третья нить завершит свои модификации. В этом случае при commit-е нить №3 обнаружит, что актуальный map уже совсем другой.

Это все сильно напоминает работу с ветками в VCS: ты отпочковался от master-а (или trunk-а), сделал изменения в своей ветке, хочешь влить в master, а master-то уже ушел вперед. И нужно делать слияние (merging). Иногда оно происходит автоматически и безболезненно, а иногда приходится разбираться с конфликтами.

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

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

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

четверг, 21 августа 2025 г.

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

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

Эта привычка помогала и после перехода на Си, а затем и на C++.

Почему-то мне казалось, что в те старые времена и в Си, и в C++ была культура использования typedef-ов для создания псевдонимов типов. Вероятно из-за того, что платформ было много, платформы были разные. А псевдонимы, сделанные через typedef, помогали справляться с их различием. Достаточно вспомнить приключения при переходе от 16 бит к 32 битам и обратно.

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

void do_something(
  std::vector<std::pair<int, std::shared_ptr<my_obj>>> & first,
  std::map<std::string, std::tuple<int, int, std::string>> & second);

А еще хуже, на мой взгляд, когда один и тот же фундаментальный тип (вроде int-а или unsigned long-а) используется для совершенно разных целей:

struct some_item {
  int _x;
  int _y;
  int _width;
  int _height;
  int _usage_percentage;
  int _reusing_mode;
  int _logging_level;
  ...
};

Когда со временем меняешь int на что-нибудь другое, то обязательно выясняется что где-то как-то параметры перепутали -- выдали _width за _height или _usage_percentage вместо _reusing_mode.

Конечно, от многих подобных проблем в C++ можно было бы избавиться, если бы в C++ из коробки была поддержка strong typedef. Но даже в ее отсутствии обычные псевдонимы типов, сделанные через using, сильно повышают читабельность кода и упрощают его сопровождение (или портирование на другую платформу).

Вроде бы уже не раз говорил в разных местах о достоинствах using-ов:

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

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

Но если мне это очевидно, то почему это не очевидно другим C++разработчикам?

Возможно потому, что им не привили правильные привычки во время обучения.

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

PS. Очень надеюсь, что рано или поздно, но C++ обзаведется штатным, доступным прямо в стандартной библиотеке, механизмом strong typedef. И если в вашем коде using используется должным образом, то переход к применение strong typedef-а окажется более простым и менее болезненным.

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

[prog] Мои подходы по именованию веток в git-е

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

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

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

Так вот, ближе к теме разговора: когда в Git-репозитории одновременно существует несколько десятков веток, надо бы как-то в этом многообразии разбираться. А значит нужна какая-то система именования веток, чтобы сделав git branch можно было понять где, что и как.

С большим удивлением для себя регулярно обнаруживаю, что часто в именованиях веток нет вообще никакой системы. Обычно встречаются ветки с именами "bug-fix" или "memory-usage". Иногда к именам веток добавляются префиксы типа "bug/" (тогда получаются имена вида "bug/login-crach" или "bug/memory-leak") или "feature/" (тогда получаются имена вида "feature/open-auth-login" или "feature/sparse-table"), или "experimental/" (тогда получаются имена вида "experimental/simd-json"). Хотя, как по мне, эти префиксы не особо-то и полезны.

Но самое плохое, что лично мне такие имена ни о чем не говорят 🙁

Ситуация получше когда для всех задач проекта есть тикеты в условной JIRA и номер тикета. Особенно когда к номеру тикета добавляется еще что-то поясняющее, вроде "issue-1416-attempt-2". Но и это не идеал.

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


Первая схема используется при работе над нашими открытыми проектами, вроде SObjectizer и RESTinio.

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

  • имя мажорной версии продукта. Например, сейчас развивается версия 5.8 для SObjectizer. Поэтому первая составляющая будет иметь вид "5.8-dev", т.е. основная dev-ветка для версии 5.8;
  • имя конкретной версии продукта. Например, сейчас в работе версия 5.8.5, которая пока до релиза не дошла. Поэтому вторая составляющая будет иметь вид "5.8.5";
  • краткое именование того, над чем идет работа. Например, "reduce-state-size". Возможно, с указанием того, какая это итерация, например, "reduce-state-size-v2".

В результате получаются ветки с именами "5.8-dev-5.8.5-reduce-state-size-v2".

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

5.8-dev
5.8-dev-5.8.5
5.8-dev-5.8.5-cmake-files-update
5.8-dev-5.8.5-issue-96
5.8-dev-5.8.5-state-time-limit-v2
5.8-dev-5.8.5-issue-93
...

Можно обратить внимание, что имена образуют иерархичность и указывают что и куда затем должно вливаться. Так, изменения из "5.8-dev-5.8.5-cmake-files-update" будут влиты в "5.8-dev-5.8.5", а затем изменения из "5.8-dev-5.8.5" будут влиты в "5.8-dev".

Как-то у меня не прижился прямой слэш в качестве разделителя, поэтому мне удобнее работать с именами вида "5.8-dev-5.8.5-cmake-files-update", а не с "5.8-dev/5.8.5/cmake-files-update". Вкусовщина, конечно же, но у меня вот так.


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

  • никнейм основного автора кода в ветке, в моем случае это "eao197";
  • дата начала работы над веткой в формате YYYYMMDD;
  • краткое именование того, над чем идет работа. Например, "reduce-state-size". Возможно, с указанием того, какая это итерация, например, "reduce-state-size-v2".

Что в результате дает имена веток вида: "eao197-20250814-versioned-data-v2-tmp". Если для задачи есть номер тикета, то он включается в третью часть. Типа "eao197-20250818-issue-77889-attempt-1".

Такое именование лично для меня оказывается полезным тем, что:

  • сразу видно кто отвечает за ветку. В частности, мне легко понято что было сделано (и/или забыто) лично мной;
  • степень "свежести" изменений. Когда прямо в имени ветки видишь, что она относится к декабрю 2023-го года, то это сразу наводит на мысль, что код в ней довольно древний. Можно даже не делать git log 😉

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


Интересно, а какие схемы именования веток в git-е вы считаете удобными и, может быть, даже практикуете?

четверг, 14 августа 2025 г.

[prog.c++] dynamic_cast и непубличные базовые классы

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

Вот пример:

#include <iostream>

class iface {
public:
    virtual ~iface() = default;

    virtual void f() = 0;
};

class basic_impl {
public:
    virtual ~basic_impl() = default;

    void impl_f() {}
};

class first_derived final : public iface, protected basic_impl
{
public:
    void f() override { impl_f(); }
};

class second_derived final : public iface, public basic_impl
{
public:
    void f() override { impl_f(); }
};

void try_dynamic_cast(iface * base)
{
    std::cout << "trying to cast " << typeid(*base).name() << std::flush;
    if(auto * p = dynamic_cast<basic_impl *>(base); p)
        std::cout << "... OK" << std::endl;
    else
        std::cout << "... FAILURE" << std::endl;
}

int main() {
    first_derived f;
    second_derived s;

    try_dynamic_cast(&f);
    try_dynamic_cast(&s);
}

Обращаю внимание, что для first_derived класс basic_impl -- это protected-база.

В результате его запуска получим:

trying to cast 13first_derived... FAILURE
trying to cast 14second_derived... OK

Цынк.

PS. Вот так: век живи, век учись. А помрешь все равно дураком 🙂

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

[prog.c++] Оператор <=> и оператор ==

Надеюсь, многие знают, что в C++20 появилась замечательная штука operator<=>. Стоит его определить, как компилятор на его основе выводит другие операторы сравнения, вроде operator< или operator>=.

Причем, что особенно хорошо, можно попросить компилятор сгенерировать operator<=> для нас автоматически:

struct example {
  ... // Какие-то поля.

  auto operator<=>(const example &) const = default;
};

И все. Остальное за нас сделает компилятор.

Однако, думаю, не все знают, что если нам не подходит штатная реализация и мы пишем operator<=> сами, то компилятор потребует, чтобы мы еще и определили свой собственный operator==.

Например, допустим, что у нас в example три поля, а в сравнении должно участвовать только два из них:

struct example {
  int m_a;
  int m_b;
  int m_c;

  auto operator<=>(const example & o) const noexcept
  {
    return std::tie(m_a, m_c) <=> std::tie(o.m_a, o.m_c);
  }
};

В этом случае мы внезапно© обнаружим, что у нас нет operator==.

Таков путь: если мы определили свой operator<=>, то компилятор не будет автоматически выводить равенство и неравенство, а потребует, чтобы мы предоставили operator==.

Может быть вам будет интересно почему?

Если да, то лучше всего прочитать раздел с мотивацией из предложения P1185 и вот этот ответ на Stackoverflow. В двух же словах: реализация сравнения на строгое равенство на базе operator<=> может быть весьма неэффективна.

Например, представим, что мы сравниваем две строки: s1="AA" и s2="AAA".

При лексикографическом сравнении s1 меньше, чем s2: общая подстрока в s1 и s2 совпадает, но в s1 символов меньше.

Когда мы для наших строк s1 и s2 вызываем operator<=>, то этот оператор пытается выяснить как именно соотносятся s1 и s2: меньше ли s1, чем s2, или же s1 больше, чем s2, или же они равны.

Теперь допустим, что в s1 находится миллион символов "A", а в s2 -- миллион и один символ "A". При вызове operator<=> придется сравнить миллион символов в общей подстроке прежде чем мы сможем понять, что s1 все-таки меньше, т.к. у нее символов меньше.

При этом мы не можем прервать сравнение раньше. Допустим, что в s1 будет девятьсот девяносто девять тысяч и девятьсот девяносто девять символов "A", а за ними единственный символ "B", тогда как в s2 все миллион + один символ -- это "A". В таком случае строка s1 окажется больше, не смотря на то, что символов в ней меньше.

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

Когда компилятор по нашей просьбе сам генерирует operator<=>, то он эти фокусы учитывает (под катом небольшой тестовый бенчмарк, который позволяет убедится, что сравнение двух неравных векторов требует минимального времени). Однако, если operator<=> предоставляет пользователь, то у компилятора нет уверенности в том, что пользователь написал свой operator<=> с учетом возможностей оптимизации операции "строгое равенство". И просит нас взять ответственность за operator== на себя.