C++ Variadic Templates | Complete Guide to Parameter Packs

C++ Variadic Templates | Complete Guide to Parameter Packs

이 글의 핵심

A practical guide to C++ variadic templates: type-safe variadic functions and classes without C-style varargs.

What are variadic templates?

Variadic templates, introduced in C++11, let you accept any number of template arguments. You can write flexible, type-safe functions and classes.

Why use them?

  • Type safety: Safer than C-style varargs (...)
  • Flexibility: Handle any number of arguments
  • Generality: Works across types
  • Compile time: No inherent runtime cost for the dispatch pattern
// ❌ C style: not type-safe
void print(int count, ...) {
    va_list args;
    va_start(args, count);
    // Types are unknown
    va_end(args);
}

// ✅ Variadic template: type-safe
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';
}

Basic syntax

// Variadic template function
template<typename... Args>
void print(Args... args) {
    (cout << ... << args) << endl;  // C++17 fold expression
}

int main() {
    print(1, 2, 3);              // 123
    print("Hello", " ", "World"); // Hello World
}

Terminology:

  • typename... Args: template parameter pack
  • Args... args: function parameter pack
  • args...: pack expansion
template<typename... Args>  // template parameter pack
void func(Args... args) {   // function parameter pack
    process(args...);       // pack expansion
}

Recursive templates

// Base case
void print() {
    cout << endl;
}

// Recursive case
template<typename T, typename... Args>
void print(T first, Args... rest) {
    cout << first << " ";
    print(rest...);  // recurse
}

int main() {
    print(1, 2, 3, 4, 5);  // 1 2 3 4 5
}

The sizeof... operator

template<typename... Args>
void printCount(Args... args) {
    cout << "Argument count: " << sizeof...(args) << endl;
}

int main() {
    printCount(1, 2, 3);           // 3
    printCount("a", "b", "c", "d"); // 4
}

Fold expressions (C++17)

// Unary left fold: (... op pack)
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // ((arg1 + arg2) + arg3) + ...
}

// Unary right fold: (pack op ...)
template<typename... Args>
auto sum2(Args... args) {
    return (args + ...);  // arg1 + (arg2 + (arg3 + ...))
}

// Binary fold
template<typename... Args>
void printAll(Args... args) {
    (cout << ... << args) << endl;
}

int main() {
    cout << sum(1, 2, 3, 4, 5) << endl;  // 15
    printAll(1, " ", 2, " ", 3);          // 1 2 3
}

Practical examples

Example 1: Type-safe printf

#include <iostream>
#include <sstream>
using namespace std;

void printf_impl(ostringstream& oss, const char* format) {
    oss << format;
}

template<typename T, typename... Args>
void printf_impl(ostringstream& oss, const char* format, T value, Args... args) {
    while (*format) {
        if (*format == '%' && *(++format) != '%') {
            oss << value;
            printf_impl(oss, format, args...);
            return;
        }
        oss << *format++;
    }
}

template<typename... Args>
string sprintf(const char* format, Args... args) {
    ostringstream oss;
    printf_impl(oss, format, args...);
    return oss.str();
}

int main() {
    cout << sprintf("Hello % from %", "World", "C++") << endl;
    cout << sprintf("% + % = %", 1, 2, 3) << endl;
}

Example 2: Tuple-like implementation

template<typename... Types>
class Tuple;

template<>
class Tuple<> {};

template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
private:
    Head value;
    
public:
    Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}
    
    Head& head() { return value; }
    Tuple<Tail...>& tail() { return *this; }
};

template<size_t Index, typename... Types>
struct TupleElement;

template<typename Head, typename... Tail>
struct TupleElement<0, Tuple<Head, Tail...>> {
    using type = Head;
    
    static Head& get(Tuple<Head, Tail...>& t) {
        return t.head();
    }
};

template<size_t Index, typename Head, typename... Tail>
struct TupleElement<Index, Tuple<Head, Tail...>> {
    using type = typename TupleElement<Index-1, Tuple<Tail...>>::type;
    
    static type& get(Tuple<Head, Tail...>& t) {
        return TupleElement<Index-1, Tuple<Tail...>>::get(t.tail());
    }
};

template<size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
    return TupleElement<Index, Tuple<Types...>>::get(t);
}

int main() {
    Tuple<int, double, string> t(42, 3.14, "Hello");
    
    cout << get<0>(t) << endl;  // 42
    cout << get<1>(t) << endl;  // 3.14
    cout << get<2>(t) << endl;  // Hello
}

Example 3: Function pipeline

template<typename... Funcs>
class Pipeline;

template<typename Func>
class Pipeline<Func> {
private:
    Func func;
    
public:
    Pipeline(Func f) : func(f) {}
    
    template<typename T>
    auto operator()(T value) {
        return func(value);
    }
};

template<typename Func, typename... Rest>
class Pipeline<Func, Rest...> {
private:
    Func func;
    Pipeline<Rest...> rest;
    
public:
    Pipeline(Func f, Rest... r) : func(f), rest(r...) {}
    
    template<typename T>
    auto operator()(T value) {
        return rest(func(value));
    }
};

template<typename... Funcs>
auto makePipeline(Funcs... funcs) {
    return Pipeline<Funcs...>(funcs...);
}

int main() {
    auto pipeline = makePipeline(
         { return x * 2; },
         { return x + 10; },
         { return x * x; }
    );
    
    cout << pipeline(5) << endl;  // ((5*2)+10)^2 = 400
}

Example 4: Variadic min/max

template<typename T>
T min(T value) {
    return value;
}

template<typename T, typename... Args>
T min(T first, Args... rest) {
    T restMin = min(rest...);
    return first < restMin ? first : restMin;
}

// C++17 fold version
template<typename... Args>
auto minFold(Args... args) {
    return (std::min)({args...});
}

int main() {
    cout << min(5, 2, 8, 1, 9) << endl;      // 1
    cout << minFold(5, 2, 8, 1, 9) << endl; // 1
}

Pack expansion patterns

// Apply a function to each argument
template<typename Func, typename... Args>
void forEach(Func f, Args... args) {
    (f(args), ...);  // fold over comma
}

// Push each argument into a vector
template<typename... Args>
vector<int> makeVector(Args... args) {
    vector<int> result;
    (result.push_back(args), ...);
    return result;
}

int main() {
    forEach( { cout << x << " "; }, 1, 2, 3, 4, 5);
    cout << endl;
    
    auto v = makeVector(10, 20, 30);
    for (int x : v) cout << x << " ";
}

Extracting types

// First type
template<typename... Args>
struct FirstType;

template<typename First, typename... Rest>
struct FirstType<First, Rest...> {
    using type = First;
};

// Nth type
template<size_t N, typename... Args>
struct NthType;

template<typename First, typename... Rest>
struct NthType<0, First, Rest...> {
    using type = First;
};

template<size_t N, typename First, typename... Rest>
struct NthType<N, First, Rest...> {
    using type = typename NthType<N-1, Rest...>::type;
};

int main() {
    using T1 = FirstType<int, double, string>::type;  // int
    using T2 = NthType<1, int, double, string>::type;  // double
}

Common pitfalls

Pitfall 1: Empty packs

// ❌ Error (empty pack) for unary + fold
template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // ill-formed if empty
}

// ✅ Provide an initializer
template<typename... Args>
auto sum(Args... args) {
    return (0 + ... + args);  // binary fold; empty => 0
}

Pitfall 2: Missing base case

// ❌ Infinite recursion
template<typename T, typename... Args>
void print(T first, Args... rest) {
    cout << first << " ";
    print(rest...);  // no base case!
}

// ✅ Add a base case
void print() {}  // base case

template<typename T, typename... Args>
void print(T first, Args... rest) {
    cout << first << " ";
    print(rest...);
}

Pitfall 3: Deduction ambiguity

// ❌ Deduction may be unclear
template<typename... Args>
void func(Args... args) {
    auto result = (args + ...);  // unclear if mixed types
}

// ✅ Explicit return type
template<typename... Args>
auto func(Args... args) -> decltype((args + ...)) {
    return (args + ...);
}

Performance notes

// Recursive template (can increase compile time)
template<typename T, typename... Args>
T sum(T first, Args... rest) {
    if constexpr (sizeof...(rest) == 0) {
        return first;
    } else {
        return first + sum(rest...);
    }
}

// Fold expression (often simpler for the compiler)
template<typename... Args>
auto sumFold(Args... args) {
    return (... + args);
}

Production patterns

Pattern 1: Logging

enum class LogLevel { INFO, WARNING, ERROR };

template<typename... Args>
void log(LogLevel level, Args&&... args) {
    std::ostringstream oss;
    
    switch (level) {
        case LogLevel::INFO:    oss << "[INFO] "; break;
        case LogLevel::WARNING: oss << "[WARN] "; break;
        case LogLevel::ERROR:   oss << "[ERROR] "; break;
    }
    
    (oss << ... << std::forward<Args>(args));
    
    std::cout << oss.str() << '\n';
}

log(LogLevel::INFO, "User ", 123, " logged in");
log(LogLevel::ERROR, "Failed to connect to ", "database");

Pattern 2: Type-safe formatting (illustrative)

template<typename... Args>
std::string format(const std::string& fmt, Args&&... args) {
    std::ostringstream oss;
    size_t argIndex = 0;
    
    for (size_t i = 0; i < fmt.size(); ++i) {
        if (fmt[i] == '{' && i + 1 < fmt.size() && fmt[i + 1] == '}') {
            ((argIndex++ == 0 ? (oss << args, true) : false) || ...);
            ++i;
        } else {
            oss << fmt[i];
        }
    }
    
    return oss.str();
}

Pattern 3: Event bus

template<typename... Args>
class Event {
    std::vector<std::function<void(Args...)>> handlers_;
    
public:
    void subscribe(std::function<void(Args...)> handler) {
        handlers_.push_back(std::move(handler));
    }
    
    void emit(Args... args) {
        for (auto& handler : handlers_) {
            handler(args...);
        }
    }
};

Event<int, std::string> userEvent;

userEvent.subscribe( {
    std::cout << "User " << id << ": " << name << '\n';
});

userEvent.emit(123, "Alice");

FAQ

Q1: When are variadic templates the right tool?

A: For APIs with a variable number of arguments (printf-style helpers), generic factories, wrappers, and metaprogramming over type lists.

Q2: Recursion vs fold expressions?

A: On C++17 and later, fold expressions are usually more concise and compile faster than hand-written recursion for the same logic.

Q3: Variadic templates vs varargs functions?

A: Prefer templates for type safety and compile-time checking; avoid C-style ... varargs in new code.

Q4: Is there runtime overhead?

A: No for the expansion itself: work is done at compile time, and the generated code is typically equivalent to manually written repetitions.

Q5: How do I debug template errors?

A: Use static_assert, read instantiation notes carefully, and test small cases first.

Q6: What is sizeof...?

A: It yields the number of arguments in a pack; it is a compile-time constant.

Q7: How do I handle an empty pack?

A: Use a binary fold with an initializer, e.g. (0 + ... + args), or branch with if constexpr (sizeof...(args) == 0).

Q8: Further reading?

A: C++ Templates: The Complete Guide (Vandevoorde, Josuttis, Gregor); cppreference — Parameter pack; Compiler Explorer.

Related: Variadic templates (advanced), fold expressions, perfect forwarding.

In one sentence: Variadic templates let you write type-safe APIs that accept any number of arguments, with all checking at compile time.


Practical tips

Debugging

  • Enable and fix compiler warnings first.
  • Reproduce issues with minimal examples.

Performance

  • Do not optimize without profiling.
  • Define measurable goals before tuning.

Code review

  • Watch for common review feedback in your team.
  • Follow your team’s coding conventions.

Checklist

Before coding

  • Is this the best fit for the problem?
  • Will teammates understand and maintain it?
  • Does it meet performance requirements?

While coding

  • Are warnings cleared?
  • Are edge cases handled?
  • Is error handling appropriate?

At review

  • Is intent clear?
  • Are tests sufficient?
  • Is documentation adequate?

Keywords

C++, variadic templates, parameter pack, fold expression, templates, TMP.


See also

  • C++ Fold Expressions
  • C++ Template basics
  • C++ auto deduction
  • C++ CTAD
  • C++20 Concepts