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