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

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 template 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!