В ленте 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(1, 2, 3, 4, 5, 6, 7, 8, 9); 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(1, 2, 3, 4, 5, 6, 7, 8, 9); 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 не нужно, я все равно потом забуду. Получится, что и вы, и я только зря потратите время.
Комментариев нет:
Отправить комментарий