22.11.11

C++11 Argument Unpacking & Tuples

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

Переписывая на новый лад вызов функций по таймеру, столкнулся с неприятной дыркой в имплементации лямбд и variadic templates (далее - VT) в g++: не удаётся использовать пачку аргументов функции в лямбде. Проиллюстрирую на синтетическом примере:
template<typename... Types>
void foo( Types... args )
{
[=]()
{
bar( args... ); // error: parameter packs not expanded with '...'
};
}
Решил попробовать обойти это дело, используя tuple. Всё, вроде бы, просто: создали локальный туполь из аргументов, сохранили его в лямбде, нō..
template<typename... Types>
void foo( Types... args )
{
auto t = std::make_tuple( args... );
[t]()
{
bar( /*эээ.. а здесь-то как быть?*/ );
};
}
Хотелось бы как-то вызвать bar со всеми членами t, однако сделать это, не зная количества аргументов, невозможно.. Or is it? :)

Разумеется, всегда можно написать стопицот специализаций для [0; N] аргументов. Так, например, до сих пор делает майкрософт в своей имплементации STL, потому что кто-то наверху не захотел правильно расставить приоритеты (*шёпотом* у них в компиляторе всё ещё нет VT). Но воздержусь от брани на эту тему, т.к. и без меня сказано было предостаточно, и вернусь к своим баранам.
Именно "стопицот специализаций" я и сделал. Родился устрашающего вида шаблон unpack_tuple, принимающий функциональный объект и собственно tuple, вызывающий первый со всеми членами последнего и возвращающий результат. Небольшая выдержка (специализация для пяти аргументов):
template<typename F, typename T> struct unpack_tuple<5,F,T> { static auto call( F f, const T &t ) ->
decltype( f(get<0>(t),get<1>(t),get<2>(t),get<3>(t),get<4>(t)) )
{
return f(get<0>(t),get<1>(t),get<2>(t),get<3>(t),get<4>(t));
} };
Итого: масса дублированного кода, сотня мест, где можно напортачить просто опечатавшись, и единственная альтернатива всему этому - препроцессор. А особенно весело выбирать между такими двумя злами, когда у тебя в распоряжении VT, но ты просто не можешь сообразить, как их здесь применить..

Немного позже наткнулся на такой вот багрепорт и понял, что не знаю чего-то очень важного о распаковке аргументов. Чтение стандарта последовало. :) А за ним и разрыв шаблона (no pun intended).
Итак, я открыл для себя, что при распаковке пачки аргументов, многоточие способно на куда более эпичные деяния, нежели тупая передача аргументов в другую функцию или список инициализации. Рассказываю)
template<typename... Types>
void foo( Types... args )
{
bar( args... );
}
args здесь - не просто имя пачки, это выражение, размножаемое до количества аргументов. Я был бы удивлён, если бы из такого объяснения кто-то что-то понял, поэтому продолжаю)
Стандарт приводит такого вида пример: bar( &args... ); И если у вас сейчас на уме что-то вроде: "Мы берём адрес.. пачки аргументов.. и передаём в функцию.. Што? о_О" - поздравляю, ваш шаблон только что надорвался. :D
Что здесь на самом деле происходит, будет легче понять, поставив скобки: bar( (&args)... );, и вообразив на минуту, что args - самый обыкновенный одиночный объект. Мы взяли его адрес. А теперь мы видим многоточие после этого выражения, что говорит нам, что выражение будет повторено для каждого аргумента в пачке args, и каждый результат этого выражения (адрес каждого аргумента в данном примере) будет передан в bar. Круто, да?) На самом деле - не очень. Пример скучен; соорудим что-нибудь повеселей)
template<typename... Types>
void foo( Types... args )
{
bar( (pow(args+1, 1.0/3) * math::pi)... );
}
i-ый аргумент суммируется с единицей, встаёт под кубический корень, умножается на некое math::pi и передаётся как i-ый аргумент в bar. И, конечно же, аргументам не обязательно быть одного типа, всё что от них всех требуется - поддержка операций сложения и умножения и перегрузка pow. Вызов foo(42, 3.14) может, например, привести к вызову bar(11.0, 4.6) (приблизительно).
Одним словом, слева от многоточия может стоять совершенно произвольное выражение, вовлекающее пачку аргументов. Нас даже может волновать не результат выражения (который будет передан в bar), а его побочные эффекты:
template<typename... Types>
void foo( Types... args )
{
bar( (cout << args << '\n')... );
}
Возникают, однако, неудобства. Происходит излишний вызов bar с пачкой объектов типа ostream, но, что более важно, порядок вывода аргументов не определён. Спасает то, что расширение (именно так "по-научному" называется данная операция) пачек аргументов многоточием возможно далеко не только в контексте вызова функции. В частности, можно сконструировать массив:
template<typename... Types>
void foo( Types... args )
{
int arr[] = { (cout << args << '\n', 0)... };
(void)arr;
}
Незачем создавать массив ostream (особенно в свете того, что его копирование запрещено :)), да и результатом какого-то другого выражения может оказаться void, так что здесь мы прибегаем к старому доброму оператору ЗПТ и составляем при его помощи int-массив нулей.
Порядок вычисления выражений на этот раз гарантируется стандартом (8.5.4.4).

Вышеприведённые исследования начали наводить меня на кое-какие мысли. Во-первых, нельзя ли таким образом размножать выражения при помощи пачки параметров шаблона? Можно)
template<typename... Types>
void foo( Types... )
{
int arr[] = { (cout << typeid(Types).name() << '\n', 0)... };
(void)arr;
}
Можно юзать пачку типов, значит можно и пачку целых:
template<int... Ints>
void foo()
{
int arr[] = { (cout << Ints << '\n', 0)... };
(void)arr;
}
Думаю, наблюдательный читатель уже догадывается, к чему я клоню. :))
template<size_t... Indices, typename Tuple>
void foo( Tuple &t )
{
bar( std::get<Indices>(t)... );
}

std::tuple<bool,float,std::string> t( true, 100500, "hello world" );
foo<0,1,2>( t );
Всё содержимое нашего туполя будет "распаковано" и передано в bar. Мы даже можем поменять порядок (и кол-во повторений) аргументов: foo<1,0,1,2,0>( t ) позовёт bar(100500, true, 100500, "hello world", true). Но это нас сейчас не интересует.
Что нас интересует, так это автоматическая генерация такой вот-> 0,1,2,... последовательности индексов на основе размера туполя. Новые старые добрые рекурсивные шаблоны спешат на помощь. :)
template<size_t N, size_t... Indices>
struct unpack_tuple_helper
{
template<typename F, typename T>
static void call( F f, const T &t )
{
unpack_tuple_helper<N-1,N-1,Indices...>::call( f, t );
}
};
template<size_t... Indices>
struct unpack_tuple_helper<0,Indices...>
{
template<typename F, typename T>
static void call( F f, const T &t )
{
f( std::get<Indices>(t)... );
}
};

template<typename Func, typename Tuple>
void unpack_tuple( Func f, const Tuple &t )
{
unpack_tuple_helper< std::tuple_size<Tuple>::value >::call( f, t );
}
Немного помогу поломавшим мозг, разглядывая это чудо) При вызове unpack_tuple с туполем размером, скажем, 4, рекурсивно инстанцируется следующая цепочка классов:
unpack_tuple_helper<4>
unpack_tuple_helper<3,3>
unpack_tuple_helper<2,2,3>
unpack_tuple_helper<1,1,2,3>
unpack_tuple_helper<0,0,1,2,3>
Последний случай подпадает под специализацию unpack_tuple_helper<0,Indices...>, и в пачке Indices оказываются все нужные нам индексы в нужном порядке. С их помощью туполь "распаковывается" при вызове предоставленного функционального объекта f.
Остаётся сделать перегрузку для неконстантного t и организовать возврат значения из f (пара auto + decltype), и можно умывать руки. ^_^

Использовать сабж можно, разумеется, не только с std::tuple, но и с std::pair и вообще любыми классами, для которых существуют соответствующие перегрузки std::get и специализации std::tuple_size.
Если для кого открыл Америку - что ж, это у меня бывает) Если кто-то почерпнул для себя что-то новое - на здоровье)

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

Отправить комментарий