Вариатический шаблон
В программировании компьютерном шаблоны с переменным числом аргументов — это шаблоны , которые принимают переменное количество аргументов.
Шаблоны Variadic поддерживаются C++ (начиная со стандарта C++11 ) и языком программирования D.
С++
[ редактировать ]Функция вариативного шаблона C++ была разработана Дугласом Грегором и Яакко Ярви. [1] [2] и позже был стандартизирован в C++11. До C++11 шаблоны (классы и функции) могли принимать только фиксированное количество аргументов, которые необходимо было указать при первом объявлении шаблона. C++11 позволяет определениям шаблонов принимать произвольное количество аргументов любого типа.
template<typename... Values> class tuple; // takes zero or more arguments
Вышеупомянутый класс шаблона tuple
будет принимать любое количество имен типов в качестве параметров шаблона. Здесь экземпляр вышеуказанного класса шаблона создается с тремя аргументами типа:
tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> some_instance_name;
Число аргументов может быть равно нулю, поэтому tuple<> some_instance_name;
тоже будет работать.
Если шаблон с переменным числом аргументов должен допускать только положительное количество аргументов, то можно использовать следующее определение:
template<typename First, typename... Rest> class tuple; // takes one or more arguments
Шаблоны с переменным числом аргументов также могут применяться к функциям, что не только обеспечивает типобезопасное дополнение к функциям с переменным числом аргументов (таким как printf), но также позволяет функции, вызываемой с синтаксисом, подобным printf, обрабатывать нетривиальные объекты.
template<typename... Params> void my_printf(const std::string &str_format, Params... parameters);
Оператор многоточия (...) выполняет две роли. Когда оно встречается слева от имени параметра, оно объявляет пакет параметров. Используя пакет параметров, пользователь может привязать ноль или более аргументов к параметрам шаблона с переменным числом аргументов. Пакеты параметров также можно использовать для нетиповых параметров. Напротив, когда оператор многоточия встречается справа от аргумента вызова шаблона или функции, он распаковывает пакеты параметров в отдельные аргументы, например args...
в теле printf
ниже. На практике использование оператора многоточия в коде приводит к повторению всего выражения, предшествующего многоточию, для каждого последующего аргумента, распакованного из пакета аргументов, при этом выражения разделяются запятыми.
Использование шаблонов с переменным числом вариантов часто является рекурсивным. Сами переменные параметры не всегда доступны для реализации функции или класса. Таким образом, типичный механизм определения чего-то вроде вариативного кода C++11 printf
замена будет следующей:
// base case
void my_printf(const char *s)
{
while (*s)
{
if (*s == '%')
{
if (*(s + 1) != '%')
++s;
else
throw std::runtime_error("invalid format string: missing arguments");
}
std::cout << *s++;
}
}
// recursive
template<typename T, typename... Args>
void my_printf(const char *s, T value, Args... args)
{
while (*s)
{
if (*s == '%')
{
if (*(s + 1) != '%')
{
// pretend to parse the format: only works on 2-character format strings ( %d, %f, etc ); fails with %5.4f
s += 2;
// print the value
std::cout << value;
// called even when *s is 0 but does nothing in that case (and ignores extra arguments)
my_printf(s, args...);
return;
}
++s;
}
std::cout << *s++;
}
}
Это рекурсивный шаблон. Обратите внимание, что версия вариативного шаблона my_printf
вызывает себя, или (в случае, если args...
пуст) вызывает базовый вариант.
Не существует простого механизма для перебора значений вариативного шаблона. Однако существует несколько способов преобразовать пакет аргументов в один аргумент, который можно оценивать отдельно для каждого параметра. Обычно это зависит от перегрузки функции или — если функция может просто выбирать один аргумент за раз — с использованием тупого маркера расширения:
template<typename... Args> inline void pass(Args&&...) {}
который можно использовать следующим образом:
template<typename... Args> inline void expand(Args&&... args)
{
pass(some_function(args)...);
}
expand(42, "answer", true);
который расширится до чего-то вроде:
pass(some_function(arg1), some_function(arg2), some_function(arg3) /* etc... */ );
Использование этой функции «pass» необходимо, поскольку расширение пакета аргументов происходит путем разделения аргументов вызова функции запятыми, которые не эквивалентны оператору «запятая». Поэтому, some_function(args)...;
никогда не будет работать. Более того, приведенное выше решение будет работать только в том случае, если возвращаемый тип some_function
не void
. Кроме того, some_function
вызовы будут выполняться в неопределенном порядке, поскольку порядок вычисления аргументов функции не определен. Чтобы избежать неопределенного порядка, можно использовать списки инициализаторов, заключенные в фигурные скобки, которые гарантируют строгий порядок вычислений слева направо. Список инициализаторов требует не- void
тип возвращаемого значения, но оператор запятая может использоваться для получения 1
для каждого элемента расширения.
struct pass
{
template<typename ...T> pass(T...) {}
};
pass{(some_function(args), 1)...};
Вместо выполнения функции можно указать и выполнить на месте лямбда-выражение, что позволяет выполнять произвольные последовательности операторов на месте.
pass{([&](){ std::cout << args << std::endl; }(), 1)...};
Однако в этом конкретном примере лямбда-функция не требуется. Вместо этого можно использовать более обычное выражение:
pass{(std::cout << args << std::endl, 1)...};
Другой способ — использовать перегрузку с «версиями завершения» функций. Это более универсальный вариант, но для его создания требуется немного больше кода и больше усилий. Одна функция получает один аргумент некоторого типа и пакет аргументов, тогда как другая не получает ни того, ни другого. (Если бы оба имели одинаковый список начальных параметров, вызов был бы неоднозначным — один только пакет переменных параметров не может устранить неоднозначность вызова.) Например:
void func() {} // termination version
template<typename Arg1, typename... Args>
void func(const Arg1& arg1, const Args&&... args)
{
process( arg1 );
func(args...); // note: arg1 does not appear here!
}
Если args...
содержит хотя бы один аргумент, он будет перенаправлен на вторую версию — пакет параметров может быть пустым, и в этом случае он просто перенаправится на версию завершения, что ничего не сделает.
Шаблоны Variadic также можно использовать в спецификации исключений, списке базовых классов или списке инициализации конструктора. Например, класс может указывать следующее:
template <typename... BaseClasses>
class ClassName : public BaseClasses...
{
public:
ClassName (BaseClasses&&... base_classes)
: BaseClasses(base_classes)...
{}
};
Оператор распаковки реплицирует типы базовых классов ClassName
, так что этот класс будет производным от каждого из переданных типов. Кроме того, конструктор должен получить ссылку на каждый базовый класс, чтобы инициализировать базовые классы ClassName
.
Что касается шаблонов функций, можно пересылать переменные параметры. В сочетании с универсальными ссылками (см. выше) это обеспечивает идеальную пересылку:
template<typename TypeToConstruct>
struct SharedPtrAllocator
{
template<typename ...Args>
std::shared_ptr<TypeToConstruct> construct_with_shared_ptr(Args&&... params)
{
return std::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...));
}
};
При этом список аргументов распаковывается в конструктор TypeToConstruct. std::forward<Args>(params)
Синтаксис прекрасно пересылает аргументы как их собственные типы, даже с учетом rvalue, в конструктор. Оператор распаковки распространит синтаксис пересылки на каждый параметр. Эта конкретная фабричная функция автоматически оборачивает выделенную память в std::shared_ptr
для определенной степени безопасности в отношении утечек памяти.
Кроме того, количество аргументов в пакете параметров шаблона можно определить следующим образом:
template<typename ...Args>
struct SomeStruct
{
static const int size = sizeof...(Args);
};
Выражение SomeStruct<Type1, Type2>::size
даст 2, а SomeStruct<>::size
даст 0.
Д
[ редактировать ]Определение
[ редактировать ]Определение вариативных шаблонов в D аналогично их аналогу в C++:
template VariadicTemplate(Args...) { /* Body */ }
Аналогично, любой аргумент может предшествовать списку аргументов:
template VariadicTemplate(T, string value, alias symbol, Args...) { /* Body */ }
Основное использование
[ редактировать ]Вариативные аргументы очень похожи по использованию на константный массив. Они могут повторяться, доступны по индексу, имеют length
свойство и может быть нарезано . Операции интерпретируются во время компиляции, что означает, что операнды не могут быть значениями времени выполнения (например, параметрами функции).
Все, что известно во время компиляции, может быть передано как аргументы с вариациями. Это делает аргументы с переменным числом аргументов похожими на аргументы псевдонима шаблона , но более мощными, поскольку они также принимают базовые типы (char, short, int...).
Вот пример печати строкового представления переменных параметров. StringOf
и StringOf2
дают равные результаты.
static int s_int;
struct Dummy {}
void main()
{
pragma(msg, StringOf!("Hello world", uint, Dummy, 42, s_int));
pragma(msg, StringOf2!("Hello world", uint, Dummy, 42, s_int));
}
template StringOf(Args...)
{
enum StringOf = Args[0].stringof ~ StringOf!(Args[1..$]);
}
template StringOf()
{
enum StringOf = "";
}
template StringOf2(Args...)
{
static if (Args.length == 0)
enum StringOf2 = "";
else
enum StringOf2 = Args[0].stringof ~ StringOf2!(Args[1..$]);
}
Выходы:
"Hello world"uintDummy42s_int
"Hello world"uintDummy42s_int
AliasSeq
[ редактировать ]Шаблоны Variadic часто используются для создания последовательности псевдонимов, называемой AliasSeq . Определение AliasSeq на самом деле очень простое:
alias AliasSeq(Args...) = Args;
Эта структура позволяет манипулировать списком переменных аргументов, который будет автоматически расширяться. Аргументы должны быть либо символами, либо значениями, известными во время компиляции. Сюда входят значения, типы, функции и даже неспециализированные шаблоны. Это позволяет выполнять любые операции, которые вы ожидаете:
import std.meta;
void main()
{
// Note: AliasSeq can't be modified, and an alias can't be rebound, so we'll need to define new names for our modifications.
alias numbers = AliasSeq!(1, 2, 3, 4, 5, 6);
// Slicing
alias lastHalf = numbers[$ / 2 .. $];
static assert(lastHalf == AliasSeq!(4, 5, 6));
// AliasSeq auto expansion
alias digits = AliasSeq!(0, numbers, 7, 8, 9);
static assert(digits == AliasSeq!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
// std.meta provides templates to work with AliasSeq, such as anySatisfy, allSatisfy, staticMap, and Filter.
alias evenNumbers = Filter!(isEven, digits);
static assert(evenNumbers == AliasSeq!(0, 2, 4, 6, 8));
}
template isEven(int number)
{
enum isEven = (0 == (number % 2));
}
См. также
[ редактировать ]Для статей о вариационных конструкциях, отличных от шаблонов.
Ссылки
[ редактировать ]- ^ Дуглас Грегор и Яакко Ярви (февраль 2008 г.). «Шаблоны с вариациями для C++0x» . Журнал объектных технологий . стр. 31–51.
- ^ Дуглас Грегор; Яакко Ярви и Гэри Пауэлл. (февраль 2004 г.). «Шаблоны с вариациями. Номер N1603 = 04-0043 в предварительной рассылке Комитета по стандарту ISO C++».