Вариативный шаблон

Вариативный шаблон или шаблон с переменным числом аргументов (англ. Variadic Template) в программировании — шаблон с заранее неизвестным числом аргументов, которые формируют один или несколько так называемых пакетов параметров.

Вариативный шаблон позволяет использовать параметризацию типов там, где требуется оперировать произвольным количеством аргументов, каждый из которых имеет произвольный тип[1]. Он может быть очень удобен в тех ситуациях, когда сценарий поведения шаблона может быть обобщён на неизвестное количество принимаемых данных[2].

Вариативные шаблоны поддерживаются в C++ (начиная со стандарта C++11) и D.

Вариативный шаблон в C++ (также известный как parameter pack) был разработан Дугласом Грегором и Яакко Ярви[3][4] и был позже стандартизирован в C++11. До появления C++11, шаблоны (классов и функций) могли принимать только фиксированное число аргументов, которые должны были быть определены, когда шаблон был впервые объявлен.

Синтаксис вариативного шаблона:

template<typename... Values> class tuple;

Приведенный выше шаблон класса 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;

Вариативные шаблоны могут также применяться к функциям.

template<typename... Params> void printf(const std::string &str_format, Params... parameters);

Оператор многоточия (…) играет две роли. Когда он стоит слева от имени параметра функции, он объявляет набор параметров. Когда оператор многоточия стоит справа от шаблона или аргумента вызова функции, он распаковывает параметры в отдельные аргументы, также как и args... в теле printf ниже.

void printf(const char *s)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                throw std::runtime_error("неправильный формат строки: отсутствуют аргументы");
            }
        }
        std::cout << *s++;
    }
}

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                std::cout << value;
                s += 2; // работает только для спецификаторов формата из двух символов (напр. %d, %f ).Не будет работать с %5.4f
                printf(s, args...); // вызов происходит даже когда *s == 0, чтобы обнаружить избыточные аргументы
                return;
            }
        }
        std::cout << *s++;
    }    
}

Это рекурсивный шаблон. Обратите внимание на то, что эта шаблонная вариативная версия функции 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) и т. д. ... );

Использование функции «pass» необходимо, поскольку распаковка аргументов происходит путём разделения аргументов функции через запятую, которые не эквивалентны оператору запятая. Поэтому some_function(args)...; никогда не будет работать. Кроме того, решение выше будет работать только тогда, когда возвращаемый тип some_function не void. Кроме того, вызовы some_function будут выполняться в произвольном порядке, потому что порядок вычисления аргументов функции не определён. Чтобы избежать произвольного порядка, может быть использован список инициализации в скобках, гарантирующий соблюдение последовательности слева-направо.

  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() {} // завершающая версия

template<typename Arg1, typename... Args>
void func(const Arg1& arg1, const Args&... args)
{
    process( arg1 );
    func(args...); // внимание: arg1 здесь не появляется!
}

Вариативные шаблоны также могут быть использованы в исключениях, списках базовых классов или в списке инициализации конструктора. Например, класс может наследовать следующее:

template <typename... BaseClasses> class ClassName : public BaseClasses... {
public:

    ClassName (BaseClasses&&... base_classes) : BaseClasses(base_classes)... {}
};

Оператор распаковки подставит базовые классы для класса наследника 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.

Пример функции суммирования:
double sum(double x)
{
	return x;
}

template<class... Args>
double sum(double x, Args... args)
{
	return x+sum(args...);
}

См. также

править

Примечания

править
  1. Вандевурд, Джосаттис, Грегор, 2018, Вариативные шаблоны, с. 89.
  2. Вандевурд, Джосаттис, Грегор, 2018, Вариативные шаблоны, с. 243.
  3. Douglas Gregor and Jaakko Järvi.
  4. Douglas Gregor, Jaakko Järvi, and Gary Powell.

Источники

править
  • Д. Вандевурд, Н. Джосаттис, Д. Грегор. Шаблоны C++. Справочник разработчика = C++ Templates. The Complete Guide. — 2-е. — СПб. : «Альфа-книга», 2018. — 848 с. — ISBN 978-5-9500296-8-4.

Ссылки

править