#093 – Using non-type template parameters

Most template examples use types as template arguments:

template <typename T>
T clamp(T value, T lower, T upper);

Here, T is a placeholder for a type such as intdouble, or std::string.

However, when we create a std::bitset, we specify the number of bits in <> as a literal, not a type. This is the template argument. However, in this context, we are passing in a literal value rather than a fundamental type such as int or double (like how we usually do).

What is occurring is that we are using a non-type template parameter. This is different from template parameters which serve as placeholders for template arguments.

#include <bitset>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 }; // The <8> is a non-type template parameter
}

non-type template parameter is a template parameter that represents a constant value.

It is treated as a constant value and can therefore be used anywhere a constant expression is required (e.g., specifying the size of a local array).

Non-type template parameters are used primarily when we need to pass constexpr values to functions (or class types) so they can be used in contexts that require a constant expression.

A non-type template parameter supports the following types:

  • An integral type (e.g. int, bool, char)
  • An enumeration type
  • std::nullptr_t
  • A floating point type (since C++20)
  • Pointers or lvalue references to objects/functions
  • auto (C++17).

Defining our own non-type template parameters

A non-type template parameter is declared inside the template parameter list, exactly like a type template parameter.

The difference is that it has a value type, such as int:

#include <iostream>

template <int N> // declare a non-type template parameter of type int named N
void print()
{
    std::cout << N << '\n'; // use value of N here
}

int main()
{
    print<5>(); // 5 is our non-type template argument
}

// terminal 
5

NOTE: Unlike class templates, function templates generally do not allow default template arguments in their declarations

Non-type parameters can be used where constants are required

Because N is known at compile time, it can be used in places that require a constant expression.

For example:

#include <iostream>

template <int Size>
void printBufferSize(){
    int buffer[Size] {};

    std::cout << "Buffer has " << Size << " elements\n";
    std::cout << "Total bytes: " << sizeof(buffer) << '\n';
}

int main(){
    printBufferSize<8>();
}

// Output
Buffer has 8 elements
Total bytes: 32

Assuming int is 4 bytes, the array occupies 32 bytes.

The key point is that this works because Size is known during compilation:

int buffer[Size] {};

Practical example: array references

Non-type template parameters are useful when working with arrays because the size of a built-in array is part of its type.

For example:

const char hello[] { "hi" };

This array has three elements:

'h', 'i', '\0'

The null terminator counts as an element, so "hi" has size 3.

Likewise:

"mom"

has size 4:

'm', 'o' ,'m', '\0'

We can write a function template that captures those sizes automatically:

#include <cstring>
#include <iostream>

template <std::size_t N, std::size_t M>
int compare(const char (&lhs)[N], const char (&rhs)[M]){
    std::cout << "lhs size: " << N << '\n';
    std::cout << "rhs size: " << M << '\n';

    return std::strcmp(lhs, rhs);
}

int main(){
    int result { compare("hi", "mom") };

    std::cout << "Comparison result: " << result << '\n';
}

// Output
lhs size: 3
rhs size: 4
Comparison result: -5

The exact comparison result varieson the character values, but the sizes are the important part.

The call:

compare("hi", "mom");

causes the compiler to instantiate:

compare<3, 4>("hi", "mom");

Why?

Because "hi" is a const char[3], and "mom" is a const char[4]

Instantiating different versions

Each different pair of array sizes creates a different specialization.

#include <cstring>
#include <iostream>

template <std::size_t N, std::size_t M>
int compare(const char (&lhs)[N], const char (&rhs)[M]){
    std::cout << "compare<" << N << ", " << M << ">\n";
    return std::strcmp(lhs, rhs);
}

int main(){
    compare("hi", "mom");      // compare<3, 4>
    compare("cat", "dog");     // compare<4, 4>
    compare("C++", "Python");  // compare<4, 7>
}

// Output
compare<3, 4>
compare<4, 4>
compare<4, 7>

The compiler deduces N and M from the array arguments.

In Conclusion

A non-type template parameter is a compile-time value passed into a template.

It is what makes types such as std::bitset<8> possible, and it is also useful when functions need compile-time values such as array sizes.