C++SeniorCoding

Что такое variadic templates?

Variadic templates (C++11) принимают произвольное число типовых аргументов через parameter packs (typename... Args). До C++17 раскрываются рекурсией; в C++17 fold expressions (args + ...) делают это однострочно без рекурсии.

Что такое variadic templates

Variadic templates (C++11) — шаблоны с переменным числом типовых параметров (parameter packs). Позволяют писать функции и классы, принимающие произвольное количество аргументов любых типов, при полной проверке типов на этапе компиляции.

#include <iostream>
#include <string>
#include <tuple>
#include <memory>

// sizeof... — количество элементов в пакете
template<typename... Args>
void count_args(Args... args) {
    std::cout << "count: " << sizeof...(args) << '\n';
}

// Рекурсивное раскрытие (базовый случай + рекурсия)
void print() {}  // базовый случай: ноль аргументов

template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first << ' ';
    print(rest...);  // рекурсия с оставшимися аргументами
}

// C++17 fold expressions — намного лаконичнее рекурсии
template<typename... Args>
void print_fold(Args... args) {
    // унарный правый fold: (args << ... ) — нет; используем запятую
    ((std::cout << args << ' '), ...);
    std::cout << '\n';
}

template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // fold expression: a + b + c + ...
}

template<typename... Args>
bool all_positive(Args... args) {
    return ((args > 0) && ...);  // all-of fold
}

// Практический пример: аналог std::make_unique
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// Вариадический класс: std::tuple реализован так
template<typename... Types>
struct MyTuple;  // forward declaration

template<>
struct MyTuple<> {};  // base case

template<typename Head, typename... Tail>
struct MyTuple<Head, Tail...> : MyTuple<Tail...> {
    Head value;
    explicit MyTuple(Head h, Tail... t)
        : MyTuple<Tail...>(t...), value(h) {}
};

// Logging с типами (реальный use-case)
template<typename... Args>
void log(const char* fmt, Args&&... args) {
    // В реальном коде: fmt::format или std::format (C++20)
    // Здесь упрощённо:
    ((std::cout << std::forward<Args>(args) << ' '), ...);
    std::cout << '\n';
}

int main() {
    count_args(1, "hello", 3.14);  // count: 3
    print(1, 2.5, "three");        // 1 2.5 three
    print_fold(10, 20, 30);        // 10 20 30

    std::cout << sum(1, 2, 3, 4, 5) << '\n';       // 15
    std::cout << all_positive(1, 2, 3) << '\n';    // 1
    std::cout << all_positive(1, -2, 3) << '\n';   // 0

    struct Point { int x, y; };
    auto p = my_make_unique<Point>(1, 2);

    log("Values:", 42, "text", 3.14f);
}

Fold expressions (C++17)

Четыре вида fold:

  • Унарный левый: (... op pack)((a op b) op c)
  • Унарный правый: (pack op ...)(a op (b op c))
  • Бинарный левый: (init op ... op pack)
  • Бинарный правый: (pack op ... op init)

Подводные камни

  • Глубокая рекурсия без fold: до C++17 рекурсивные variadic шаблоны генерируют глубокую цепочку инстанциаций — медленная компиляция и возможное превышение лимита глубины шаблонов (обычно 900).
  • Порядок вычислений в fold: для operator, порядок гарантирован слева направо, для + — нет (применяйте бинарный fold с осторожностью для операций с побочными эффектами).
  • Пустой пакет и fold: унарный fold с пустым пакетом допустим только для && (→ true), || (→ false) и , (→ void); для + с пустым пакетом — ошибка компиляции.
  • Раскрытие в инициализаторе: int arr[] = {(expr(args), 0)...}; — старый трюк для гарантии порядка; с C++17 используйте fold с запятой.
  • SFINAE и variadic: ошибки внутри инстанциации пакета дают трудночитаемые сообщения; в C++20 используйте concepts и requires.
  • Перегрузка vs специализация: нельзя частично специализировать variadic функцию — используйте перегрузку или вспомогательный шаблон класса.
  • Производительность компиляции: вариадические шаблоны с большим числом аргументов (100+) резко замедляют компиляцию; для таких случаев рассмотрите type erasure или std::initializer_list.

Common mistakes

  • Объяснять variadic templates только по синтаксису, без жизненного цикла и стоимости.
  • Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
  • Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
  • Показывать сырой указатель без объяснения владельца и момента освобождения.

What the interviewer is testing

  • Кандидат формулирует точную модель для variadic templates, а не только определение.
  • Пример компилируем, безопасен по lifetime и соответствует версии технологии.
  • Названы trade-off, ограничения и диагностируемые симптомы ошибки.

Sources

Related topics