Простые варианты использования
Более сложные случаи Ещё некоторое количество хитростей |
Те, кто читал книгу Андрея Александреску «Современное программирование на C++», знают, что существует обширный класс задач (в области метапрограммирования с использованием шаблонов), когда шаблону при воплощении (instantiation) необходимо указать переменное (заранее неизвестное) количество аргументов. Типичные примеры таких задач:
В каждой такой задаче точное количество параметров, передаваемых соответствующему шаблону в качестве аргументов, заранее определить сложно. И, вообще говоря, зависит от желания и потребностей того, кто намеревается использовать соответствующий шаблонный класс.В рамках действующего стандарта С++ сколь-нибудь удобного решения таких задач не существует. Шаблоны могут принимать строго определённое количество параметров и никак иначе. А. Александреску (в упомянутой выше книге) предлагает общее решение, основанное на так называемых «списках типов», в котором типы представлены в виде односвязного списка, реализованного посредством рекурсивных шаблонов. Альтернативным решением (используемым, например, в boost::variant и boost::tuple) является объявление шаблонного класса с большим количеством параметров, которым (всем, кроме первого) присвоено некоторое значение по умолчанию. Оба этих решения являются половинчатыми и не охватывают весь спектр возможных задач. Поэтому для устранения недостатков существующих решений и упрощения кода новый стандарт предлагает С++-разработчикам новый вариант объявления шаблонов: «шаблоны с переменным количеством параметров» или, в оригинале, «variadic templates».
Объявление шаблона с переменным количеством параметров выглядит следующим образом:
template<typename ... Types> class VariadicTemplate { }; |
подобным же образом объявляются шаблоны с переменным количеством параметров-не типов:
template<int ... Ints> class VariadicTemplate { }; |
ПРИМЕЧАНИЕ Здесь необходимо отметить, что эмуляция подобного в рамках стандарта 2003-го года — весьма нетривиальная задача (если не сказать, что невыполнимая). |
Помимо шаблонных классов, можно также объявлять шаблонные функции с переменным количеством параметров. Подобные объявления могут выглядеть следующим образом:
template<typename ... Type> void printf(constchar* format, Type ... args); |
Очевидно, что такого рода параметры шаблонов (они называются «пакетами параметров» или «parameter packs») не могут использоваться везде, где могут использоваться обычные (одиночные) параметры шаблонов. Допустимо использование пакетов параметров в следующих контекстах:
ПРИМЕЧАНИЕ Атрибуты (attributes) – новое понятие, появившееся в C++11. Позволяет ассоциировать с различными конструкциями языка (типами, переменными, декларациями, блоками или единицами трансляции) дополнительную информацию. Например, атрибут [[noreturn]], добавленный к декларации функции, означает, что функция никогда (при штатной работе) не вернёт управление. |
В зависимости от того, где именно используется пакет параметров, соответствующим образом интерпретируются элементы этого пакета. Само использование пакета параметров называется «раскрытием пакета» (pack expansion) и записывается в коде следующим образом:
Types ... |
Где Types — это название пакета параметров.
Например, для такого объявления шаблона:
template<typename ... Types> class VariadicTemplate { }; |
возможные варианты раскрытия пакета параметров могут выглядеть так:
// раскрытие в список базовых классов. 'public Types' - паттерн class VariadicTemplate : public Types ... { //...// Раскрытие в список параметров другого шаблона. Паттерн - Typestypedef OtherVariadicTemplate<Types ...> OtherVT; // Более сложный вариант. Паттерн - Types *typedef OtherVariadicTemplate<Types* ...> SomeOtherVT; // Раскрытие в список параметров функции. Паттерном является Types, // a args - это новый список параметров:voidoperator () (Types ... args) { // Раскрытие в список аргументов при вызове функции // Операция взятия адреса будет подставлена к каждому аргументу foo(&args ...); } // Раскрытие в списке инициализации в конструкторе: VariadicTemplate() : Types() ... }; |
Под термином «паттерн» здесь понимается фрагмент кода, окружающего имя пакета параметров, который будет повторяться при раскрытии соответствующего пакета. В приведённом примере, если провести раскрытие параметров вручную, то получится, что такое воплощение шаблона:
/* ... */ VariadicTemplate<int, char, double> /* ... */ |
Будет раскрыто следующим образом:
class VariadicTemplate : publicint, publicchar, publicdouble { //...typedef OtherVariadicTemplate<int, char, double> OtherVT; typedef OtherVariadicTemplate<int*, char*, double*> SomeOtherVT; voidoperator () (int args1, char args2, double args3) { foo(&args1, &args2, &args3); } VariadicTemplate() : int(), char(), double() // очевидно, этот код получится некомпилируемым для такого списка типов }; |
ПРИМЕЧАНИЕ Вне зависимости от контекста, в процессе раскрытия пакета параметров в качестве разделителя элементов компилятор использует запятую. |
В качестве достаточно простого примера использования шаблонов с переменным числом параметров можно привести реализацию функтора. Выглядит эта реализация следующим образом:
#include <iostream> // Объявляем общий вариант шаблона, хранящего указатель на функцию. // При этом все возможные типы, которые могут придти в шаблон// в процесс воплощения, мы упаковываем в пакет параметровtemplate<typename ... Args> struct FunctorImpl; // Специализируем шаблон для указателя на простые функции. // При этом указываем, что пакет параметров содержит тип возвращаемого// значения (R) и аргументы (Args). Из этих двух параметров // (простого, и пакетного) затем формируем сигнатуру функцииtemplate<typename R, typename ... Args> struct FunctorImpl<R (Args ...)> { // Описываем тип указателя на функцию с нужной сигнатурой. // При этом раскрываем пакет параметровtypedef R (*FT)(Args ...); FunctorImpl(FT fn) : m_fn(fn) {;} // Объявляем оператор вызова функции таким образом, что он принимает // на вход ровно столько параметров, сколько аргументов// у хранимого типа функции. R operator () (Args ... args) { // Вызываем функцию, передавая ей все полученные аргументыreturn m_fn(args ...); } FT m_fn; }; // Объявляем общий шаблон-диспетчерtemplate<typename FT> struct Functor : public FunctorImpl<FT> { Functor() : FunctorImpl<FT>(NULL) {;} Functor(FT fn) : FunctorImpl<FT>(fn) {;} }; int plus_fn(int a, int b) {return a + b;} int minus_fn(int a, int b) {return a - b;} int increment(int& a) {return a ++;} int main() { Functor<int (int, int)> plus(plus_fn); Functor<int (int, int)> minus(minus_fn); Functor<int (int&)> inc(increment); std::cout << plus(10, 20) << " " << minus(10, 20) << std::endl; int a = 100; std::cout << inc(a) << " "; std::cout << a << std::endl; } |
Результат выполнения этого кода вполне ожидаемый:
30 -10
100 101
а код — простой и понятный. Для сравнения можно посмотреть файлы с реализацией boost::function.
Описанные выше шаблоны несложно специализировать для указателей на функции-члены:
// Объявляем специализацию контейнера функции для указателя на функцию член, // конкретизируя всё тот же пакет параметров template<typename T, typename R, typename ... Args> struct FunctorImpl<R (T::*)(Args ...)> { typedef R (T::*FT)(Args ...); typedef T HostType; FunctorImpl(FT fn = nullptr, T* obj = nullptr) : m_fn(fn), m_obj(obj) {;} // Объявляем два варианта оператора вызова функции - для случая, когда // функтор используется как "замыкание", и когда объект,// для которого вызывается метод, передаётся первым аргументом R operator() (Args... args) { (m_obj->*m_fn)(args ...); } R operator() (T* obj, Args... args) { (obj->*m_fn)(args ...); } FT m_fn; T* m_obj; }; // Объявляем класс-замыкание, принимающий в конструкторе объект, для которого// будет вызываться функция-член. Выглядит он очень простоtemplate<typename FT> struct Closure : public FunctorImpl<FT> { typedeftypename FunctorImpl<FT>::HostType HostType; Closure(HostType* obj, FT fn) : FunctorImpl<FT>(fn, obj) {;} }; // Использованиеclass A { public: A(int base = 0) : m_base(base) {;} int foo(int a) {return a + m_base;} private: int m_base; }; A b1(10), b2; Closure<int (A::*)(int)> a_foo(&b1, &A::foo); // Можно заметить, что общаяя реализация функтора также корректно работает // с указателями на функции-члены Functor<int (A::*)(int)> b_foo(&A::foo); std::cout << a_foo(20) << " " << b_foo(&b2, 20) << " " << b_foo(&b1, 20) << std::endl; |
Приведённый пример достаточно прост и наглядно демонстрирует основные возможности шаблонов с переменным количеством параметров. Анализируя его, можно определить следующую общую схему использования шаблонов с переменным количеством параметров:
1. Декларируется наиболее общий шаблон, последний параметр которого описывается в виде пакета параметров. В примере это:
template<typename ... Args> struct FunctorImpl; |
2. Определяются частичные специализации этого шаблона, конкретизирующие ту или иную часть пакета параметров. В приведённом примере это определение:
template<typename R, typename ... Args> struct FunctorImpl<R (Args ...)> |
3. В ряде случаев при специализации может потребоваться учитывать, что пакет параметров может оказаться пустым. Такое, вообще говоря, допустимо.
ПРИМЕЧАНИЕ При этом необходимо помнить, что в случае с шаблонными классами, параметры, упакованные в пакет, могут конкретизироваться, начиная с головы пакета. Конкретизировать параметры, начиная с хвоста пакета, невозможно (в силу того, что пакет параметров может только замыкать список параметров шаблона). В отношении шаблонных функций такого ограничения нет. |
Как отмечалось выше, пакеты параметров могут содержать не только типы, но и не-типы. Например:
// Объявляем шаблон, принимающий переменное количество целых чисел template<int ... Nums> struct NumsPack { // Объявляем статический массив, размер которого равен количеству // фактически переданных аргументовstaticint m_nums[sizeof...(Nums)]; // А также объявляем перечисление, сохраняющее количество элементов в массивеenum {nums_count = sizeof ... (Nums)}; }; // Инициализируем статический массивtemplate<int ... Nums> int NumsPack<Nums ...>::m_nums[] = {Nums ...}; |
Проверочный код:
typedef NumsPack<10, 20, 30, 40, 50> Nums_5; std::cout << Nums_5::nums_count << std::endl; for (int n = 0; n < Nums_5::nums_count; ++ n) std::cout << Nums_5::m_nums[n] << " "; std::cout << std::endl; |
печатает на консоль ожидаемые
5
10 20 30 40 50
Конструкция sizeof ... (Nums), приведённая в этом примере, используется для получения количества параметров в пакете. В ней Nums — это имя пакета параметров. К сожалению, дизайн шаблонов с переменным количеством параметров таков, что это — единственное, что можно сделать с пакетом параметров (помимо его непосредственно раскрытия). Получить параметр из пакета по его индексу, например, или совершить какие-либо более сложные манипуляции в рамках проекта нового стандарта невозможно.
При раскрытии пакетов можно применять более сложные паттерны. Например, в приведённом выше коде можно сделать следующую замену:
template<int ... Nums> int NumsPack<Nums ...>::m_nums[] = {Nums * 10 ...}; |
что приведёт к выводу на экран другой последовательности:
100 200 300 400 500
ПРИМЕЧАНИЕ Вообще, конкретный вид паттерна зависит от контекста, в котором он раскрывается. Более того, паттерн может содержать упоминание более одного пакета параметров. В этом случае все упомянутые в паттерне пакеты будут раскрываться синхронно, а потому количество фактических параметров в них должно совпадать. |
Предположим, необходимо организовать универсальный функтор-композитор, задача которого — передать в некоторую функцию результаты выполнения заданных функций для некоего аргумента. Пусть существует некоторый набор функций:
double fn1(double a) { return a * 2; } int fn2(int a) { return a * 3; } int fn3(int a) { return a * 4; } |
И две операции:
int test_opr(int a, int b) { return a + b; } int test_opr3(int a, int b, int c) { return a + b * c; } |
Необходимо написать универсальный функтор, применение операции вызова функции к которому приводило бы к выполнению такого кода:
test_opr(f1(x), f2(x)); |
или
test_opr3(f1(x), f2(x), f3(x)); |
Функтор должен принимать на вход операцию и перечень функций, результаты работы которых надо передать в качестве аргументов этой операции. Каркас определения такого функтора может выглядеть следующим образом:
template<typename Op, typename ... F> class Compositor { public: Compositor(Op op, F ... fs); }; |
Первую задачу, которую необходимо решить — это определить способа сохранения данных, переданных функции. Для этого можно применить множественное наследование от классов, непосредственно хранящих данные заданного типа:
template<typename T> struct DataHolder { T m_data; }; template<typename Op, typename ... F> class Composer : public DataHolder<F> ... { // ... }; |
Но есть некоторая сложность: если в списке передаваемых функций присутствуют несколько функций, типы которых совпадают, то код не скомпилируется, т. к. в списке базовых классов будет присутствовать один и тот же класс. Для устранения этой неоднозначности типы в пакете можно проиндексировать. Для этого будет использоваться вспомогательный тип «кортеж целых чисел», содержащий числа от 0 до заданного в качестве параметра N:
// Определяем класс собственно кортежа template<int ... Idxs> struct IndexesTuple { }; // Определяем общий вид шаблона, используемого для порождения кортежаtemplate<int Num, typename Tp = IndexesTuple<>> struct IndexTupleBuilder; // Определяем специализацию, которая генерирует последовательность чисел // в виде пакета целочисленных параметров.// Для этого в качестве второго параметра в объявлении шаблона используется// не собственно тип кортежа, а ранее сформированный пакет. Для получения // итогового пакета наследуемся от порождаемого шаблона, добавляя в пакет // новое числоtemplate<int Num, int ... Idxs> struct IndexTupleBuilder<Num, IndexesTuple<Idxs ...>> : IndexTupleBuilder<Num - 1, IndexesTuple<Idxs ..., sizeof ... (Idxs)>> { }; // Терминирующая рекурсию специализация. Содержит итоговый typedef, // определяющий кортеж с нужным набором чиселtemplate<int ... Idxs> struct IndexTupleBuilder<0, IndexesTuple<Idxs ...>> { typedef IndexesTuple<Idxs...> Indexes; }; |
В итоге использовать этот шаблон можно следующим образом:
typedef typename IndexTupleBuilder<6> Indexes; |
При этом Indexes будет эквивалентно:
IndexesTuple<0, 1, 2, 3, 4, 5> |
Чтобы этот класс был применён в реализации композитора, надо ввести промежуточный базовый класс, который и будет наследником классов с данными. При этом каждый класс с данными будет снабжён своим уникальным индексом:
template<int idx, typename T> struct DataHolder { DataHolder(T const& data) : m_data(data) {;} T m_data; }; // Сначала объявляем общий шаблон, принимающий на вход кортеж. // Объявление непосредственно в таком виде нам не потребуется, но// оно требуется для последующей специализации.template<typename IdxsTuple, typename ... F> struct ComposerBase; // Специализируем общий шаблон, извлекая из кортежа пакет параметров. // В данном случае шаблон объявляется с двумя пакетами параметров. // Это разрешено, т. к. пакеты могут быть однозначно разделены.// При наследовании используется паттерн, в котором упоминается // сразу два пакета параметров. Это позволяет однозначно сопоставить// элементы целочисленного кортежа и перечня типов функций.template<int ... Idxs, typename ... F> struct ComposerBase<IndexesTuple<Idxs...>, F ...> : public DataHolder<Idxs, F>... { // А здесь паттерн содержит сразу три пакета - пакет с индексами, // пакет типов функций и пакет аргументов. Всё это раскрывается в список// инициализации конструктора. ComposerBase(F ... fs) : DataHolder<Idxs, F>(fs)... {;} }; // Наследуем шаблон композитора от описанного выше шаблона, содержащего фактические данныеtemplate<typename Op, typename ... F> struct Composer : public ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...> { Op m_op; public: // Объявляем конструктор Composer(Op op, F const &... fs) : m_op(op), Base(fs...) {;} }; |
ПРИМЕЧАНИЕ Здесь конструкция struct ComposerBase<IndexesTuple<Idxs...>, F ...> : public DataHolder<Idxs, F>... демонстрирует пример синхронного раскрытия двух пакетов параметров. В данном случае – Idxs и F. |
Чтобы завершить реализацию композитора, необходимо определить оператор вызова функции. Для удобства его определения сначала определяется тип возвращаемого значения:
template<typename Op, typename ... F> struct Composer : /* ... */ { Op m_op; public: typedefdecltype(m_op((*(F*)nullptr)(0)...)) result_t; // ... }; |
ПРИМЕЧАНИЕ Для определения типа возвращаемого значения используется другая новая для C++ конструкция — decltype. Результатом её применения (в данном случае) является тип возвращаемого функцией значения. Конструкция выглядит несколько странной. По смыслу она эквивалентна такой: decltype(op(fs(0) ...)) Но поскольку в области видимости класса пакет fs не определён, то оператор применяется к сконвертированному к ссылке на тип функции nullptr. |
Теперь всё готово для определения оператора вызова функции. Поскольку классы, хранящие участвующие в композиции функции, в качестве одного из параметров шаблона принимают целочисленный индекс, то этот оператор реализуется через вспомогательную функцию, в которую передаётся всё тот же целочисленный кортеж:
template<typename Op, typename ... F> struct Composer : /* ... */ { Op m_op; public: ret_type operator()(int x) const { return MakeCall(x, Indexes()); } private: // Здесь используется тот же самый трюк, что и в определении класса ComposerBase. Тип кортежа используется для того, чтобы "поймать"// пакет целочисленных индексовtemplate<int ... Idxs> ret_type MakeCall(int x, IndexesTuple<Idxs...> const&) const { return m_op(DataHolder<Idxs, F>::m_data(x)...); } }; |
Осталось только определить функцию, облегчающую создание экземпляров этого класса:
template<typename Op, typename ... F> Composer<Op, F ...> Compose(Op op, F ... fs) { return Composer<Op, F...>(op, fs ...); } |
и композитор готов. Пара примеров его использования:
auto f = MakeOp(test_opr, fn1, fn2); auto ff = MakeOp(test_opr3, fn1, fn2, fn3); auto ff1 = MakeOp(test_opr3, fn1, fn2, [=](int x) {return f(x) * 5;}); // здесь последним параметром в композитор передаётся лямбда-функция. |
Полное определение шаблонного класса-композитора выглядит так:
template<int ... Idxs, typename ... F> struct ComposerBase<IndexesTuple<Idxs...>, F ...> : public DataHolder<Idxs, F>... { ComposerBase(F ... fs) : DataHolder<Idxs, F>(fs)... {;} }; template<typename Op, typename ... F> struct Composer : public ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...> { Op m_op; public: typedef ComposerBase<typename IndexTupleBuilder<sizeof...(F)>::Indexes, F...> Base; typedefdecltype(m_op((*(F*)nullptr)(0)...)) result_t; Composer(Op op, F const &... fs) : m_op(op), Base(fs...) {;} result_t operator()(int x) const { return MakeCall(x, typename IndexTupleBuilder<sizeof...(F)>::Indexes()); } private: template<int ... Idxs> result_t MakeCall(int x, IndexesTuple<Idxs...> const&) const { return m_op(DataHolder<Idxs, F>::m_data(x)...); } }; |
СОВЕТ Также этот класс можно было бы реализовать на базе кортежей из STL (std::tuple). В этом случае в классе DataHolder не было бы необходимости. С использованием std::tuple реализация композитора будет следующей: |
template<typename Op, typename ... F> class TupleComposer { Op m_op; std::tuple<F ...> m_fs; public: typedefdecltype(m_op((*(F*)nullptr)(0)...)) result_t; TupleComposer(Op op, F... fs) : m_op(op), m_fs(fs ...) {;} result_t operator()(int x) const { return MakeCall(x, typename IndexTupleBuilder<sizeof...(F)>::Indexes()); } private: template<int ... Idxs> result_t MakeCall(int x, IndexesTuple<Idxs...> const&) const { return m_op(std::get<Idxs>(m_fs) (x)...); } }; |
СОВЕТ Такой вариант выглядит несколько проще. |
Раскрытие пакета параметров в контексте «список инициализации» предоставляет программисту достаточно большую свободу действий, т. к. в этом случае паттерном может быть полноценное выражение. Например, сумму переданных в качестве аргументов чисел можно посчитать так:
template<typename ... T> void ignore(T ...) {;} template<typename ... T> int CalcSum(T ... nums) { int ret_val = 0; ignore(ret_val += nums ...); return ret_val; } |
То же в связке с constexpr:
template<typename ... T> constexprint ignore(T ... args) { return 0; } template<typename ... T> constexprint CalcSumImpl(int result, T ... args) { return (ignore(result += args ...)), result; } template<typename ... T> constexprint CalcSum(T ... args) { return CalcSumImpl(0, args ...); } |
ПРИМЕЧАНИЕ Здесь связка CalcSum - CalcSumImpl нужна для того, чтобы в constexpr-функции появилась именованная переменная-аккумулятор. В противном случае, если вводить её в теле функции, то это не будет удовлетворять требованиям квалификатора constexpr. |
Проверить, есть ли среди переданных чисел положительные — так:
template<typename ... T> bool HasPositives(T ... nums) { bool ret_val = true; ignore(ret_val = ret_val && nums >= 0 ...); return ret_val; } |
ПРЕДУПРЕЖДЕНИЕ При использовании такого метода нельзя забывать, что последовательность вычислений аргументов, передаваемых в функцию, строго говоря, не определена, и в каком именно порядке будут выполнены операции — заранее сказать нельзя. |
Подводя итог, можно сказать, что шаблоны с переменным количеством параметров — очень мощное средство, появляющееся в языке C++. Они лишены очевидных недостатков существующих сейчас списков типов (или иных эмуляций подобного поведения), позволяют относительно небольшим объёмом кода выражать достаточно сложные концепции. Приведённые в этой статье конструкции можно сравнить с аналогичными, выполненными в рамках действующего стандарта (для этого можно заглянуть в исходные файлы boost::bind, boost::function, boost::tuple). Но они не лишены и некоторых недостатков. Главный из них — ограниченное число контекстов, в которых пакеты параметров могут раскрываться. В частности, пакеты не могут раскрываться в выражения, чтобы можно было написать, например, так:
auto result = args + ...; |
К элементам пакета нельзя обращаться по индексу.