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:
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:
#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:
- No runtime allocation overhead
- Size errors caught at compile time
- Potential for better optimization by the compiler
Example 2: Compile-time Power Function
Non-type parameters are excellent for compile-time computations:
#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:
#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;
}
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:
#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:
#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:
#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:
#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:
- Prior to C++20: Only integral types, enumeration types, pointers, references, and
nullptr_t
- C++20 adds: Floating-point types and literal class types (with restrictions)
Performance Considerations
Non-type template parameters can improve performance:
- Compile-time computations: Calculations can be performed during compilation
- No runtime overhead: Values are built into the code rather than passed at runtime
- 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:
- You need compile-time constants that affect the template implementation
- The value is known at compile time and won't change
- You're implementing compile-time algorithms or optimizations
Avoid when:
- The value frequently changes at runtime
- It leads to excessive template instantiations
- 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
- Create a template class
Matrix<T, Rows, Cols>
that represents a fixed-size matrix. - Implement a compile-time factorial function using non-type template parameters.
- Design a compile-time prime number checker using template specialization.
- Create a fixed-size, type-safe circular buffer using non-type parameters.
- Implement a compile-time binary-to-decimal converter that works with a non-type parameter representing a binary number.
Additional Resources
- C++ Reference: Template Parameters
- Effective Modern C++ by Scott Meyers
- C++ Templates: The Complete Guide by David Vandevoorde, Nicolai M. Josuttis, and Douglas Gregor
- C++20: The Complete Guide by Nicolai M. Josuttis
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! :)