Non-Type Template Parameters
Template parameters don't have to be types. A template can also be parameterized by a compile-time value — an integer, an enum, a pointer, or (since C++20) even a floating-point number or a suitable class. These are called non-type template parameters (NTTPs), and they are what make std::array<int, 10> possible.
Values as template arguments
Consider a fixed-size buffer: you want the element type to be a type parameter, but the size also needs to be part of the type — fixed at compile time so the compiler can allocate the storage inline, with no heap and no indirection. A runtime size_t size constructor argument would work, but then all buffer sizes share the same type and the compiler has no visibility into the size at all. The solution is to make size a template parameter rather than a constructor argument.
template <typename T, size_t N>
class buffer {
T data_[N]; // N is a compile-time constant — stack allocation
public:
constexpr T& operator[](size_t i) { return data_[i]; }
constexpr const T& operator[](size_t i) const { return data_[i]; }
constexpr size_t size() const { return N; }
constexpr T* data() { return data_; }
};
buffer<int, 10> b1; // 10 ints on the stack
buffer<int, 10> b2; // same type as b1
buffer<int, 15> b3; // DIFFERENT type — 15 ints on the stackThis is exactly the pattern used by std::array<T, N> in the standard library — and by std::bitset<N>, std::tuple_element<I, T>, and std::get<I>(). NTTPs are ubiquitous in the STL.
Different NTTP values produce different types
This is the most important thing to internalize about NTTPs: when the template argument is a different compile-time value, the compiler generates a completely different type. buffer<int, 10> and buffer<int, 15> are as unrelated as int and double — you cannot assign one to the other, pass one where the other is expected, or put both in the same container. The value is part of the type.
buffer<int, 10> b1; buffer<int, 20> b2; // Two ways to verify: same vs. different static_assert( std::is_same_v<decltype(b1), buffer<int, 10>>); // OK static_assert(!std::is_same_v<decltype(b1), decltype(b2)>); // different types // Compile-time expressions are evaluated — the result is what matters: buffer<int, 2*5> b3; // same type as b1 — 2*5 evaluates to 10 at compile time static_assert( std::is_same_v<decltype(b1), decltype(b3)>); // same type // Runtime variables cannot be NTTP arguments: int n = 10; // buffer<int, n> bad; // ERROR: n is not a constant expression // constexpr variables work: constexpr int M = 10; buffer<int, M> b4; // OK — M is a compile-time constant
What types can be used as NTTPs?
Not every type can serve as a non-type template parameter. The argument must be a compile-time constant of a structural type. The standard defines structural types as follows:
| Category | Available since | Examples |
|---|---|---|
| Integral types | C++98 | int, size_t, bool, char, long long, … |
| Enumerations | C++98 | enum Color { Red, Green } |
| Pointer types | C++98 | T*, void (*f)() — must have external linkage |
| Pointer-to-member types | C++98 | void (C::*m)() |
| Lvalue reference types | C++98 | T& — object/function must have external linkage |
| Floating-point types | C++20 | double, float |
| Literal class types (structural) | C++20 | All bases and non-static members public, non-mutable |
// Integral — the most common case
template <int N>
struct fixed_array { int data[N]; };
fixed_array<8> arr; // int data[8]
// Enumeration
enum class Direction { North, South, East, West };
template <Direction D>
struct mover { void step(); };
mover<Direction::North> m;
// Floating-point (C++20)
template <double Tolerance>
bool almost_equal(double a, double b) {
return std::abs(a - b) < Tolerance;
}
bool ok = almost_equal<1e-6>(3.14159, 3.14160);
// Pointer to function (must have external/static linkage)
void say_hello() { std::cout << "Hello\n"; }
template <void (*Action)()>
struct runner { void run() { (*Action)(); } };
runner<&say_hello> r;
r.run(); // prints "Hello"Default NTTP values
Just like type template parameters, NTTPs can have default values. The rules are the same: in a class template, all parameters after the first defaulted parameter must also have defaults. In a function template, this restriction doesn't apply.
// Default size of 64
template <typename T, size_t N = 64>
class buffer { T data_[N]; /* ... */ };
buffer<int> b1; // buffer<int, 64> — uses default
buffer<int, 32> b2; // explicit override
// Multiple defaults
template <typename T = int, size_t N = 16, bool ZeroInit = false>
class typed_buffer { /* ... */ };
typed_buffer<> tb1; // int, 16, false
typed_buffer<double> tb2; // double, 16, false
typed_buffer<double, 8> tb3; // double, 8, false
typed_buffer<double, 8, true> tb4; // double, 8, trueauto non-type template parameters (C++17)
Before C++17, the type of a NTTP had to be written explicitly in the template parameter list. C++17 introduced template <auto V>, which lets the compiler deduce the type of the parameter from the argument supplied at instantiation. This is particularly useful when writing utilities that need to work with constants of any integral type — or (from C++20) of any structural type — without forcing the user to repeat the type.
// Before C++17: type must be explicit
template <int N> struct old_tag {};
template <char C> struct old_ctag {};
// Need one template per integral type — cumbersome
// C++17: auto deduces the type
template <auto V>
struct tag {
using value_type = decltype(V);
static constexpr value_type value = V;
};
tag<42> t1; // tag<int, 42> — value_type is int
tag<42u> t2; // tag<unsigned, 42u>
tag<'X'> t3; // tag<char, 'X'>
tag<3.14> t4; // tag<double, 3.14> — only valid C++20 (float NTTP)
tag<true> t5; // tag<bool, true>
static_assert(std::is_same_v<tag<42>::value_type, int>);
static_assert(tag<'X'>::value == 'X');The auto specifier also works with parameter packs, allowing a single template to accept a sequence of NTTP arguments of potentially different types. The compiler deduces each type independently.
// auto NTTP pack: each element can be a different type
template <auto... Vs>
struct value_list {
static constexpr std::size_t size = sizeof...(Vs);
};
value_list<42, 'X', true, 3.14> vl; // int, char, bool, double — all deduced
static_assert(vl.size == 4);
// Useful for compile-time sequences of mixed constants
// Used in tag dispatch, policy classes, compile-time lookup tablesClass-type NTTPs (C++20)
C++20 allows user-defined literal class types as NTTP arguments, provided the type meets the structural type requirements: all base classes and non-static data members must be public and non-mutable, and must themselves be of structural types. This unlocks the ability to pass string literals, fixed-point numbers, or small configuration structs directly as template arguments.
// A structural string literal type (C++20)
template <size_t N>
struct fixed_string {
char value[N]{};
constexpr fixed_string(const char (&str)[N]) {
std::copy_n(str, N, value);
}
// Equality needed for NTTP identity
constexpr bool operator==(const fixed_string&) const = default;
};
// Deduction guide — lets the compiler infer N from the string literal
template <size_t N>
fixed_string(const char (&)[N]) -> fixed_string<N>;
// Now use it as a NTTP
template <fixed_string Name>
struct named_type {
static constexpr auto name() { return Name.value; }
};
using MyType = named_type<"Widget">;
// MyType::name() == "Widget" — at compile time// Configuration struct as NTTP (C++20)
struct Config {
int max_size;
bool debug;
// All members public + non-mutable = structural type
};
template <Config Cfg>
class engine {
std::array<int, Cfg.max_size> buffer_;
public:
void run() {
if constexpr (Cfg.debug)
std::cout << "debug mode\n";
// ...
}
};
engine<Config{.max_size = 128, .debug = false}> prod_engine;
engine<Config{.max_size = 16, .debug = true}> dev_engine;
// prod_engine and dev_engine have completely different typesNTTPs in the standard library
NTTPs appear throughout the standard library. Once you recognize the pattern — a compile-time value baked into the type — you'll see them everywhere.
std::array<T, N>Fixed-size array; N is a NTTP of type std::size_t. No heap allocation. std::array<int,10> and std::array<int,15> are different types.
std::bitset<N>Fixed-size bitset. N bits, inline storage. Cannot change size at runtime.
std::get<I>(tuple)I is a NTTP. Returns the I-th element of a std::tuple. The index is resolved at compile time — I must be in range or the program is ill-formed.
std::integral_constant<T, V>The building block of type traits. std::true_type is std::integral_constant<bool, true>. V is a NTTP.
std::make_index_sequence<N>Generates an std::index_sequence<0,1,2,...,N-1>. Used to iterate over tuple elements at compile time.
Common NTTP patterns
Several recurring idioms in C++ template code rely on NTTPs. Two of the most useful are compile-time index sequences (for iterating over tuple elements) and policy injection via function pointer NTTPs.
// Pattern 1: index sequences — unpacking tuples at compile time
template <typename Tuple, size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << std::get<Is>(t) << ' '), ...); // fold expression
}
template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
print_tuple_impl(t, std::make_index_sequence<sizeof...(Ts)>{});
}
auto tup = std::make_tuple(1, 2.5, "hello");
print_tuple(tup); // 1 2.5 hello// Pattern 2: policy injection via function pointer NTTP
// The action is wired in at compile time — no virtual dispatch overhead
void log_info(const char* msg) { std::cout << "[INFO] " << msg << '\n'; }
void log_warning(const char* msg) { std::cout << "[WARN] " << msg << '\n'; }
template <void (*Logger)(const char*)>
class service {
public:
void run(const char* status) {
Logger(status); // inlined — zero overhead
}
};
service<&log_info> prod;
service<&log_warning> dev;
prod.run("started"); // [INFO] started
dev.run("started"); // [WARN] started
// prod and dev are different types — cannot be stored in the same container
// without type erasure (e.g. std::function or a base class)Quick reference
| Syntax | Meaning |
|---|---|
| template <int N> | NTTP of type int |
| template <size_t N = 0> | NTTP with default value 0 |
| template <auto V> | NTTP — compiler deduces type from argument (C++17) |
| template <auto... Vs> | NTTP parameter pack — each element can differ in type (C++17) |
| template <double D> | Floating-point NTTP (C++20) |
| template <MyStruct S> | Class-type NTTP — struct must meet structural requirements (C++20) |
| template <void (*F)()> | Function pointer NTTP — F must have external linkage |