AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

12 Templates and generic programming / Lesson 4 of 6

Non-type and variadic templates — numbers and packs as parameters

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What is a variadic template in C++?

A variadic template is a template that accepts any number of arguments through a parameter pack: template <typename... Ts> void f(const Ts&... args). args... expands the pack into a comma-separated list, sizeof...(args) counts it, and a pack may be empty. Before C++17 each element was processed by recursion; fold expressions such as (args + ...) now do it in one line.

A template parameter does not have to be a type. std::array<int, 4> takes a number, std::bitset<64> takes a number, and std::tuple<int, std::string, double> takes as many types as you give it. Both ideas — a non-type parameter that is a compile-time value, and a parameter pack that stands for any number of arguments — are what let a container know its size without storing it and a print function accept three arguments of three types. This lesson covers non-type parameters, packs and sizeof..., recursion over a pack, and the C++17 fold expressions that replace most of that recursion with one line.

Non-type template parameters

#include <array>
#include <cstddef>

template <typename T, std::size_t N>
class Ring {                                // a fixed-capacity buffer that overwrites its oldest entry
public:
    static constexpr std::size_t capacity = N;

    void push(const T& value) {
        data_[(start_ + size_) % N] = value;
        if (size_ < N) ++size_;
        else start_ = (start_ + 1) % N;     // full: the slot we just wrote was the oldest
    }
    std::size_t size() const { return size_; }
    const T& at(std::size_t i) const { return data_[(start_ + i) % N]; }   // 0 = oldest

private:
    std::array<T, N> data_{};
    std::size_t start_ = 0;
    std::size_t size_ = 0;
};

Ring<int, 4> recent;                        // N = 4, baked into the type

std::size_t N declares a parameter that is a value, and every use of N inside the class is a compile-time constant: it sizes the std::array, it is the modulus, and it can initialise a static constexpr member. There is no size field for the capacity because the capacity is part of the type — sizeof(std::array<int, 4>) is 16 bytes, exactly four ints, which is the reason std::array exists (Module 6). Ring<int, 4> and Ring<int, 8> are different types, as unrelated as Stack<int> and Stack<long> were in lesson 2.

The argument must be a constant expression. int n; std::cin >> n; Ring<int, n> r; is refused with the value of 'n' is not usable in a constant expression; a const int n = 4; is fine because its initialiser is a constant, while const int n = read(); is not. When the size genuinely comes from input, either dispatch at run time to a few fixed instantiations — if (n == 4) run<4>(); else run<8>(); — or use a std::vector.

Allowed kinds: integers and enumerations, pointers and references to objects with static storage, std::nullptr_t, and since C++20 floating-point values and class types whose members are all public. template <auto V> lets the parameter's type be deduced from the argument. Non-type parameters can also be deduced from a call: template <typename T, std::size_t N> std::size_t length(const T (&)[N]) { return N; } learns the length of a built-in array from the argument's type, which is how std::size(arr) works.

Parameter packs

#include <iostream>

template <typename... Ts>                   // Ts is a pack of types
void count_args(const Ts&... args) {        // args is a pack of parameters
    std::cout << sizeof...(args) << " arguments\n";
}

count_args(1, 2.5, "three");                // Ts = {int, double, char[6]}: 3 arguments
count_args();                               // an empty pack is legal: 0 arguments

typename... Ts declares a pack; Ts... or args... expands it — the compiler replaces the expansion with a comma-separated list, one entry per element. sizeof... gives the count without expanding. Expansion can carry a pattern: f(args)... becomes f(a1), f(a2), f(a3), and std::forward<Ts>(args)... is the pattern std::make_unique uses to pass every constructor argument through unchanged. A pack can be empty, which matters when you write the base case below.

Recursion over a pack

Before C++17 the only way to do something with each element was to peel one off and recurse:

void print() { std::cout << '\n'; }                          // base case: the empty pack

template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if (sizeof...(rest) > 0) std::cout << ' ';
    print(rest...);                                          // one shorter each time
}

print("total", 3, 2.5);                                      // total 3 2.5

print("total", 3, 2.5) instantiates print<char[6], int, double>, which calls print<int, double>, then print<double>, then the non-template print(). Four functions for one call, and the base case is not optional — without it the last call print() has no candidate and the error names it. if constexpr (sizeof...(rest) > 0) print(rest...); (lesson 6) removes the need for the base case by not compiling the recursive call when the pack is empty. The peel-one-off shape is still the right one when the first element is special: a separator argument, a format string, a stream.

Fold expressions (C++17)

A fold applies one binary operator across the whole pack in a single expression:

template <typename... Ts>
auto sum(Ts... xs) { return (xs + ...); }             // unary right fold: x1 + (x2 + (x3))

template <typename... Ts>
auto sum_from_zero(Ts... xs) { return (0 + ... + xs); } // binary left fold: ((0 + x1) + x2) + x3

template <typename... Ts>
bool all(Ts... flags) { return (... && flags); }      // true for an empty pack

template <typename... Ts>
void print_all(const Ts&... xs) {
    ((std::cout << xs << ' '), ...);                  // fold over the comma operator: one statement per element
    std::cout << '\n';
}
FormSpellingExpands to
unary right fold(pack op ...)x1 op (x2 op (x3))
unary left fold(... op pack)((x1 op x2) op x3)
binary right fold(pack op ... op init)x1 op (x2 op init)
binary left fold(init op ... op pack)(init op x1) op x2

The parentheses are part of the syntax. Left and right differ only when the operator is not associative — (xs - ...) and (... - xs) give different answers — and the binary forms supply a starting value, which is what makes them legal on an empty pack: (xs + ...) with no arguments is a compile error, (0 + ... + xs) is 0. Only &&, || and , may fold an empty pack unaided, giving true, false and nothing. The comma fold is the everyday one: it runs an expression once per element, in order, and replaces the whole recursive print. (std::cout << ... << xs) is a binary left fold over << that prints every element with no separator.

Variadic class templates

std::tuple<Ts...> stores one member per pack element; std::variant<Ts...> stores one of them at a time (Module 15). The one variadic class you will write yourself is the visitor helper:

template <typename... Fs>
struct Overloaded : Fs... {                 // inherit from every lambda
    using Fs::operator()...;                // and pull in every call operator
};

Passed to std::visit, it lets a handful of lambdas act as one overload set; Module 15 shows the whole idiom.

Pitfalls

MistakeWhat happens
Ring<int, n> with a run-time nNot a constant expression: compile error.
Passing Ring<int, 4> to a function taking Ring<int, 8>&Different types: no match.
(xs + ...) on an empty packCompile error; use (0 + ... + xs).
Recursion without a base caseno matching function for call to 'print()'.
f(args...) when you meant f(args)...Passes all arguments to one call instead of calling once per argument.
sizeof(Ts)... for sizeof...(Ts)The former is a list of sizes, not a count.
Reading (xs - ...) as left to rightIt is a right fold: x1 - (x2 - x3).

Key takeaways

  • A non-type parameter is a compile-time value baked into the type; std::array<T, N> stores no size because N is the type.
  • A template argument must be a constant expression; dispatch run-time values to a few fixed instantiations.
  • typename... Ts declares a pack, Ts... expands it, sizeof...(Ts) counts it, and a pack may be empty.
  • Recursion peels one element per call and needs a base case; if constexpr can replace it.
  • Fold expressions apply one operator across a pack; the binary forms are safe on an empty pack, and the comma fold runs code per element.

Common questions

What is a fold expression in C++17?

A fold expression applies one binary operator across a whole parameter pack: (xs + ...) is a unary right fold, (... + xs) a unary left fold, and (0 + ... + xs) a binary fold with a starting value. The parentheses are part of the syntax. The comma fold ((std::cout << xs << ' '), ...) runs an expression once per element, in order.

What happens when you fold an empty parameter pack?

A unary fold over an empty pack is a compile error for most operators, so (xs + ...) with no arguments fails. Only &&, || and the comma operator have defaults: true, false and nothing. A binary fold supplies its own initial value, so (0 + ... + xs) is simply 0 for an empty pack.

What is a non-type template parameter?

A template parameter that is a compile-time value rather than a type, as in template <typename T, std::size_t N> class Ring. The value is part of the type, so Ring<int, 4> and Ring<int, 8> are different types and the size needs no data member. The argument must be a constant expression; a value read at run time cannot be used.

How does std::array know its size without storing it?

The size is a non-type template parameter: std::array<int, 4> has N = 4 baked into its type, so the object is exactly the size of four ints, with no size field. A function template can deduce N from a built-in array's type too, which is how std::size(arr) works.

How do you recurse over a parameter pack?

Peel off the first argument and call the function again with the rest: template <typename First, typename... Rest> void print(const First& first, const Rest&... rest) prints first and calls print(rest...). A non-template print() is the base case for the empty pack; if constexpr (sizeof...(rest) > 0) can replace it.

Mark as readSign in to keep your place.

C++ Variadic Templates and Fold Expressions Explained

Variadic templates take any number of arguments as a parameter pack, and C++17 fold expressions apply one operator across it. Plus non-type template parameters.

  • Course: C++ study plan
  • Module: Templates and generic programming
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What is a variadic template in C++?

A variadic template is a template that accepts any number of arguments through a parameter pack: template <typename... Ts> void f(const Ts&... args). args... expands the pack into a comma-separated list, sizeof...(args) counts it, and a pack may be empty. Before C++17 each element was processed by recursion; fold expressions such as (args + ...) now do it in one line.

Lesson

A template parameter does not have to be a type. std::array<int, 4> takes a number, std::bitset<64> takes a number, and std::tuple<int, std::string, double> takes as many types as you give it. Both ideas — a non-type parameter that is a compile-time value, and a parameter pack that stands for any number of arguments — are what let a container know its size without storing it and a print function accept three arguments of three types. This lesson covers non-type parameters, packs and sizeof..., recursion over a pack, and the C++17 fold expressions that replace most of that recursion with one line.

Non-type template parameters

#include <array>
#include <cstddef>

template <typename T, std::size_t N>
class Ring {                                // a fixed-capacity buffer that overwrites its oldest entry
public:
    static constexpr std::size_t capacity = N;

    void push(const T& value) {
        data_[(start_ + size_) % N] = value;
        if (size_ < N) ++size_;
        else start_ = (start_ + 1) % N;     // full: the slot we just wrote was the oldest
    }
    std::size_t size() const { return size_; }
    const T& at(std::size_t i) const { return data_[(start_ + i) % N]; }   // 0 = oldest

private:
    std::array<T, N> data_{};
    std::size_t start_ = 0;
    std::size_t size_ = 0;
};

Ring<int, 4> recent;                        // N = 4, baked into the type

std::size_t N declares a parameter that is a value, and every use of N inside the class is a compile-time constant: it sizes the std::array, it is the modulus, and it can initialise a static constexpr member. There is no size field for the capacity because the capacity is part of the type — sizeof(std::array<int, 4>) is 16 bytes, exactly four ints, which is the reason std::array exists (Module 6). Ring<int, 4> and Ring<int, 8> are different types, as unrelated as Stack<int> and Stack<long> were in lesson 2.

The argument must be a constant expression. int n; std::cin >> n; Ring<int, n> r; is refused with the value of 'n' is not usable in a constant expression; a const int n = 4; is fine because its initialiser is a constant, while const int n = read(); is not. When the size genuinely comes from input, either dispatch at run time to a few fixed instantiations — if (n == 4) run<4>(); else run<8>(); — or use a std::vector.

Allowed kinds: integers and enumerations, pointers and references to objects with static storage, std::nullptr_t, and since C++20 floating-point values and class types whose members are all public. template <auto V> lets the parameter's type be deduced from the argument. Non-type parameters can also be deduced from a call: template <typename T, std::size_t N> std::size_t length(const T (&)[N]) { return N; } learns the length of a built-in array from the argument's type, which is how std::size(arr) works.

Parameter packs

#include <iostream>

template <typename... Ts>                   // Ts is a pack of types
void count_args(const Ts&... args) {        // args is a pack of parameters
    std::cout << sizeof...(args) << " arguments\n";
}

count_args(1, 2.5, "three");                // Ts = {int, double, char[6]}: 3 arguments
count_args();                               // an empty pack is legal: 0 arguments

typename... Ts declares a pack; Ts... or args... expands it — the compiler replaces the expansion with a comma-separated list, one entry per element. sizeof... gives the count without expanding. Expansion can carry a pattern: f(args)... becomes f(a1), f(a2), f(a3), and std::forward<Ts>(args)... is the pattern std::make_unique uses to pass every constructor argument through unchanged. A pack can be empty, which matters when you write the base case below.

Recursion over a pack

Before C++17 the only way to do something with each element was to peel one off and recurse:

void print() { std::cout << '\n'; }                          // base case: the empty pack

template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if (sizeof...(rest) > 0) std::cout << ' ';
    print(rest...);                                          // one shorter each time
}

print("total", 3, 2.5);                                      // total 3 2.5

print("total", 3, 2.5) instantiates print<char[6], int, double>, which calls print<int, double>, then print<double>, then the non-template print(). Four functions for one call, and the base case is not optional — without it the last call print() has no candidate and the error names it. if constexpr (sizeof...(rest) > 0) print(rest...); (lesson 6) removes the need for the base case by not compiling the recursive call when the pack is empty. The peel-one-off shape is still the right one when the first element is special: a separator argument, a format string, a stream.

Fold expressions (C++17)

A fold applies one binary operator across the whole pack in a single expression:

template <typename... Ts>
auto sum(Ts... xs) { return (xs + ...); }             // unary right fold: x1 + (x2 + (x3))

template <typename... Ts>
auto sum_from_zero(Ts... xs) { return (0 + ... + xs); } // binary left fold: ((0 + x1) + x2) + x3

template <typename... Ts>
bool all(Ts... flags) { return (... && flags); }      // true for an empty pack

template <typename... Ts>
void print_all(const Ts&... xs) {
    ((std::cout << xs << ' '), ...);                  // fold over the comma operator: one statement per element
    std::cout << '\n';
}
FormSpellingExpands to
unary right fold(pack op ...)x1 op (x2 op (x3))
unary left fold(... op pack)((x1 op x2) op x3)
binary right fold(pack op ... op init)x1 op (x2 op init)
binary left fold(init op ... op pack)(init op x1) op x2

The parentheses are part of the syntax. Left and right differ only when the operator is not associative — (xs - ...) and (... - xs) give different answers — and the binary forms supply a starting value, which is what makes them legal on an empty pack: (xs + ...) with no arguments is a compile error, (0 + ... + xs) is 0. Only &&, || and , may fold an empty pack unaided, giving true, false and nothing. The comma fold is the everyday one: it runs an expression once per element, in order, and replaces the whole recursive print. (std::cout << ... << xs) is a binary left fold over << that prints every element with no separator.

Variadic class templates

std::tuple<Ts...> stores one member per pack element; std::variant<Ts...> stores one of them at a time (Module 15). The one variadic class you will write yourself is the visitor helper:

template <typename... Fs>
struct Overloaded : Fs... {                 // inherit from every lambda
    using Fs::operator()...;                // and pull in every call operator
};

Passed to std::visit, it lets a handful of lambdas act as one overload set; Module 15 shows the whole idiom.

Pitfalls

MistakeWhat happens
Ring<int, n> with a run-time nNot a constant expression: compile error.
Passing Ring<int, 4> to a function taking Ring<int, 8>&Different types: no match.
(xs + ...) on an empty packCompile error; use (0 + ... + xs).
Recursion without a base caseno matching function for call to 'print()'.
f(args...) when you meant f(args)...Passes all arguments to one call instead of calling once per argument.
sizeof(Ts)... for sizeof...(Ts)The former is a list of sizes, not a count.
Reading (xs - ...) as left to rightIt is a right fold: x1 - (x2 - x3).

Key takeaways

  • A non-type parameter is a compile-time value baked into the type; std::array<T, N> stores no size because N is the type.
  • A template argument must be a constant expression; dispatch run-time values to a few fixed instantiations.
  • typename... Ts declares a pack, Ts... expands it, sizeof...(Ts) counts it, and a pack may be empty.
  • Recursion peels one element per call and needs a base case; if constexpr can replace it.
  • Fold expressions apply one operator across a pack; the binary forms are safe on an empty pack, and the comma fold runs code per element.

Common questions

What is a fold expression in C++17?

A fold expression applies one binary operator across a whole parameter pack: (xs + ...) is a unary right fold, (... + xs) a unary left fold, and (0 + ... + xs) a binary fold with a starting value. The parentheses are part of the syntax. The comma fold ((std::cout << xs << ' '), ...) runs an expression once per element, in order.

What happens when you fold an empty parameter pack?

A unary fold over an empty pack is a compile error for most operators, so (xs + ...) with no arguments fails. Only &&, || and the comma operator have defaults: true, false and nothing. A binary fold supplies its own initial value, so (0 + ... + xs) is simply 0 for an empty pack.

What is a non-type template parameter?

A template parameter that is a compile-time value rather than a type, as in template <typename T, std::size_t N> class Ring. The value is part of the type, so Ring<int, 4> and Ring<int, 8> are different types and the size needs no data member. The argument must be a constant expression; a value read at run time cannot be used.

How does std::array know its size without storing it?

The size is a non-type template parameter: std::array<int, 4> has N = 4 baked into its type, so the object is exactly the size of four ints, with no size field. A function template can deduce N from a built-in array's type too, which is how std::size(arr) works.

How do you recurse over a parameter pack?

Peel off the first argument and call the function again with the rest: template <typename First, typename... Rest> void print(const First& first, const Rest&... rest) prints first and calls print(rest...). A non-template print() is the base case for the empty pack; if constexpr (sizeof...(rest) > 0) can replace it.

Exercises

Ring<T, N> with the capacity chosen at run time

Write template <typename T, std::size_t N> class Ring — a fixed-capacity buffer over a std::array<T, N> that overwrites its oldest entry when full — with push, size, at(i) (0 is the oldest) and a static constexpr std::size_t capacity = N. Then a function template run<N>() that prints capacity=<N> from Ring<int, N>::capacity and processes commands.

The first line is the capacity, one of 2, 4 or 8. It is read at run time, so it cannot be a template argument directly: dispatch to run<2>(), run<4>() or run<8>(). Commands: push <v>; size (print size=<k>); oldest (print oldest=<v> or empty); print (print ring: <oldest> … <newest> or ring: (empty)).

Input: the capacity, then commands. Output: capacity=<N> first, then one line per size, oldest and print.

4
push 1
push 2
push 3
push 4
push 5
print
size
oldest

prints

capacity=4
ring: 2 3 4 5
size=4
oldest=2

A variadic report built from fold expressions

Write template <typename... Ts> void report(Ts... xs) that prints one line: args=<count> [<x1> <x2> …] sum=<s> all_positive=<yes|no>. The count is sizeof...(xs); the values are printed by a fold over the comma operator with a single space between them (and [] for no values); sum is a binary fold (0LL + ... + xs) so that an empty pack sums to 0 and large values do not overflow int; all_positive is the fold (... && (xs > 0)), which is yes for an empty pack. No loops over the pack, no recursion — folds only.

Read n, then n lines of <k> <v1> … <vk> with k from 0 to 4 and integer values. A pack's size is fixed at compile time, so read the values into an array and dispatch on k: report(), report(v[0]), report(v[0], v[1]) and so on — five instantiations of one template.

Input: n, then n lines. Output: one line per input line.

4
3 1 2 3
0
2 -1 5
1 7

prints

args=3 [1 2 3] sum=6 all_positive=yes
args=0 [] sum=0 all_positive=yes
args=2 [-1 5] sum=4 all_positive=no
args=1 [7] sum=7 all_positive=yes

In this module: Templates and generic programming

← Specialisation — when one type needs its own version · Concepts and constraints — saying what T must be →