C++ / TEMPLATES AND GENERIC PROGRAMMING
Non-type template parameters and auto placeholders
Parameterise templates by compile-time values, reason about why each value makes a distinct type, and use template<auto V> to deduce a value's type.
What you will learn
- Parameterise a class or function template by a compile-time value
- Use template <auto V> to deduce a value argument's type and read it with decltype(V)
- Explain why Vec<3> and Vec<4> are unrelated types with no conversion between them
- Tell valid NTTP arguments from invalid ones like runtime variables
Understanding Non-type template parameters and auto placeholders
A template parameter list is not restricted to types. Writing template <std::size_t N> declares N as a value that must be known while the compiler runs, and the compiler substitutes it everywhere it appears, so Vec<3> and Vec<4> come from one piece of source but are two unrelated types with different object sizes and no implicit conversion between them. The mental model is that the argument list is part of the entity's name: a value in that list is as much a part of the identity as int is in vector<int>. That is precisely how std::array<T, N> keeps its length inside the type and can report size() as a constant.
Because the argument ends up baked into a mangled name, it has to be a constant expression with an unambiguous identity. Up to C++17 that means integral and enumeration values, pointers or references to objects and functions with static storage duration, pointers to members, and nullptr; C++20 adds floating-point values and structural class types, whose bases and non-static data members are all public and non-mutable. A local variable is rejected not because you do not know its value but because instantiation happens during translation while the variable exists only at run time; const int n = 8; works exactly because that initialiser makes n a constant expression.
Since C++17, template <auto V> asks the compiler to deduce the parameter's type from the argument you write, using the ordinary auto rules, and decltype(V) recovers that type inside the body. This saves you from spelling types that are tedious or variable, such as int Item::* for &Item::qty, and lets one template accept an enumerator from any enumeration. The price is that deduction is exact: f<7> and f<7u> name different specialisations because the deduced types differ, and template <auto... Vs> consequently accepts a pack of values of mixed types.
placeholder
<cstddef>
<iostream>
<type_traits>
template <std::size_t N>
struct Vec {
double e[N]{};
static constexpr std::size_t size() { return N; }
};
template <std::size_t N> // N is deduced from the arguments
double dot(const Vec<N>& a, const Vec<N>& b) {
double s = 0.0;
for (std::size_t i = 0; i < N; ++i) s += a.e[i] * b.e[i];
return s;
}
template <auto Step> // type and value both deduced
void describe() {
std::cout << "value=" << Step
<< " int=" << std::is_same_v<decltype(Step), int>
<< " long=" << std::is_same_v<decltype(Step), long>
<< " char=" << std::is_same_v<decltype(Step), char> << '\n';
}
int main() {
Vec<3> a{{1.0, 2.0, 3.0}};
Vec<3> b{{4.0, 5.0, 6.0}};
std::cout << dot(a, b) << '\n';
std::cout << Vec<3>::size() << '\n';
static_assert(Vec<3>::size() == 3);
static_assert(!std::is_same_v<Vec<3>, Vec<4>>);
describe<7>();
describe<7L>();
describe<'x'>();
}
A non-type template parameter is a value fixed into the entity's identity at compile time, so every distinct value yields a distinct type or function, and auto lets the compiler deduce that value's type.
Worked examples
A value argument folded into a constant
Shows an int parameter driving a compile-time computation, and which arguments count as constant expressions.
<cstdint>
<iostream>
template <int Bits>
struct Mask {
static_assert(Bits > 0 && Bits <= 32, "Bits must be 1..32");
static constexpr std::uint32_t value =
(Bits == 32) ? 0xFFFFFFFFu : ((1u << Bits) - 1u);
};
template <int Bits>
std::uint32_t clip(std::uint32_t x) { return x & Mask<Bits>::value; }
int main() {
std::cout << clip<4>(0xABCD) << '\n';
std::cout << clip<8>(0xABCD) << '\n';
std::cout << Mask<12>::value << '\n';
const int width = 8; // constant expression, usable as an argument
std::cout << clip<width>(0xABCD) << '\n';
int runtime_width = 8; // clip<runtime_width>(0xABCD) would not compile
std::cout << (runtime_width == width) << '\n';
}
Example explained
Line 1Mask<8>::value is evaluated during compilation, so clip<8> reduces to x & 0xFF and prints 0xCD as 205.
Line 2Mask<4>, Mask<8> and Mask<12> are three separate class types, and the static_assert is only checked for the ones actually instantiated.
Line 3const int width = 8 has a constant initialiser, so clip<width> names the very same specialisation as clip<8>.
Line 4Swapping in runtime_width makes the program ill-formed: the compiler must have the argument before the program ever runs.
auto for an argument whose type is awkward to spell
Uses a pointer to member as a non-type argument so the template never has to name the type int Item::*.
<iostream>
<string>
<vector>
struct Item { std::string name; int qty; int price; };
template <auto Field> // Field has type int Item::*
int total(const std::vector<Item>& items) {
int sum = 0;
for (const Item& it : items) sum += it.*Field;
return sum;
}
int main() {
std::vector<Item> cart{{"bolt", 4, 30}, {"nut", 10, 5}, {"washer", 2, 12}};
std::cout << total<&Item::qty>(cart) << '\n';
std::cout << total<&Item::price>(cart) << '\n';
}
Example explained
Line 1&Item::qty is a pointer to member, one of the argument kinds a non-type parameter may take, and it is a compile-time constant.
Line 2template <auto Field> avoids writing int Item::* in the header and would equally accept a pointer to any other member.
Line 3it.*Field becomes a fixed member offset in each instantiation, so total<&Item::qty> and total<&Item::price> are two separate functions, not one function reading a variable.
Important notes
template <auto> and std::is_same_v are C++17 features, so compile these examples with -std=c++17 or later; floating-point and class-type arguments need C++20.
auto in a non-type parameter deduces by value, so top-level const is dropped and arrays and functions decay to pointers; write template <auto& R> when you want to bind to a named object with static storage duration instead of copying its value.
Common mistakes
Feeding a runtime value to the parameter: int n = 5; Vec<n> v; is rejected with 'non-type template argument is not a constant expression', and adding const to a variable initialised at run time does not help; a runtime size needs a heap container or a switch that selects among a fixed set of instantiations.
Assuming template <auto V> ignores the literal's type: calling f<0>, f<0u> and f<0L> quietly produces three specialisations with three sets of function-local statics and three copies of the code, and decltype(V) is int, unsigned int and long respectively.
Trying to pass a string literal, as in template <const char* S> and greet<"hello">(): the literal is not a permitted argument, so it fails to compile; pre-C++20 you need a constexpr character array with linkage, and in C++20 a structural class type wrapping the array.
Try it yourself
Change, predict, then run
In an online compiler write template <std::size_t N> struct Grid holding int cell[N][N]{} with a static constexpr std::size_t area() returning N * N, and add static_assert(Grid<4>::area() == 16). Then write template <auto V> void tag() that prints V and std::is_same_v<decltype(V), unsigned>, and call tag<3>() and tag<3u>() to see that they are two different instantiations.
Open the C++ workspaceCheck your understanding
Given template <auto V> void f() { }, how many distinct specialisations exist after the calls f<10>(); f<10u>(); f<10L>(); f<10>();?
- One, because every argument denotes the same value 10
- Two, because unsigned int and long both promote to int for template matching
- Three, because 10, 10u and 10L deduce different types, and the repeated f<10>() reuses the first specialisation
- Four, because one specialisation is created per call expression
Show answer
Template arguments are compared by type as well as by value, so the deduced types int, unsigned int and long give three separate specialisations; the fourth call repeats an argument that is identical in both type and value, so it reuses the existing one rather than creating a fourth. The 'one' answer is tempting because all four arguments print as 10, but with auto the type is part of the argument's identity, and no promotion happens when matching template arguments.