пятница, 3 июня 2022 г.

[prog.c++.flame] Пример C++ного кода встретил в LinkedIn и в очередной раз убедился, что C++ придуман экспертами для экспертов

В ленте LinkedIn наткнулся на пост из категории "что делает этот код?". Разместившие этот пост товарищи заявили, что они типа работают на переднем краю и заинтересованы в C++Ninjas. Т.е., как я понимаю, им нужны крутые разработчики.

Код же вот такой:

#include <iostream>
#include <utility>
#include <tuple>

template<typename... Args>
void do_all(Args &&... args) {
    auto do_one = [count = sizeof...(args)](auto && arg) mutable {
        std::cout << arg;
        --count > 0 ? std::cout << "," : std::cout << "\n";
        return std::ignore;
    };

    (do_one(std::forward<Args>(args)) = ...);
}

int main() {
    do_all(123456789);
    return 0;
}

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

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

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

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

В показанном примере решается простая задача обработки parameters pack в обратном порядке.

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

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

Пример выше демонстрирует остроумное решение, основанное на том, что операция присваивания правоассоциативна. Т.е. если у нас есть a = b = c = d, то компилятор обязан выполнять это вот в таком порядке: a = (b = (c = d)).

Если вас, как и меня, смущает корректность кода из примера с LinkedIn, то вот несколько ссылок для изучения, которые говорят о том, что с evaluation order там все должно быть в порядке: What are the evaluation order guarantees introduced by C++17?, Nifty Fold Expression Tricks, Folding over operator=.

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

Лучшее, что я смог придумать за час экспериментов, это вот такой вариант:

#include <iostream>

template<typename Cont, typename Head>
void do_all_impl(Cont && cont, Head && h) {
   std::cout << h;
   cont();
}

template<typename Cont, typename Head, typename... Tail>
void do_all_impl(Cont && cont, Head && h, Tail &&... t) {
   do_all_impl(
      [&h, &cont]() { std::cout << "," << h; cont(); },
      std::forward<Tail>(t)...);
}

template<typename... Args>
void do_all(Args &&... args) {
   do_all_impl(
      []() { std::cout << "\n"; },
      std::forward<Args>(args)...);
}

int main() {
   do_all(123456789);
   return 0;
}

И, вроде как, GCC-11 даже для этого варианта генерирует бинарник чуть-чуть меньшего размера (а вот clang-14, напротив, чуть большего).

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


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

Так вот, я просто в откровенном восхищении от использования fold expression с присваиванием для обхода параметров в обратном порядке. И искрене не понимаю, как кто-то до этого додумался.

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

Тупо завидую. Мне такое не под силу.

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

Тут мне вспоминается другой пример из современного C++, который для меня все еще выглядит как черная магия. Речь про integer_sequence/index_sequence.

Это нечто, то пока что лежит за пределами моего понимания. Т.е. я как-то усвоил способ(ы?) применения index_sequence, вроде как осознаю когда, куда и как index_sequence нужно запихнуть. Но если заставить меня на пальцах объяснить как работает, скажем, вот этот пример с cppreference:

// Convert array into a tuple
template<typename Array, std::size_t... I>
auto a2t_impl(const Array& a, std::index_sequence<I...>)
{
    return std::make_tuple(a[I]...);
}
 
template<typename T, std::size_t N, typename Indices = std::make_index_sequence<N>>
auto a2t(const std::array<T, N>& a)
{
    return a2t_impl(a, Indices{});
}

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

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


Я у себя в вконтактике стал делиться разными сиюминутными эмоциями на тему программизма и прочих околорабочих вещей. Всякая мелочь, которую лень публиковать в блоге. Раньше я таким делился в FB, но сейчас в FB в режиме read-only, поэтому делюсь разным во вконтактике. Так что, если кому-то интересно, то заглядывайте: vk.com/eao197.

PS. Объяснять мне в комментариях как работают parameters pack и index_sequence не нужно, я все равно потом забуду. Получится, что и вы, и я только зря потратите время.

Комментариев нет: