C++ For C# Developers: Part 28 – Variadic Templates
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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.
#1 by tman on March 16th, 2021 ·
typo:arguements
#2 by jackson on March 16th, 2021 ·
Thanks for pointing this out. I’ve updated the article to fix the typo.