Variadic templates. Tuples, unpacking and more

В этом посте я поговорю о шаблонах с переменным числом параметров. В качестве примера будет приведена простейшая реализация класса tuple. Также я расскажу о распаковке tuple’а и подстановки, хранимых там значений в качестве аргументов функции. И напоследок приведу пример использования вышеописанных техник для реализации отложенного выполнения функции, которое может быть использовано, например, в качестве аналога finally блоков в других языках.ТеорияШаблоном с переменным числом параметров (variadic template) называется шаблон функции или класса, принимающий так называемый parameter pack. При объявлении шаблона это выглядит следующим образом template struct some_type; Такая запись значит то, что шаблон может принять 0 или более типов в качестве своих аргументов. В теле же шаблона синтаксис использования немного другой. template // Объявление void foo (Args… args); // Использование Вызов foo (1,2.3, «abcd») инстанциируется в foo(1, 2.3, «abcd»). У parameter pack«ов есть много интересных свойств (например они могу использоваться в листах захвата лямбд или в brace-init-lists), но сейчас я хотел бы остановится на двух свойствах, которыми я буду активно пользоваться далее.1. Вариадик параметр можно использовать в качестве аргумета вызова функции, применять к нему операции каста и т.п. При этом раскрывается он в зависимости от положения эллипсиса, а именно, раскрывается выражение непосредственно прилегающее к эллипсису. Звучит непонятно, но на примере думаю все станет ясно.

template T bar (T t) {/*…*/}

template void foo (Args… args) { //… }

template void foo2(Args… args) { foo (bar (args)…); } В этом примере в функции foo2 так как эллипсис стоит после вызова bar (), то сначала для каждого значения из args сначала вызовется функция bar () и в foo () в качестве аргументов попадут значения возвращенные bar ().Еще несколько примеров.

(const args&…) // → (const T1& arg1, const T2& arg2, …) ((f (args) + g (args))…) // → (f (arg1) + g (arg1), f (arg2) + g (arg2), …) (f (args…) + g (args…)) // → (f (arg1, arg2,…) + g (arg1, arg2, …)) (std: make_tuple (std: forward(args)…)) // → (std: make_tuple (std: forward(arg1), std: forward(arg2), …)) 2. Количество параметров в паке можно получить используя оператор sizeof…

template void foo (Args… args) { std: cout << sizeof...(args) << std::endl; }

foo (1, 2.3) // 2 Tuple Класс Tuple интересен, как мне кажется, даже не столько тем что для его написания и создания вспомогательных функций сейчас используются variadic templates (можно обойтись и без них), сколько тем, что tuple — рекурсивная структура данных, пришелец из другого, функционального мира (привет Haskell), что в свою очередь в очередной раз показывает насколько многогранен может быть С++.Я приведу, набросанную на коленке простейшую реализацию такого класса, которая, тем не менее, показывает основную технику работы с variadic шаблонами — «откусывание головы» пака параметров и рекурсивная обработка «хвоста», которая, кстати, также широко распространена в функциональных языках.Итак.Базовый шаблон класса, никогда не инстанциируется, поэтому без тела. template struct tuple; Основная специализация шаблона. Здесь мы отделяем «голову» типов параметров и «голову» переданных нам аргументов в конструкторе. Этот аргумент мы сохраняем в текущем классе, остальными рекурсивно займутся базовые. Получить доступ к данным базового класса мы можем скастовав «себя» к базовому типу.

template struct tuple: tuple { tuple (Head h, Tail… tail) : tuple(tail…), head_(h) {} typedef tuple base_type; typedef Head value_type; base_type& base = static_cast(*this); Head head_; }; Последний штрих (что опять же привычно для функциональных языков) — это специализировать «дно» рекурсии.

template<> struct tuple<> {}; В общем-то, необходимый минимум уже написан. Можно пользоваться нашим классом следующим образом:

tuple t (12, 2.34, 89); std: cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl; Однако, отсчитывать вручную, сколько раз надо написать .base, чтобы добраться до нужного нам элемента не очень удобно, поэтому в стандартной библиотеке написан шаблон функции get(), позволяющий получить содержимое N-ного элемента объекта класса tuple. Мы вынуждены обернуть функцию в структуру, чтобы обойти запрет на специализацию функций. В этом базовом шаблоне также происходит "откусывание головы” от тупла и перенаправление к следующему типу getter со значением индекса на единицу меньше как в случае типа элемента, так и, собственно, функции получения этого элемента.

template struct getter { typedef typename getter:: return_type return_type; static return_type get (tuple t) { return getter:: get (t); } }; И лишь когда мы стукнемся о дно рекурсии, можно сделать первые реальные действия. Тип возвращаемого значения мы возьмем на этот раз из тупла и вернем взятое оттуда же значение.

template struct getter<0, Head, Args...> { typedef typename tuple:: value_type return_type; static return_type get (tuple t) { return t.head_; } }; Ну и как это обычно принято, пишется небольшая вспомогательная функция, избавляющая нас от необходимости вручную писать параметры шаблона структуры.

template typename getter:: return_type get (tuple t) { return getter:: get (t); } Эту функцию мы и используем.

test: tuple t (12, 2.34, 89); std: cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl; std::cout << get<0>(t) << " " << get<1>(t) << " " << get<2>(t) << std::endl; Unpacking Распаковка tuple в С++! Что может быть круче=)? Эта возможность показалась настолько важной создателем Питона, что они даже внесли в язык специальный синтаксис для поддержки этой операции. Теперь мы можем пользоваться этим и в С++. Реализовать это можно по-разному (по крайней мере внешне, сам принцип везде один и тот же), я же покажу здесь самое простое на мой взгляд решение. К тому же оно напоминает то, что мы видели выше при реализации getter’a для извлечения элементов тупла. Здесь нам поможет свойство номер 1, описанное в теории выше. Наша функция распаковки должна выглядеть как-то так template auto call (F f, Tuple&& t) { return f (std: get(std: forward(t))…); } Как вы помните,

f (std: get(std: forward(t))…); распакуется в f (std: get(std: forward(t)), std: get(std: forward(t)), …) Но тут есть одна проблема, а именно, в такой функции нужно будет вручную указывать все интовые аргументы шаблона, причем указывать их правильно (в нужном порядке и нужное количество). Было бы очень хорошо, если бы удалось автоматизировать этот процесс. Для этого поступим похожим на подход к извлечению элементов из тупла образом.

template struct call_impl { auto static call (F f, Tuple&& t) { return call_impl:: call (f, std: forward(t)); } }; Здесь, как мне кажется, стоит объяснить подробнее. Начнем с параметров шаблона. С F и Tuple я думаю все понятно. Первый отвечает за наш callable объект, второй, собственно, за тупл, из которого мы будет брать объекты и подсовывать callable«у в качестве аргументов вызова. Далее идет булевый параметр Enough. Он сигнализирует набралось ли уже достаточно int параметров в …N и по нему мы будем далее специализировать наш шаблон. Наконец, TotalArgs — значение равное размеру тупла. В функции call мы, как и раньше, перенаправляем рекурсивно вызов к следующей инстанциации шаблона.При этом в самом первом вызове тип будет

call_impl // (N… — пусто, sizeof…(N) = 0) , во втором call_impl // (N… =0, sizeof…(N) = 1) и т.п. то есть ровно что нам и нужно.Наконец нам нужна специализация, в котором будут производится реальные действия, будет наконец вызываться наша функция с нужными аргументами. Эта специализация выглядит следующим образом

template struct call_impl { auto static call (F f, Tuple&& t) { return f (std: get(std: forward(t))…); } }; Также не помешает вспомогательная функция.

template auto call (F f, Tuple&& t) { typedef typename std: decay:: type type; return call_impl:: value, std: tuple_size:: value >:: call (f, std: forward(t)); } Здесь, я думаю, все прозрачно.Использовать это можно следующим образом.

int foo (int i, double d) { std: cout << "foo: " << i << " " << d << std::endl; return i; }

std: tuple t1(1, 2.3); std: cout << call(foo, t1) << std::endl; Defer Описанные выше приемы, позволяют организовывать ленивые, отложенные вычисления. В качестве частного примера таких вычислений я рассмотрю здесь ситуацию, когда нужно выполнить какой-то функционал, независимо от того каким образом мы выходим из функции, независимо от условных конструкций внутри, а также от того было ли вызвано исключение. Такая поведение похоже на finally блоки в питонах и явах или, например, в языке Go есть оператор defer, который обеспечивает описанное выше поведение.Хочу сразу оговориться, что как и многое другое в С++, эту задачу можно решить разными способами, например, используя std::bind или лямбду, собирающую аргументы и возвращающую другую лямбду и т.п. Но также вполне подойдет и хранение callable объекта и тупла с нужными аргументами.Собственно, зная то, что мы уже знаем, реализация тривиальна. template struct defer { defer (F f, Args&&… args) : f_(f), args_(std: make_tuple (std: forward(args)…)) {} F f_; std: tuple args_; ~defer () { try { call (f_, args_); } catch (…) {} } }; Как обычно, вспомогательная функция

template defer make_deferred (F f, Args&&… args) { return defer(f, std: forward(args)…); } И использование

auto d = make_deferred (foo, 1,2);

© Habrahabr.ru