Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Value-Based Template Parameters
Use compile-time constant values as template arguments.
Template parameters aren't limited to types. C++ templates can also accept values as parameters, known as non-type parameters. While type parameters let you parameterize which types a template uses, non-type parameters let you parameterize constant values that configure template behavior.
What are non-type parameters?
A non-type parameter is a template parameter with a predefined type that accepts a compile-time constant value. The value becomes part of the template's identity, allowing the compiler to optimize and specialize code based on that value.
Non-type parameters can be:
- Integral types (int, long, char, bool, etc.)
- Enumeration types
- Pointers or references to objects
- Pointers or references to functions
- Pointers to member functions
- std::nullptr_t
- Floating-point types (since C++20)
Fixed-size buffer with non-type parameters
Consider a scenario where you want a buffer with compile-time fixed capacity. Using a non-type parameter, you can specify the size at compile time:
#include <iostream>
template <typename T, int Capacity>
class FixedBuffer
{
private:
T m_data[Capacity]{};
int m_count{0};
public:
void add(const T& value)
{
if (m_count < Capacity)
m_data[m_count++] = value;
}
const T& get(int index) const
{
return m_data[index];
}
T& operator[](int index)
{
return m_data[index];
}
int size() const { return m_count; }
int capacity() const { return Capacity; }
};
int main()
{
FixedBuffer<int, 8> numbers;
for (int i{0}; i < 8; ++i)
numbers.add(i * 5);
std::cout << "Buffer contents: ";
for (int i{0}; i < numbers.size(); ++i)
std::cout << numbers.get(i) << ' ';
std::cout << '\n';
std::cout << "Capacity: " << numbers.capacity() << '\n';
return 0;
}
Output:
Buffer contents: 0 5 10 15 20 25 30 35
Capacity: 8
In this example, Capacity is a non-type parameter of type int. When you create FixedBuffer<int, 8>, the compiler replaces Capacity with 8, making m_data an array of size 8. Since the size is known at compile time, the array can be allocated on the stack rather than requiring dynamic allocation.
Why use non-type parameters?
Non-type parameters provide several advantages:
1. No dynamic allocation needed
Since the size is known at compile time, you can use stack-allocated arrays:
template <typename T, int Size>
class CircularQueue
{
private:
T m_buffer[Size]{}; // Stack allocation, no new/delete needed
int m_head{0};
int m_tail{0};
int m_count{0};
public:
void enqueue(const T& item)
{
if (m_count < Size)
{
m_buffer[m_tail] = item;
m_tail = (m_tail + 1) % Size;
++m_count;
}
}
T dequeue()
{
T value{m_buffer[m_head]};
m_head = (m_head + 1) % Size;
--m_count;
return value;
}
int size() const { return m_count; }
bool isEmpty() const { return m_count == 0; }
bool isFull() const { return m_count == Size; }
};
2. Better performance
Compile-time constants enable optimizations that runtime values don't:
int main()
{
CircularQueue<double, 4> temps;
temps.enqueue(98.6);
temps.enqueue(99.1);
temps.enqueue(98.8);
while (!temps.isEmpty())
std::cout << temps.dequeue() << '\n';
return 0;
}
The compiler knows the exact size and can optimize array access without bounds checking overhead.
3. Type safety
Different sizes create different types, preventing accidental mixing:
FixedBuffer<int, 10> buf10;
FixedBuffer<int, 20> buf20;
// These are different types and cannot be assigned to each other
// buf10 = buf20; // Compilation error
Defining member functions outside the class
When defining member functions outside a template class with non-type parameters, you must include all template parameters:
template <typename T, int Rows, int Cols>
class Matrix
{
private:
T m_data[Rows][Cols]{};
public:
T& at(int row, int col);
const T& at(int row, int col) const;
void fill(const T& value);
void print() const;
};
template <typename T, int Rows, int Cols>
T& Matrix<T, Rows, Cols>::at(int row, int col)
{
return m_data[row][col];
}
template <typename T, int Rows, int Cols>
const T& Matrix<T, Rows, Cols>::at(int row, int col) const
{
return m_data[row][col];
}
template <typename T, int Rows, int Cols>
void Matrix<T, Rows, Cols>::fill(const T& value)
{
for (int r{0}; r < Rows; ++r)
for (int c{0}; c < Cols; ++c)
m_data[r][c] = value;
}
template <typename T, int Rows, int Cols>
void Matrix<T, Rows, Cols>::print() const
{
for (int r{0}; r < Rows; ++r)
{
for (int c{0}; c < Cols; ++c)
std::cout << m_data[r][c] << ' ';
std::cout << '\n';
}
}
Usage:
#include <iostream>
int main()
{
Matrix<int, 3, 3> mat;
mat.fill(0);
mat.at(0, 0) = 1;
mat.at(1, 1) = 2;
mat.at(2, 2) = 3;
std::cout << "3x3 Matrix:\n";
mat.print();
return 0;
}
Output:
3x3 Matrix:
1 0 0
0 2 0
0 0 3
Non-type parameters must be constexpr
The value passed to a non-type parameter must be known at compile time. This means it must be a literal or constexpr value:
template <int Size>
class Ring
{
private:
int m_buffer[Size]{};
public:
// ...
};
int main()
{
constexpr int bufSize{16};
Ring<16> ring1; // OK: literal value
Ring<bufSize> ring2; // OK: constexpr variable
int dynamicSize{32};
Ring<dynamicSize> ring3; // Error: dynamicSize is not constexpr
return 0;
}
This restriction exists because the compiler needs to know the value during compilation to generate code.
Real-world example: std::array
The C++ Standard Library's std::array uses non-type parameters. When you write:
#include <array>
std::array<int, 5> numbers{};
You're using a template with a type parameter (int) and a non-type parameter (5). The 5 becomes part of the type, making std::array<int, 5> fundamentally different from std::array<int, 10>.
Here's a simplified view of how std::array might be implemented:
template <typename T, std::size_t N>
class array
{
private:
T m_elements[N];
public:
constexpr std::size_t size() const { return N; }
T& operator[](std::size_t index) { return m_elements[index]; }
const T& operator[](std::size_t index) const { return m_elements[index]; }
// Additional member functions...
};
Combining type and non-type parameters
You can mix multiple type and non-type parameters for sophisticated compile-time configuration:
#include <iostream>
#include <cstring>
template <typename CharT, int MaxLength>
class FixedString
{
private:
CharT m_buffer[MaxLength + 1]{}; // +1 for null terminator
int m_length{0};
public:
FixedString() = default;
FixedString(const CharT* str)
{
int len{0};
while (str[len] && len < MaxLength)
{
m_buffer[len] = str[len];
++len;
}
m_length = len;
m_buffer[m_length] = '\0';
}
void append(CharT ch)
{
if (m_length < MaxLength)
{
m_buffer[m_length++] = ch;
m_buffer[m_length] = '\0';
}
}
const CharT* c_str() const { return m_buffer; }
int length() const { return m_length; }
int maxLength() const { return MaxLength; }
};
int main()
{
FixedString<char, 20> name{"Alice"};
name.append('!');
std::cout << "Name: " << name.c_str() << '\n';
std::cout << "Length: " << name.length() << '/' << name.maxLength() << '\n';
FixedString<wchar_t, 10> wideName{L"Bob"};
std::wcout << L"Wide name: " << wideName.c_str() << '\n';
return 0;
}
This template has both a type parameter (CharT for the character type) and a non-type parameter (MaxLength for the maximum string size).
Default values for non-type parameters
Non-type parameters can have default values:
template <typename T, int Capacity = 100>
class Cache
{
private:
T m_items[Capacity]{};
int m_size{0};
public:
void store(const T& item)
{
if (m_size < Capacity)
m_items[m_size++] = item;
}
int size() const { return m_size; }
int capacity() const { return Capacity; }
};
int main()
{
Cache<int> defaultCache; // Uses default capacity of 100
Cache<int, 50> smallCache; // Uses capacity of 50
std::cout << "Default capacity: " << defaultCache.capacity() << '\n';
std::cout << "Small capacity: " << smallCache.capacity() << '\n';
return 0;
}
Common use cases
Non-type parameters excel in these scenarios:
- Fixed-size containers: Arrays, buffers, matrices with compile-time sizes
- Compile-time algorithms: Unrolled loops, compile-time calculations
- Policy-based design: Configuring behavior with integral constants
- Memory optimization: Avoiding dynamic allocation for fixed-size data
Non-type parameters are a powerful feature that enables compile-time configuration and optimization. They work hand-in-hand with type parameters to create flexible, efficient template classes.
Value-Based Template Parameters - Quiz
Test your understanding of the lesson.
Practice Exercises
Static Array with Compile-Time Size
Create a class template `StaticArray` that uses a non-type parameter to specify the array size at compile time. The class should store integers and provide methods to set and get values by index, as well as display all elements.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!