What Is a Non-Type Template Parameter?
A non-type template parameter (NTTP) lets you pass a value known at compile time to a template: for example, an array size, a numeric constant, or a pointer to an object or function.
Such a parameter is declared with a type and a name, as in int Value or std::size_t N. Its value becomes part of the template specialization. For pointer and reference parameters, the identity of the referenced object, rather than a snapshot of its contents, becomes part of the specialization; the object itself may remain mutable at runtime.
Non-Type Parameters of Class Templates
A class template can accept both types and values:
#include <array>
#include <cstddef>
template<typename T, std::size_t N = 16>
struct Buffer
{
std::array<T, N> data{};
};
Buffer<int, 8> a;
Buffer<int, 16> b;
Buffer<double> c; // N = 16
The parameter T specifies the element type, while N specifies the number of elements. The template argument for N defaults to 16, so omitting it makes Buffer<double> equivalent to Buffer<double, 16>.
By contrast, Buffer<int, 8 + 8> and Buffer<int, 16> denote the same type because both arguments have the same value after evaluation.
Non-Type Parameters of Function Templates
Function templates can also have non-type parameters:
template<int Value, typename T>
T addValue(T x)
{
return x + Value;
}
auto a = addValue<5>(10); // 15
auto b = addValue<5>(20); // 25
auto c = addValue<7>(10); // 17
In the call addValue<5>(10):
5supplies the value of the template parameterValue;10is passed to the ordinary function parameterx;- the compiler deduces
Tasint.
The first two calls use the addValue<5, int> specialization, while the third uses addValue<7, int>. These specializations are implicitly instantiated when they are used; no explicit specialization with template<> is required.
Unlike Value, the value of the ordinary argument x may be determined at runtime.
Passing a Function to an Algorithm
A function produced from a template can be passed to a standard algorithm:
A concrete specialization as a callable
#include <algorithm>
#include <array>
template<int Value, typename T>
T addValue(T x)
{
return x + Value;
}
int main()
{
std::array<int, 3> values{1, 2, 3};
std::transform(
values.begin(), values.end(),
values.begin(),
addValue<5, int>
);
// values = {6, 7, 8}
}
std::transform accepts a callable and applies it to each element in the range. Here, the concrete function addValue<5, int> is passed without parentheses.
In simplified form, the algorithm's core operation looks like this:
*result = op(*first);
The supplied object must therefore be callable with an input element, and its result must be suitable for assignment to an output element.
Deducing a Non-Type Argument
The compiler can deduce a non-type argument from the type of an ordinary function argument:
#include <cstddef>
template<typename T, std::size_t N>
constexpr std::size_t arraySize(const T (&)[N])
{
return N;
}
int values[]{10, 20, 30};
auto count = arraySize(values); // T = int, N = 3
The size N is deduced from the array type int[3].
Restrictions on Non-Type Parameters
Constant Expressions
For a numeric non-type parameter, the argument must be a converted constant expression of the parameter's type. In other words, the compiler must be able to evaluate the argument and convert it to the declared parameter type at compile time using one of the permitted conversions.
constexpr std::size_t n = 8;
Buffer<int, n * 2> a; // OK
std::size_t size = 8;
// Buffer<int, size> b; // error
An ordinary mutable variable such as size is not a constant expression, even when initialized from a literal.
Permitted Types
In C++17, non-type template parameters may have the following types:
- integral and enumeration types (
int,bool,enum, and others); - pointers to objects and functions;
- pointers to class members;
- lvalue references to objects and functions;
std::nullptr_t.
Since C++20, floating-point types and structural class types have also been permitted:
template<double Factor>
struct Scale {};
struct Point
{
int x;
int y;
};
template<Point P>
struct Marker {};
Scale<1.5> scale; // C++20
Marker<Point{10, 20}> marker; // C++20
A structural class type must be a literal type. All its base classes and non-static data members must be public, and its data members must not be mutable. The type of each such data member and base class must itself be a structural type or an array of structural types.
For example, std::string cannot be passed by value as an NTTP.
Pointers and References
When a template argument is a pointer or reference to an object, it must identify a suitable object with static storage duration. This can be a global variable or a local variable declared with static.
Since C++20, pointers and references to subobjects, such as class data members and array elements, are no longer prohibited in general:
int numbers[]{10, 20, 30};
struct Settings
{
int limit;
};
Settings config{10};
template<int* P>
struct Pointer
{
int* p = P;
};
int main()
{
Pointer<&numbers[1]> a; // C++20
Pointer<&config.limit> b; // C++20
*a.p = 42; // numbers[1] = 42
*b.p = 20; // config.limit = 20
}
Here, numbers and config are declared at namespace scope and therefore have static storage duration. Using &numbers[1] or &config.limit as a non-type template argument is prohibited in C++17 but permitted in C++20.
The objects' contents may still change at runtime. The template argument represents the compile-time identity of the object or subobject at the specified address.
For example:
template<const char* Text>
struct Message {};
constexpr char text[] = "hello";
Message<text> a; // OK
Message<&text[1]> b; // C++20
// Message<"hello"> c; // error
// Message<&"hello"[1]> d; // error
The expression that takes the address of a character in a string literal is valid by itself:
const char* p = &"hello"[1]; // address of the character 'e'
It is equivalent to &("hello"[1]). The error occurs specifically when such an address is used as a non-type template argument.
Expressions Using the > Operator
A comparison expression that uses > should be enclosed in parentheses so that the symbol is not interpreted as the end of the template argument list:
template<bool Flag>
struct Check {};
Check<(sizeof(int) > 4)> check;
Non-Type Parameters with auto
Since C++17, the type of a non-type template parameter can be deduced automatically:
template<auto Value>
struct Constant
{
using value_type = decltype(Value);
};
Constant<42> a; // int
Constant<42u> b; // unsigned int
Constant<'A'> c; // char
Constant<1.5> d; // double, C++20
With auto, both the type and the value of the argument are significant. Consequently, Constant<42> and Constant<42u> are different types.
When the type of an integral non-type parameter is specified explicitly, the argument must be a converted constant expression of that type:
#include <type_traits>
static_assert(std::is_same_v<
Buffer<int, 8>,
Buffer<int, 8u>
>);
static_assert(!std::is_same_v<
Constant<8>,
Constant<8u>
>);
In the Buffer<T, N> template, N is declared as std::size_t, so both arguments are converted to that type and produce the value 8. In the Constant<Value> template, the type of Value is deduced through auto, so 8 and 8u produce different specializations.
Dependencies Between Parameters
A parameter's type or a default template argument may depend on preceding parameters:
template<auto Value, typename T = decltype(Value)>
T getValue()
{
return Value;
}
template<typename T, T Value = T{}>
T getDefault()
{
return Value;
}
In the first case, the default type of T is determined from the type of Value:
getValue<5>(); // T = int, returns 5
In the second case, the default value of Value is created by the expression T{}:
getDefault<int>(); // Value = 0, returns 0
Non-Type Parameters with decltype(auto)
Since C++17, decltype(auto) can be used instead of auto. Such a parameter's type is deduced according to the rules of decltype, so it can preserve the argument's reference type.
First, consider the difference between the two forms of decltype:
int counter = 0;
using CounterType = decltype(counter); // int
using CounterReference = decltype((counter)); // int&
decltype(counter) yields the variable's declared type, int. decltype((counter)) examines an expression; because counter is an lvalue, the result is int&.
The same rules apply to non-type template parameters:
Parentheses change the deduced type
template<decltype(auto) Value>
struct Holder
{
void set(int x)
{
Value = x;
}
};
int main()
{
static int counter = 0;
// Holder<counter> a; // error: Value is deduced as int
Holder<(counter)> b; // OK: Value has type int&
b.set(42); // counter = 42
}
Without parentheses, Value is deduced as int. The compiler would then require counter to have a constant value, but the variable is mutable, so the declaration is ill-formed.
With parentheses, Value is deduced as int&. The specialization receives a reference to a specific object whose value may change at runtime.
An object passed this way must have static storage duration. That is why counter is declared with static. Without that specifier, an ordinary local variable with automatic storage duration is not a valid argument:
int main()
{
int counter = 0;
// Holder<(counter)> b; // error
}
Key Takeaways
- A non-type template argument is a value that contributes to the identity of a template specialization.
- A numeric argument must be a converted constant expression of the corresponding parameter's type.
- When the parameter type is explicit, the argument is converted to it; with
auto, the type is deduced and also distinguishes specializations. - The compiler can deduce non-type arguments from the types of ordinary function arguments, as with the array size
N. - C++20 expanded the set of permitted types to include floating-point and structural class types.
- Pointers and references capture an object's identity but do not make its contents immutable.
decltype(auto)preserves the distinction between values and references, so parentheses around an expression can change the deduction result.