C++ For C# Developers: Part 26 – Template Parameters
Last time, we started looking at a core feature of C++: templates. We compared and contrasted them to C# generics and saw how they’re applied to classes, functions, lambdas, and even variables. Today we’ll leverage the power of so-called “non-type template parameters” and “template template parameters” to write some really interesting code.
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
Type Template Parameters
All of the examples of templates in the intro article took one parameter:
template<typename T>
That’s often enough to create many templates as we’ve seen from types like C#’s List<T>
and NativeArray<T>
generic classes. Still, many others like Dictionary<TKey, TValue>
require more parameters. Adding these is simple and just like adding function parameters:
// Class template that takes two parameters template<typename TKey, typename TValue> struct Pair { TKey Key; TValue Value; }; // Specify both parameters to instantiate the template Pair<int, float> pair{123, 3.14f}; DebugLog(pair.Key, pair.Value); // 123, 3.14
Also similar to function parameters, but unlike type parameters to C# generics, we can specify default values:
// Second parameter has a default value template<typename TKey, typename TValue=int> struct Pair { TKey Key; TValue Value; }; // Only need to pass one parameter // The second gets the default: int Pair<int> pair1{123, 456}; DebugLog("TValue is int?", typeid(pair1.Value) == typeid(int)); // true DebugLog(pair1.Key, pair1.Value); // 123, 456 // We can still pass two parameters Pair<int, float> pair2{123, 3.14f}; DebugLog("TValue is int?", typeid(pair2.Value) == typeid(int)); // false DebugLog(pair2.Key, pair2.Value); // 123, 3.14
It’s also common to see class
instead of typename
in the template parameters list. There is no difference between the two. Non-class types like int
and float
are perfectly usable with class
template parameters. The choice of which to use is mostly one of style:
// Parameters are "class" instead of "typename" // Behaves the same template<class TKey, class TValue=int> struct Pair { TKey Key; TValue Value; }; // Non-class types are still usable as template arguments Pair<int> pair1{123, 456}; Pair<int, float> pair2{123, 3.14f};
Unlike C#, the names of the parameters are optional, even when they have default values:
// Parameter names omitted, with and without default values template<class, class=int> void DoNothing() { } DoNothing<float>(); DoNothing<float, int>();
The types also don’t need to be defined, only declared, in order to be used to instantiate a template. This works as long as the template doesn’t use the type, similar to if it wasn’t given a name at all:
// Declared, but not defined struct Vector2; // Template that doesn't use its type parameter template<typename T> void DoNothing() { } // OK because Vector2 doesn't actually get used DoNothing<Vector2>();
In the case that a template parameter has the same name as a name outside of the template, there’s no collision as the context makes it clear which one is being referred to:
struct Vector { float X = 0; float Y = 0; }; // Template parameter has the same name as a class outside the template: Vector template<typename Vector> Vector Make() { return Vector{}; } // "int" used as the type named "Vector" auto val = Make<int>(); DebugLog(val); // 0 // "Vector" doesn't refer to the type parameter // The template isn't referenced here auto vec = Vector{}; DebugLog(vec.X, vec.Y); // 0, 0
Template Template Parameters
Consider a Map
class template that holds keys and values via a List<T>
class template:
template<typename T> struct List { // ... implementation similar to C# }; template<typename TKey, typename TValue> struct Map { List<TKey> Keys; List<TValue> Values; }; Map<int, float> map;
The Map
template always uses the List
template. If we wanted to abstract the kind of container that holds the keys and values to make Map
more flexible, we could use a “template template parameter.” This is where we pass a template like List
, not an instantiation of a template like List<T>
, as a parameter to a template:
template<typename T> struct List { // ... implementation similar to C# }; template<typename T> struct FixedList { // ... implementation similar to C# except that it's a fixed size }; // The third parameter is a template, not a type // That template needs to take one type parameter template<typename TKey, typename TValue, template<typename> typename TContainer> struct Map { // Use the template parameter instead of directly using List TContainer<TKey> Keys; TContainer<TValue> Values; }; // Pass List, which is a template taking one type parameter, as the parameter // Do not pass an instantiation of the template like List<int> Map<int, float, List> listMap; // Pass FixedList as the parameter // It also takes one type parameter Map<int, float, FixedList> fixedListMap;
When we do this, the compiler instantiates these two classes for listMap
and fixedListMap
:
struct MapList { List<int> Keys; List<float> Values; }; struct MapFixedList { FixedList<int> Keys; FixedList<float> Values; };
Template template parameters can also have default values:
template< typename TKey, typename TValue, template<typename> typename TKeysContainer=List, template<typename> typename TValuesContainer=List> struct Map { TKeysContainer<TKey> Keys; TValuesContainer<TValue> Values; }; // TKeysContainer=List, TValuesContainer=List Map<int, float> map1; // TKeysContainer=FixedList, TValuesContainer=List Map<int, float, FixedList> map2; // TKeysContainer=FixedList, TValuesContainer=FixedList Map<int, float, FixedList, FixedList> map3;
Non-Type Template Parameters
The third kind of template parameter is known as a “non-type template parameter.” These are compile-time constant values, not the names of types or templates. For example, we can use this to write the FixedList
type backed by an array data member:
// Size is a "non-type template parameter" // A compile-time constant needs to be passed template<typename T, int Size> struct FixedList { // Use Size like any other int T Elements[Size]; T& operator[](int index) { return Elements[index]; } int GetLength() const noexcept { return Size; } }; // Pass 3 for Size FixedList<int, 3> list1; list1[1] = 123; DebugLog(list1[1]); // 123 // Pass 2 for Size FixedList<float, 2> list2; list2[0] = 3.14f; DebugLog(list2[0]); // 3.14
Just like with “type template parameters” and “template template parameters,” the compiler substitutes the value anywhere it’s used when instantiating the template:
struct FixedListInt3 { int Elements[3]; int& operator[](int index) { return Elements[index]; } int GetLength() const noexcept { return 3; } }; struct FixedListFloat2 { float Elements[2]; float& operator[](int index) { return Elements[index]; } int GetLength() const noexcept { return 2; } };
Default values are allowed for non-type template parameters, too:
// Template parameters control the initial capacity and growth factor template<typename T, int InitialCapacity=4, int GrowthFactor=2> class List { // ... implementation };
We can now use these to tune the performance of our List
classes based on expected usage:
// Defaults are acceptable List<int> list1; // Start off with a lot of capacity List<int, 1024> list2; // Don't start with a little capacity, but grow fast List<int, 4, 10> list3; // Start empty and grow by doubling List<int, 0, 2> list4;
The kinds of values we can pass to non-type template parameters is restricted to “structural types.” The first such kind of “structural type” is the one we’ve already seen: integers. We can use any size (short
, long
, etc.) and it doesn’t matter if it’s signed or not (signed
, unsigned
, no specifier). This also includes quasi-integers like char
and the type of nullptr
.
The second kind of values are pointers and lvalue references:
// Takes a pointer and a reference to some type T template<typename T, const T* P, const T& R> constexpr T Sum = *P + R; // A constant array and a constant integer constexpr int a[] = { 100 }; constexpr int b = 23; // The 'a' array "decays" to a pointer // The 'b' integer is an lvalue because it has a name: b constexpr int sum = Sum<int, a, b>; DebugLog(sum); // 123
The third kind is similar: pointers to members.
// A class with two int data members struct Player { int Health = 100; int Armor = 50; }; // Template for a function that gets an int data member // Takes the type of the class and a pointer to one of its int data members template<typename TCombatant, int TCombatant::* pStat> constexpr int GetStat(const TCombatant& combatant) { return combatant.*pStat; } // Get both int data members via the function template and pointers to members Player player; DebugLog(GetStat<Player, &Player::Health>(player)); // 100 DebugLog(GetStat<Player, &Player::Armor>(player)); // 50
Starting in C++20, there are two more kinds. First, floating point types like float
and double
:
template<float MinValue, float MaxValue> float Clamp(float value) { return x > MaxValue ? MaxValue : x < MinValue ? MinValue : value; } DebugLog(Clamp<0, 100>(50)); // 50 DebugLog(Clamp<0, 100>(150)); // 100 DebugLog(Clamp<0, 100>(-50)); // 0
Second, “literal types” we’ve seen before when writing compile-time code:
// As a simple aggregate, this is a "literal type" struct Pixel { int X; int Y; }; // Template taking a "literal type" template<Pixel P> bool IsTopLeft() { return P.X == 0 && P.Y == 0; } // Passing a "literal type" as a template argument DebugLog(IsTopLeft<Pixel{2, 4}>()); // false DebugLog(IsTopLeft<Pixel{0, 0}>()); // true
Regardless of language version, there are some additional restrictions on the kinds of expressions we can pass as a template argument. First, we can’t pass pointers or references to sub-objects such as base classes and array elements:
template<const int& X> constexpr int ValOfTemplateParam = X; constexpr int a[] = { 100 }; // Compiler error: can't reference sub-object of a as non-type template param constexpr int val = ValOfTemplateParam<a[0]>;
Temporary objects also can’t be passed:
// Compiler error: can't pass a temporary object constexpr int val = ValOfTemplateParam<123>;
Nor can string literals:
template<const char* str> void Print() { DebugLog("Letter:", *str); } constexpr char c = 'A'; // Compiler error: can't pass a string literal Print<"hi">(); Print<&c>(); // Letter: A
And finally, whatever type typeid
returns can’t be passed as a template argument:
template<decltype(typeid(char)) tid> void PrintTypeName() { DebugLog(tid.name()); } // Compiler error: can't pass what typeid evaluates to PrintTypeName<typeid(char)>();
While they’re not strictly prohibited, it’s important to know that arrays in template parameters are implicitly converted to pointers. This can have some important consequences:
// Array parameter is automatically transformed to a pointer template<const int X[]> constexpr void PrintSizeOfArray() { // Bug: prints the size of a pointer, not the size of the array DebugLog(sizeof(X)); } constexpr int32_t arr[3] = { 100, 200, 300 }; // Bug PrintSizeOfArray<arr>(); // 8 (on 64-bit CPUs) // OK DebugLog(sizeof(arr)); // 12
Ambiguity
There are a few cases that can arise where template parameters appear ambiguous. Similar to operator precedence, there are clear rules that determine how the compiler disambiguates template parameters. These cases usually don’t arise as programmers make good choices with naming, but it’s important to know the rules to be able to figure out what the compiler is doing in edge cases.
The first case happens when a member template is declared outside the class with a parameter that has the same name as a member of the class it’s a member of. In this case, the member of the class is used instead of the template parameter:
// Class template with one type parameter: T template<class T> struct MyClass { // Member class struct Thing { }; // Member function declaration, not definition int GetSizeOfThing(); }; // Member function definition outside the class // Uses 'Thing' instead of 'T' as the class' type parameter name // 'Thing' is the same name as the member class 'Thing' template<class Thing> int MyClass<Thing>::GetSizeOfThing() { // 'Thing' refers to the member class, not the type parameter return sizeof(Thing); } // Instantiate the class template with T=double MyClass<double> mc{}; // Call the member function on a MyClass<double> // Returns the size of the member class: 1 for an empty struct DebugLog(mc.GetSizeOfThing()); // 1, not 8
The second case also happens when a member of a class template is defined outside the class template. Specifically, it only happens when the name of a parameter is the same as the name of a member of the namespace the class is in. In this case, we get the opposite: the type parameter is used instead of the namespace member.
namespace MyNamespace { // Class member of the namespace class Thing { }; // Class template with one type parameter: T template<class T> struct MyClass { // Member function declaration, not definition int GetSizeOfThing(T thing); }; } // Member function definition outside the class // Uses 'Thing' instead of 'T' as the class' type parameter name // 'Thing' is the same name as the namespace member class 'Thing' // 'Thing' is used as the type of a parameter to the function template<class Thing> int MyNamespace::MyClass<Thing>::GetSizeOfThing(Thing thing) { // 'Thing' refers to the type parameter, not the namespace member return sizeof(Thing); } // Instantiate the class template with T=double MyNamespace::MyClass<double> mc{}; // Call the member function on a MyClass<double> // Returns the size of the type parameter: 8 for double DebugLog(mc.GetSizeOfThing({})); // 8, not 1
The third case is when a class template’s parameter has the same name as a member of one of its base classes. In this case, the ambiguity goes to the base class’ member:
struct BaseClass { struct Thing { }; }; // Class template with one type parameter: Thing // 'Thing' is the same name as the base class' member class 'Thing' template<class Thing> struct DerivedClass : BaseClass { // 'Thing' refers to the base class' member class, not the type parameter int Size = sizeof(Thing); }; // Instantiate the class template with Thing=double DerivedClass<double> dc; // See how big 'Thing' was when initializing 'Size' // It's the size of BaseClass::Thing: 1 for an empty struct DebugLog(dc.Size); // 1, not 8
Unlike the first two cases, this case is possible in C# as well. Unlike C++, Thing
refers to the type parameter, not the base class member:
// C# public class BaseClass { public struct Thing { }; }; // Generic class with one type parameter: Thing // 'Thing' is the same name as the base class' member class 'Thing' public class DerivedClass<Thing> : BaseClass { // 'Thing' refers to the type parameter, not base class' member class public Type ThingType = typeof(Thing); }; // Instantiate the generic class with Thing=double DerivedClass<double> dc = new DerivedClass<double>(); // See what type 'Thing' was when initializing 'ThingType' // It's the type parameter 'double', not BaseClass.Thing DebugLog(dc.ThingType); // System.Double
Conclusion
C# generics provide support for type parameters, but not the non-type parameters and template parameters that C++ templates provide support for. Even so, C++ type parameters include additional functionality such as support for default arguments and omitting the name of the parameter.
Template parameters allow for more generic code by using a template like Container
as a variable rather than a specific template like List<T>
. Non-type template parameters allow passing compile-time constant expressions so we can use values, not types, in our templates. This allows us to create class templates like FixedList<T>
with static sizes to avoid dynamic allocation and the cost of dynamic resizing when we don’t need it or to tune the allocation strategy of a List<T>
when we do need dynamic resizing.
There’s a lot more about templates for us to explore. Next week we’ll get into template argument deduction that allows us to write Foo(3.14f)
instead of Foo<float>(3.14f)
and template specialization where we can write custom versions of templates (Vector<2>
) instead of relying on a generalized version (Vector<int NumDimensions>
). Stay tuned!
#1 by Hackbeam on February 22nd, 2023 ·
Hi Jackson!
I’ve love your articles. I’m learning a lot and refreshing concepts from my days at university.
Thank you so much for your time to write this amazing series.
I’ve found a typo at the beggining of the article:
— It’s also common to see class instead of “template” in the template parameters list
I think you mean “instead of typename”
Again, nice work and thank you!
#2 by jackson on February 22nd, 2023 ·
I’m glad you’re enjoying them! Thanks for letting me know about the typo. I’ve updated the article with a fix.