Skip to main content

C++ Non-type Template Parameters

Introduction

When working with C++ templates, you've likely seen examples like template<typename T> where T is a type parameter. However, C++ templates are more powerful than that - they also support non-type template parameters, which allow you to parameterize templates with actual values rather than just types.

Non-type template parameters are compile-time constants that can be used to customize your templates based on specific values known at compile time. This powerful feature enables you to create more specialized and efficient code without runtime overhead.

In this tutorial, you'll learn:

  • What non-type template parameters are
  • How to use them in your code
  • Common applications and practical examples
  • Advanced techniques and limitations

What Are Non-type Template Parameters?

Non-type template parameters are constant values that serve as parameters to templates. Unlike type parameters (specified with typename or class), non-type parameters represent actual values like integers, pointers, or references.

Here's the basic syntax:

cpp
template<type-name parameter-name>
class ClassName {
// Implementation
};

Where type-name can be:

  • Integer types (like int, long, char)
  • Enumeration types
  • Pointer types
  • Reference types
  • Nullptr type (std::nullptr_t)
  • Member function pointers
  • (Since C++20) Floating-point types and literal class types with certain constraints

Basic Examples

Example 1: Array with Compile-time Size

Let's start with a classic example - an array with a size defined at compile time:

cpp
#include <iostream>
#include <array>

template<typename T, size_t Size>
class FixedArray {
private:
T data[Size];
public:
T& operator[](size_t index) {
return data[index];
}

const T& operator[](size_t index) const {
return data[index];
}

size_t size() const {
return Size;
}
};

int main() {
// Create a fixed array of 5 integers
FixedArray<int, 5> numbers;

// Initialize array elements
for (size_t i = 0; i < numbers.size(); ++i) {
numbers[i] = i * 10;
}

// Print array elements
for (size_t i = 0; i < numbers.size(); ++i) {
std::cout << "numbers[" << i << "] = " << numbers[i] << std::endl;
}

// This would cause a compile-time error:
// FixedArray<int, -1> negativeArray; // Size cannot be negative

return 0;
}

Output:

numbers[0] = 0
numbers[1] = 10
numbers[2] = 20
numbers[3] = 30
numbers[4] = 40

In this example, Size is a non-type template parameter that determines the array's size at compile time. This approach has several advantages over dynamic arrays:

  1. No runtime allocation overhead
  2. Size errors caught at compile time
  3. Potential for better optimization by the compiler

Example 2: Compile-time Power Function

Non-type parameters are excellent for compile-time computations:

cpp
#include <iostream>

// Compile-time power function using non-type template parameters
template<typename T, int exponent>
class Power {
public:
static T calculate(T base) {
if constexpr (exponent < 0) {
return 1 / Power<T, -exponent>::calculate(base);
} else if constexpr (exponent == 0) {
return 1;
} else {
return base * Power<T, exponent - 1>::calculate(base);
}
}
};

int main() {
// Calculate powers at compile time
std::cout << "2^3 = " << Power<int, 3>::calculate(2) << std::endl;
std::cout << "2^8 = " << Power<int, 8>::calculate(2) << std::endl;
std::cout << "5^2 = " << Power<int, 2>::calculate(5) << std::endl;
std::cout << "3^0 = " << Power<int, 0>::calculate(3) << std::endl;
std::cout << "2^-2 = " << Power<double, -2>::calculate(2) << std::endl;

return 0;
}

Output:

2^3 = 8
2^8 = 256
5^2 = 25
3^0 = 1
2^-2 = 0.25

This example shows how non-type parameters can be used for compile-time recursion. The computation happens during compilation, resulting in efficient runtime code.

Practical Applications

Compile-time String Hashing

Non-type parameters are useful for generating hash values at compile time:

cpp
#include <iostream>
#include <string_view>

// Compile-time string hash function
template<std::string_view str>
struct StringHash {
static constexpr std::size_t value = [] {
std::size_t hash = 0;
for (char c : str) {
hash = hash * 131 + c;
}
return hash;
}();
};

// Since C++20, we can use string literals directly as template parameters
int main() {
constexpr auto hash1 = StringHash<"hello">::value;
constexpr auto hash2 = StringHash<"world">::value;

std::cout << "Hash of 'hello': " << hash1 << std::endl;
std::cout << "Hash of 'world': " << hash2 << std::endl;

// Use in a switch statement
constexpr std::string_view input = "hello";
switch (StringHash<input>::value) {
case StringHash<"hello">::value:
std::cout << "Input is 'hello'" << std::endl;
break;
case StringHash<"world">::value:
std::cout << "Input is 'world'" << std::endl;
break;
default:
std::cout << "Unknown input" << std::endl;
}

return 0;
}
note

This example requires C++20 support for string literals as template parameters.

Fixed-point Math

Non-type parameters can be used to implement fixed-point arithmetic for embedded systems:

cpp
#include <iostream>
#include <cstdint>

// A fixed-point number with a configurable number of fractional bits
template<typename T, unsigned int FractionalBits>
class FixedPoint {
private:
T value;

public:
FixedPoint() : value(0) {}

// Convert from floating point
explicit FixedPoint(double v) : value(static_cast<T>(v * (1 << FractionalBits))) {}

// Convert back to floating point
double toDouble() const {
return static_cast<double>(value) / (1 << FractionalBits);
}

// Addition
FixedPoint operator+(const FixedPoint& other) const {
FixedPoint result;
result.value = value + other.value;
return result;
}

// Multiplication
FixedPoint operator*(const FixedPoint& other) const {
FixedPoint result;
result.value = static_cast<T>((static_cast<int64_t>(value) * other.value) >> FractionalBits);
return result;
}
};

int main() {
// Fixed point with 8 fractional bits (using int32_t)
using Fixed8 = FixedPoint<int32_t, 8>;

Fixed8 a(3.14);
Fixed8 b(2.5);

Fixed8 sum = a + b;
Fixed8 product = a * b;

std::cout << "a = " << a.toDouble() << std::endl;
std::cout << "b = " << b.toDouble() << std::endl;
std::cout << "a + b = " << sum.toDouble() << std::endl;
std::cout << "a * b = " << product.toDouble() << std::endl;

// Different precision (16 fractional bits)
using Fixed16 = FixedPoint<int32_t, 16>;
Fixed16 c(3.14159);
std::cout << "Higher precision: " << c.toDouble() << std::endl;

return 0;
}

Output:

a = 3.13672
b = 2.5
a + b = 5.63672
a * b = 7.84375
Higher precision: 3.14159

This implementation uses the non-type parameter FractionalBits to determine the fixed-point number's precision at compile time.

Advanced Techniques

Variadic Non-type Parameters

Since C++11, you can use variadic templates with non-type parameters:

cpp
#include <iostream>
#include <utility>

// Sum of integers at compile time
template<int... Values>
struct Sum;

template<>
struct Sum<> {
static constexpr int value = 0;
};

template<int First, int... Rest>
struct Sum<First, Rest...> {
static constexpr int value = First + Sum<Rest...>::value;
};

// Compile-time integer sequence
template<int... Values>
void printSequence() {
std::cout << "Sequence sum: " << Sum<Values...>::value << std::endl;
std::cout << "Values: ";
((std::cout << Values << " "), ...); // Fold expression (C++17)
std::cout << std::endl;
}

int main() {
printSequence<1, 2, 3, 4, 5>();
printSequence<10, 20, 30>();
printSequence<-5, 5>();

return 0;
}

Output:

Sequence sum: 15
Values: 1 2 3 4 5
Sequence sum: 60
Values: 10 20 30
Sequence sum: 0
Values: -5 5

Auto Non-type Parameters (C++17)

C++17 introduced auto as a type specifier for non-type template parameters:

cpp
#include <iostream>

// Template that can accept different types of non-type parameters
template<auto Value>
struct ValueHolder {
static constexpr auto value = Value;
using type = decltype(Value);
};

int main() {
// Integer
constexpr auto intHolder = ValueHolder<42>::value;
std::cout << "Integer value: " << intHolder << std::endl;

// Character
constexpr auto charHolder = ValueHolder<'A'>::value;
std::cout << "Character value: " << charHolder << std::endl;

// Boolean
constexpr auto boolHolder = ValueHolder<true>::value;
std::cout << "Boolean value: " << std::boolalpha << boolHolder << std::endl;

// Enum
enum Color { Red, Green, Blue };
constexpr auto enumHolder = ValueHolder<Red>::value;
std::cout << "Enum value: " << enumHolder << std::endl;

return 0;
}

Output:

Integer value: 42
Character value: A
Boolean value: true
Enum value: 0

Class Types as Non-type Parameters (C++20)

C++20 allows using certain class types as non-type template parameters:

cpp
#include <iostream>
#include <string>
#include <compare> // For <=> operator

// A class that can be used as a non-type template parameter in C++20
struct Point {
int x;
int y;

// Requirements for class type non-type template parameters:
// 1. All non-static data members must be public
// 2. The class must have a defaulted or deleted operator<=>
// 3. The destructor must be trivial

constexpr Point(int x, int y) : x(x), y(y) {}

// Required for C++20 class type non-type template parameters
auto operator<=>(const Point&) const = default;
};

template<Point p>
class PointTemplate {
public:
void printPoint() const {
std::cout << "Template instantiated with Point(" << p.x << ", " << p.y << ")" << std::endl;
}

Point getPoint() const {
return p;
}
};

int main() {
constexpr Point p1{10, 20};
constexpr Point p2{30, 40};

PointTemplate<p1> t1;
PointTemplate<p2> t2;

t1.printPoint();
t2.printPoint();

return 0;
}

Output:

Template instantiated with Point(10, 20)
Template instantiated with Point(30, 40)

Limitations and Best Practices

Type Restrictions

Not all types can be used as non-type template parameters:

  1. Prior to C++20: Only integral types, enumeration types, pointers, references, and nullptr_t
  2. C++20 adds: Floating-point types and literal class types (with restrictions)

Performance Considerations

Non-type template parameters can improve performance:

  1. Compile-time computations: Calculations can be performed during compilation
  2. No runtime overhead: Values are built into the code rather than passed at runtime
  3. Optimization opportunities: Compiler has more information for optimizations

However, they can also increase compilation time and code size due to template instantiation.

When to Use Non-type Parameters

Use non-type template parameters when:

  1. You need compile-time constants that affect the template implementation
  2. The value is known at compile time and won't change
  3. You're implementing compile-time algorithms or optimizations

Avoid when:

  1. The value frequently changes at runtime
  2. It leads to excessive template instantiations
  3. Runtime polymorphism would be more appropriate

Summary

Non-type template parameters are a powerful feature of C++ templates that allow you to parameterize your code with actual values rather than just types. They enable:

  • Compile-time computations
  • Fixed-size containers with no runtime overhead
  • Highly optimized code specialized for specific values
  • Advanced metaprogramming techniques

With C++20, the capabilities of non-type template parameters have expanded further, allowing for more sophisticated compile-time programming.

Exercises

  1. Create a template class Matrix<T, Rows, Cols> that represents a fixed-size matrix.
  2. Implement a compile-time factorial function using non-type template parameters.
  3. Design a compile-time prime number checker using template specialization.
  4. Create a fixed-size, type-safe circular buffer using non-type parameters.
  5. Implement a compile-time binary-to-decimal converter that works with a non-type parameter representing a binary number.

Additional Resources

Happy coding with C++ non-type template parameters!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)