Ch15.4: Non-Type Template Parameters: Enums, Structural Types, Variable Templates
1. A Template Argument Can Be a Value
Until now every template argument was a type. It can just as well be a value, and
then it belongs to the type of the instantiation: array<int, 4>
and array<int, 8> are different types, with different
sizeof, and neither can be assigned to the other.
template <typename T, ::std::size_t N>
struct array
{
T elements[N]{};
[[nodiscard]] constexpr ::std::size_t size() const noexcept { return N; }
};
Such a parameter is a non-type template parameter (NTTP). N
is usable anywhere a constant expression is: as an array bound, in
static_assert, in if constexpr, in another template
argument.
The allowed kinds have grown over the years, and all four of the ones below are used by fast_io itself:
| Kind | Example parameter | Used by fast_io for |
|---|---|---|
| integer | ::std::size_t N |
buffer sizes, fixed capacities |
| enum | open_mode mode |
flags known at compile time (::fast_io::open_mode::in) |
auto |
template <auto v> |
character literals: ::fast_io::char_literal_v<u8'0', char_type> |
| structural class | fixed_name N |
turning a string literal into a type |
2. Enums as Parameters: the Switch That Costs Nothing
An enumerator in a template argument is a compile-time constant, so
if constexpr can select a branch and discard the others
(Ch15.8). The result is a
switch with no jump table, no branch, no argument:
enum class channel : ::std::uint_least8_t
{
red,
green,
blue
};
template <channel c>
[[nodiscard]] inline ::std::string_view channel_name() noexcept
{
if constexpr (c == channel::red)
{
return "red";
}
else if constexpr (c == channel::green)
{
return "green";
}
else
{
return "blue";
}
}
channel_name<channel::green>() returns
"green" and the generated function contains nothing else. The usual
warning applies: an enum NTTP must be used at compile time to be worth
anything. If the runtime value varies, take channel as a function
parameter instead — a template instantiation per enumerator is bloat, not
speed.
3. auto: Let the Value Pick the Type
template <auto v> accepts any NTTP-eligible value and deduces its
type from the argument. This is how fast_io writes character comparisons without
repeating the character type, and it is the shortest way to write a value-parameterised
constant:
template <auto v>
inline constexpr auto doubled = v * 2;
doubled<21> is the int 42, and
doubled<'A'> is a char. Note that the type is part of
the instantiation's identity: doubled<21> and
doubled<21u> are different variables, because
int and unsigned are different NTTP types.
4. Structural Class Types: a String Literal As a Type
A class can be a template argument if it is a structural type: a
literal type whose every base and non-static data member is public, non-mutable, and
itself structural, with a constexpr constructor and no
virtual anything. Then the argument is compared member by member, and
two calls with different strings are different instantiations.
struct fixed_name
{
char chars[8]{};
::std::size_t size{};
template <::std::size_t N>
constexpr fixed_name(char const (&s)[N]) noexcept
{
static_assert(N <= 8, "name too long");
for (::std::size_t i{}; i != N; ++i)
{
chars[i] = s[i];
size = i;
}
}
[[nodiscard]] constexpr ::std::string_view view() const noexcept { return {chars, size}; }
};
template <fixed_name N>
[[nodiscard]] inline ::std::string_view declared_name() noexcept
{
return N.view();
}
Now declared_name<fixed_name{"alpha"}>() returns
"alpha" at run time, with the string already baked into the code
section. The template-taking constructor is what makes
fixed_name{"alpha"} work with a six-character literal in an
eight-character array; without it, the array bound would have to match exactly.
The catch is the same as with every NTTP: every distinct string is a distinct instantiation. Use this for constants that are fixed by the program, never for strings that arrive from a config file.
5. Variable Templates: a Constant That Depends on a Type
A variable template is a constant whose value is a function of its
template arguments. It is inline constexpr, so every translation unit
sees the same object and no .cpp file has to define it:
template <typename T>
inline constexpr ::std::size_t width = sizeof(T) * 8;
width<int> is 32, width<char> is
8. This is the shape of almost every constant in fast_io:
::fast_io::io_reserve_type<char, rgb> is a variable template whose
value is the tag object that Ch15.11
dispatches on.
Two refinements earn their keep. First, a normalising alias, so callers may pass a
reference or a const type without changing the answer:
template <typename T>
inline constexpr ::std::size_t width_v = width<::std::remove_cvref_t<T>>;
Second, an initialiser that is a function call. The initialiser is a constant
expression, so the call is constexpr or consteval
(Chapter 7) and runs at compile time:
template <typename T>
inline constexpr ::std::size_t digits = count_digits<T>();
width_v<int const &> is 32, not something else
— that is the whole point of the _v form.
6. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <string_view>
enum class channel : ::std::uint_least8_t
{
red,
green,
blue
};
template <channel c>
[[nodiscard]] inline ::std::string_view channel_name() noexcept
{
if constexpr (c == channel::red)
{
return "red";
}
else if constexpr (c == channel::green)
{
return "green";
}
else
{
return "blue";
}
}
struct fixed_name
{
char chars[8]{};
::std::size_t size{};
template <::std::size_t N>
constexpr fixed_name(char const (&s)[N]) noexcept
{
static_assert(N <= 8, "name too long");
for (::std::size_t i{}; i != N; ++i)
{
chars[i] = s[i];
size = i;
}
}
[[nodiscard]] constexpr ::std::string_view view() const noexcept { return {chars, size}; }
};
template <fixed_name N>
[[nodiscard]] inline ::std::string_view declared_name() noexcept
{
return N.view();
}
template <typename T>
inline constexpr ::std::size_t width = sizeof(T) * 8;
template <auto v>
inline constexpr auto doubled = v * 2;
template <typename T>
inline constexpr ::std::size_t width_v = width<::std::remove_cvref_t<T>>;
int main()
{
try
{
::fast_io::io::println(channel_name<channel::green>());
::fast_io::io::println(declared_name<fixed_name{"alpha"}>());
::fast_io::io::println("width<int>=", width<int>, " width<char>=", width<char>);
::fast_io::io::println("doubled<21>=", doubled<21>);
::fast_io::io::println("width_v<int const &>=", width_v<int const &>);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
green
alpha
width<int>=32 width<char>=8
doubled<21>=42
width_v<int const &>=32
7. Rules of Thumb
- An NTTP must be a compile-time constant; if the value is not, it is a function parameter, not a template parameter.
- Every distinct value is a distinct instantiation. Ten
fixed_namestrings are ten copies of everything the template touches. - Prefer
autoNTTPs when the same logic is used forchar,char8_tandwchar_t; it saves writing the character type twice. - Variable templates are the idiomatic home for per-type constants: no
structwith astatic constexprmember, no_vtrait boilerplate. - An
inline constexprinitialiser that calls a function forces that function to beconstexpr; keep such functionsnoexceptso the constant cannot fail at compile time.