Skip to content
Theory · Templates · lesson 9 of 11

Variadic templates

The function double average(double a, double b) computes the average of two numbers. Three numbers would need one more overload, and five numbers another one. A template with an ellipsis after typename takes any number of arguments of any types:

C++
#include <iostream>

template <typename... Ts>
double average(Ts... xs) {
    return (xs + ... + 0.0) / sizeof...(Ts);
}

int main() {
    std::cout << average(1, 2) << ' ' << average(1, 2.5, 4) << '\n';  // 1.5 2.5
}

Parameter pack

Such a template is called a variadic template. The notation typename... Ts declares a parameter pack: not one type but a list of types of any length, empty included. In the function's parameter list, Ts... xs is a pack of values, one for each type in Ts. For the call average(1, 2.5, 4), the compiler deduces Ts as int, double, int, and xs holds three values. If the body never uses the values, the name may be left out in a definition too: Ts....

For each set of types, the compiler instantiates a separate function: average(1, 2) and average(1, 2.5, 4) call two different functions.

sizeof...(Ts) is the number of elements in the pack, not a size in bytes as with sizeof. For average(1, 2.5, 4) it is 3, and for an empty pack it is 0. The value is known at compile time.

Fold expression

A pack cannot be walked with a loop: its elements may have different types. Instead of a loop you write a fold expression: the pack, an operator and an ellipsis in required parentheses. For three values, (xs + ... + 0.0) expands to x1 + (x2 + (x3 + 0.0)), where x1, x2 and x3 are the elements of xs. The last operand 0.0 is the initial value: for an empty pack, the expression equals it.

The initial value also affects the type of the sum. With 0, the sum of two int values would stay an int, and average(1, 2) would return 1: dividing integers drops the fraction. With 0.0, the sum has the type double, and the result is 1.5. So the initial value takes the type the sum needs: for joining text it is an empty std::string{}, since two quoted texts cannot be added with +.

Return type auto

If a template's return type is not known in advance, auto takes its place. The compiler deduces the type from return, as with a lambda: in template <typename... Ts> auto sum(Ts... xs) { return (xs + ... + 0); }, for sum(1, 2) the result is an int, for sum(1, 2.5) a double.

Common mistake

The fold is written without an initial value:

C++
#include <iostream>

template <typename... Ts>
double total(Ts... xs) {
    return (xs + ...);  // ✗
}

int main() {
    std::cout << total(1, 2) << total() << '\n';
}
TEXT
error: unary fold expression has empty expansion for operator '+' with no fallback value

For total(1, 2) such a fold works, but for total() the pack is empty and there is nothing to add. The compiler reports the error when it instantiates total(). Add an initial value: (xs + ... + 0.0).

Key points

  • typename... Ts declares a pack of types, and Ts... xs a pack of values of any length.
  • sizeof...(Ts) is the number of elements in the pack, known at compile time.
  • The fold expression (xs + ... + 0.0) adds up a pack without a loop, and the initial value sets the type of the sum.
  • Without an initial value, a + fold over an empty pack does not compile.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way