функции как значения · обобщённый кодfunctions as values · generic code

variadic templates

Шаблоны с произвольным, неограниченным числом параметров типов. Фундамент, на котором построены make_unique, std::tuple, std::function и perfect forwarding для функций с любым числом аргументов.Templates with an arbitrary, unbounded number of type parameters. The foundation underlying make_unique, std::tuple, std::function, and perfect forwarding for functions with any number of arguments.

этап компиляцииcompile-time only рекурсивное разворачиваниеrecursive unpacking

Что этоWhat it is

template<class... Args> — параметр-пакет (parameter pack), представляющий ноль или более типов сразу. Функция или класс с таким шаблоном может принимать произвольное число аргументов произвольных типов, а пакет разворачивается через синтаксис Args... в местах, где нужен список типов или значений.template<class... Args> — a parameter pack, representing zero or more types at once. A function or class built on such a template can accept an arbitrary number of arguments of arbitrary types, and the pack is expanded via the Args... syntax wherever a list of types or values is needed.

до C++11 — перегрузки вручнуюbefore C++11 — hand-written overloads
void print(); // база рекурсии// recursion base case
template <class T>
void print(T a) { std::cout << a; }
template <class T1, class T2>
void print(T1 a, T2 b) { print(a); print(b); }
// ...и так для каждого числа аргументов// ...and so on for every argument count
variadic template
void print() {}                    // база рекурсии// recursion base case

template <class T, class... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);                 // рекурсивное разворачивание// recursive unpacking
}

Зачем ввелиWhy it was introduced

До C++11 переменное число аргументов в шаблонном коде решалось плохо:Before C++11, variable argument counts in template code were handled poorly:

01

C-style variadic (...) небезопасны по типамC-style variadic (...) is not type-safe

printf-подобные функции через va_list не проверяют типы аргументов на этапе компиляции — несоответствие формата и типа приводит к undefined behavior в рантайме, что было источником уязвимостей.printf-style functions built on va_list don't check argument types at compile time — a mismatch between format and type causes undefined behavior at runtime, which was a well-known source of vulnerabilities.

02

Ручное дублирование перегрузок по числу аргументовManual duplication of overloads per argument count

Библиотеки вроде Boost.Tuple до C++11 генерировали перегрузки для 1, 2, 3... N аргументов через макросы препроцессора (Boost.Preprocessor) — рабочее, но громоздкое и трудно читаемое решение с искусственным потолком N.Libraries like Boost.Tuple, before C++11, generated overloads for 1, 2, 3... N arguments via preprocessor macros (Boost.Preprocessor) — a working but bulky, hard-to-read solution with an artificial cap on N.

03

Необходимость для perfect forwarding сквозь цепочку вызововA requirement for perfect forwarding across a call chain

std::make_unique<T>(args...) должен принять любое число аргументов конструктора T и передать их дальше — без variadic templates эта задача нерешаема в общем виде.std::make_unique<T>(args...) must accept any number of constructor arguments for T and forward them onward — without variadic templates this problem has no general solution.

Как это работает под капотомHow it works under the hood

Компилятор инстанцирует отдельную функцию (или класс) для каждого конкретного набора типов, с которым вызван шаблон — сам параметр-пакет не существует в рантайме, это чисто механизм этапа компиляции. Разворачивание пакета (...) — операция, которую компилятор применяет к паттерну, содержащему пакет, порождая список через запятую или рекурсивный вызов.The compiler instantiates a separate function (or class) for each concrete set of types the template is invoked with — the parameter pack itself doesn't exist at runtime, it's a purely compile-time mechanism. Pack expansion (...) is an operation the compiler applies to a pattern containing the pack, producing a comma-separated list or a recursive call.

template <class... Args>
void log(Args&&... args) {           // args — параметр-пакет// args is a parameter pack
    (std::cout << ... << args);       // C++17 fold expression, для контраста;// C++17 fold expression, shown for contrast;
}                                      // в C++11 — только явная рекурсия (см. print выше)// in C++11 — only explicit recursion (see print above)

template <class T, class... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    // Args&&... args — пакет universal references,// Args&&... args — a pack of universal references,
    // std::forward<Args>(args)... — разворачивание с forward на каждый элемент// std::forward<Args>(args)... — expansion with forward applied to each element
}

// sizeof...(Args) — число элементов в пакете, вычисляется на этапе компиляции// sizeof...(Args) — the number of elements in the pack, computed at compile time
template <class... Args>
constexpr std::size_t count() { return sizeof...(Args); }

В C++11 единственный способ обработать пакет поэлементно — рекурсия с базовым случаем (перегрузка без пакета или с одним элементом); fold expressions ((... op args)), которые убирают эту рекурсию, — фича C++17.In C++11, the only way to process a pack element by element is recursion with a base case (an overload with no pack, or with a single element); fold expressions ((... op args)), which eliminate that recursion, are a C++17 feature.

ГраблиGotchas

Обязательна база рекурсии — иначе бесконечное инстанцированиеA recursion base case is mandatory — otherwise infinite instantiation

Без нерекурсивной перегрузки print() без аргументов компилятор либо не сможет остановить инстанцирование при пустом пакете, либо выдаст ошибку об отсутствии подходящей перегрузки на последнем шаге разворачивания.Without a non-recursive print() overload taking no arguments, the compiler either can't stop instantiating on an empty pack, or errors out with no matching overload at the last step of expansion.

Порядок вычисления элементов пакета не определёнThe evaluation order of pack elements is unspecified

При разворачивании пакета в списке аргументов вызова (f(g(args)...)) порядок вычисления каждого g(args) не специфицирован стандартом C++11 — если g имеет побочные эффекты, порядок их проявления не гарантирован.When a pack is expanded inside a call's argument list (f(g(args)...)), the evaluation order of each g(args) is unspecified by the C++11 standard — if g has side effects, the order in which they occur is not guaranteed.

sizeof...(Args) — это не sizeof(Args)sizeof...(Args) is not sizeof(Args)

sizeof...(Args) возвращает количество типов в пакете, а не размер в байтах — синтаксически похоже на sizeof одного типа, что путает при беглом чтении незнакомого кода.sizeof...(Args) returns the number of types in the pack, not a size in bytes — it looks syntactically similar to sizeof of a single type, which is confusing on a quick read of unfamiliar code.