All of the templates we’ve written so far had a fixed number of parameters, but C++ lets us take a variable number of parameters too. This is like params in C# functions, but for parameters to C++ templates. Today we’ll dig into this feature, which has no C# equivalent, and learn how to write and use templates with any number of parameters.

Table of Contents

Parameter Packs

A “variadic template” is one that has a “parameter pack.” A parameter pack represents zero or more parameters, just like params to a C# function represents an array of zero or more parameters.

Here’s a variadic function template that includes one parameter pack:

template<typename ...TArgs>
void LogAll(TArgs... args)
{
}

TArgs is a parameter pack because it has ... before the (optional) parameter name: TArgs. It’s a parameter pack of type parameters because it starts with typename.

To use the parameter pack, we add ... after the name of the parameter: TArgs.... The compiler expands this to a comma-delimited list of the arguments.

Let’s look at some instantiations of this template to see how this expansion works:

// Zero arguments to the TArgs parameter pack
LogAll();
void LogAll() {}
 
// One argument to the TArgs parameter pack
LogAll<int>(123);
void LogAll(int) {}
 
// One argument to the TArgs parameter pack (with deduction)
LogAll(123);
void LogAll(int) {}
 
// Two arguments to the TArgs parameter pack
LogAll(123, 3.14f);
void LogAll(int, float) {}

We’re free to mix parameter packs with other template parameters:

template<typename TPrefix, typename ...TArgs>
void LogAll(TPrefix prefix, TArgs... args)
{
}

Unlike C# params, the parameter pack doesn’t even have to be the last parameter as long as the compiler can deduce all the parameters:

// Parameter pack is not the last parameter
template<typename ...TLogParts, typename TPrefix>
void LogWithPrefix(TPrefix prefix, TLogParts... parts)
{
}
 
// Compiler deduces that TPrefix is 'float' and TLogParts is (int, int, int)
LogWithPrefix(3.14f, 123, 456, 789);

Note that the compiler can never deduce this with class templates, so the parameter pack must come at the end.

Pack Expansion

Now that we know how to declare packs of template parameters and how to use them in function parameters, let’s look at some more ways to use them. One common way is to pass them as function arguments:

template<typename ...TArgs> // Template parameter pack
void LogError(TArgs... args) // Use parameter pack to declare parameters
{
    DebugLog("ERROR", args...); // Pass parameters as arguments to a function
}
 
// Pass arguments to function template
// Template arguments deduced from parameter types
LogError(3.14, 123, 456, 789); // ERROR, 3.14, 123, 456, 789
 
// The compiler instantiates this function
void LogError(double arg1, int arg2, int arg3, int arg4)
{
    DebugLog("ERROR", arg1, arg2, arg3, arg4);
}

In this example we passed the arguments straight through as args.... This was expanded to arg1, arg2, arg3, arg4. If we apply some operation to the parameter pack name, it’ll be applied to all of the arguments:

template<typename ...TArgs>
void LogPointers(TArgs... args)
{
    // Apply dereferencing to each value in the pack
    DebugLog(*args...);
}
 
// Pass pointers
float f = 3.14f;
int i1 = 123;
int i2 = 456;
LogPointers(&f, &i1, &i2); // 3.14, 123, 456
 
// The compiler instantiates this function
void LogPointers(float* arg1, int* arg2, int* arg3)
{
    DebugLog(*arg1, *arg2, *arg3);
}

If we name more than one parameter pack in the same expansion, they get expanded simultaneously:

// Class template with two parameters
template<typename T1, typename T2>
struct KeyValue
{
    T1 Key;
    T2 Value;
};
 
// Class template with a parameter pack
template<typename ...Types>
struct Map
{
    // ...implementation
};
 
// Class template with a parameter pack
template<class ...Keys>
struct MapOf
{
    // Member class template with a parameter pack
    template<class ...Values>
    // Derives from Map class template
    // Pass KeyValue<Keys, Values>... as the template arguments to Map
    // Expands to (KeyValue<Keys1, Values1>, KeyValue<Keys2, Values2>, etc.)
    struct KeyValues : Map<KeyValue<Keys, Values>...>
    {
    };
};
 
// Instantiate the template with Keys=(int, float) and Values=(double, bool)
// Pairs derives from Map<KeyValue<int, double>, KeyValue<float, bool>>
MapOf<int, float>::KeyValues<double, bool> map;
 
// The compiler instantiates this class
struct MapOf
{
    struct KeyValues : Map<KeyValue<int, double>, KeyValue<float, bool>>
    {
    };
};
Where Packs Can Be Expanded

So far we’ve seen packs expanded into function parameters, function arguments, and template arguments. There are quite a few more places they can be expanded. First, when initializing with parentheses:

struct Pixel
{
    int X;
    int Y;
 
    Pixel(int x, int y)
        : X(x), Y(y)
    {
    }
};
 
// Function template takes a parameter pack of ints
template<int ...Components>
Pixel MakePixel()
{
    // Expand into parentheses initialization
    return Pixel(Components...);
};
 
Pixel pixel = MakePixel<2, 4>();
DebugLog(pixel.X, pixel.Y); // 2, 4

Or initializing with curly braces:

// Function template takes a parameter pack of ints
template<int ...Components>
Pixel MakePixel()
{
    // Expand into curly braces initialization
    return Pixel{Components...};
};

Second, we can expand type parameter packs into packs of non-type parameters:

// Class template with a pack of type parameters
template<typename... Types>
struct TypedPrinter
{
    // Function template with a pack of non-type parameters
    // Formed from the expansion of the Types pack
    template<Types... Values>
    static void Print()
    {
        // Expand the non-type parameters pack
        DebugLog(Values...);
    }
};
 
// Instantiate the templates with type and non-type parameters
TypedPrinter<char, int>::Print<'c', 123>(); // c, 123
 
// Compiler error: 'c' is not a bool
TypedPrinter<bool, int>::Print<'c', 123>();

Third, a class can inherit from zero or more base classes by expanding a pack of types:

struct VitalityComponent
{
    int Health;
    int Armor;
};
 
struct WeaponComponent
{
    float Range;
    int Damage;
};
 
struct SpeedComponent
{
    float Speed;
};
 
template<class... TComponents>
// Expand a pack of base classes
class GameEntity : public TComponents...
{
};
 
// turret is a class that derives from VitalityComponent and WeaponComponent
GameEntity<VitalityComponent, WeaponComponent> turret;
turret.Health = 100;
turret.Armor = 200;
turret.Range = 10;
turret.Damage = 15;
 
// civilian is a class that derives from VitalityComponent and SpeedComponent
GameEntity<VitalityComponent, SpeedComponent> civilian;
civilian.Health = 100;
civilian.Armor = 200;
civilian.Speed = 2;

Fourth, the list of a lambda’s captures can be formed by pack expansion:

template<class ...Args>
void Print(Args... args)
{
    // Expand the 'args' pack into the lambda capture list
    auto lambda = [args...] { DebugLog(args...); };
    lambda();
}
 
Print(123, 456, 789); // 123, 456, 789

Fifth, the sizeof operator has a variant that takes a parameter pack. This evaluates to the number of elements in the pack, regardless of their sizes:

// General form of summation
// Declaration only since it's never actually instantiated
template<typename ...TValues>
int Sum(TValues... values);
 
// Specialization for when there is at least one value
template<typename TFirstValue, typename ...TValues>
int Sum(TFirstValue firstValue, TValues... values)
{
    // Expand pack into a recursive call
    return firstValue + Sum(values...);
}
 
// Specialization for when there are no values
template<>
int Sum()
{
    return 0;
}
 
template<typename ...TValues>
int Average(TValues... values)
{
    // Expand pack into a Sum call
    // Use sizeof... to count the number of parameters in the pack
    return Sum(values...) / sizeof...(TValues);
}
 
DebugLog(Average(10, 20)); // 15

In this example, the compiler instantiates these templates:

// Instantiated for Sum(10, 20)
int Sum2(int firstValue, int value)
{
    // Expand pack into a recursive call
    return firstValue + Sum1(value);
}
 
// Instantiated for Sum(20)
int Sum1(int firstValue)
{
    return firstValue + Sum0();
}
 
// Instantiated for Sum()
int Sum0()
{
    return 0;
}
 
int Average(int value1, int value2)
{
    return Sum2(value1, value2) / 2;
}
 
DebugLog(Average(10, 20)); // 15

Compiler optimizations will almost always boil this down to a constant:

DebugLog(15); // 15

Or for arguments x and y that aren’t compile-time constants:

DebugLog((x + y) / 2);
Conclusion

Variadic templates enable us to write templates based on arbitrary numbers of parameters. This saves us from needing to write nearly-identical versions of the same templates over and over. For example, C# has Action<T>, Action<T1,T2>, Action<T1,T2,T3>, all the way up to Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16>! The same massive duplication is applied to its Func counterpart: Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>. This is so painful to write that we usually just don’t bother or write a code generator to output all this redundant C#. At no point do we end up with a solution that takes arbitrary numbers of parameters, just arbitrary enough for now numbers of parameters.