第15.4章 非类型模板参数:枚举、结构化类型、变量模板

English

1. 模板实参可以是一个值

到这里为止,每个模板实参都是类型。它同样可以是一个值,此时它属于该实例化的类型: array<int, 4> 与 array<int, 8> 是不同类型, sizeof 不同,也不能互相赋值。


template <typename T, ::std::size_t N>
struct array
{
	T elements[N]{};

	[[nodiscard]] constexpr ::std::size_t size() const noexcept { return N; }
};

这样的参数叫非类型模板参数(NTTP)。N 可以出现在任何需要 常量表达式的地方:数组界、static_assert、if constexpr、 另一个模板实参。

允许的种类这些年一直在增加,下面四种 fast_io 自己都在用:

种类 参数示例 fast_io 用它做什么
整数 ::std::size_t N 缓冲区大小、固定容量
枚举 open_mode mode 编译期已知的标志(::fast_io::open_mode::in)
auto template <auto v> 字符字面量:::fast_io::char_literal_v<u8'0', char_type>
结构化类类型 fixed_name N 把字符串字面量变成类型

2. 枚举作参数:不花代价的 switch

模板实参里的枚举量是编译期常量,所以 if constexpr 可以选一个分支、丢弃其余 (第15.8章)。 结果是:没有跳转表、没有分支、没有参数的 switch。


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>() 返回 "green", 生成的函数里没有别的东西。老警告照旧:枚举 NTTP 只有在编译期被用到时才有价值。 如果值在运行期变化,就直接用 channel 作函数参数 —— 一个枚举量一个实例化是 膨胀,不是速度。

3. auto:让值自己挑类型

template <auto v> 接受任何合法的 NTTP 值,并从实参推导类型。 fast_io 就是用它来写字符比较,免得把字符类型写两遍;它也是写“按值参数化的常量”最短的方式:


template <auto v>
inline constexpr auto doubled = v * 2;

doubled<21> 是 int 的 42, doubled<'A'> 是 char。注意类型是实例化身份的一部分: doubled<21> 与 doubled<21u> 是不同的变量, 因为 int 与 unsigned 是不同的 NTTP 类型。

4. 结构化类类型:把字符串字面量变成类型

只要类是结构化类型,它就能当模板实参:字面量类型、所有基类与非静态数据 成员都是公开的、非 mutable、本身也是结构化的,有 constexpr 构造函数,且没有任何 virtual。此时实参按成员逐一比较,两个不同的字符串就是 两个不同的实例化。


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();
}

现在 declared_name<fixed_name{"alpha"}>() 在运行期返回 "alpha",而字符串早已被塞进代码段。能让 fixed_name{"alpha"} 这个六字符字面量装进八字符数组的,正是那个模板构造函数; 没有它,数组界必须严丝合缝。

代价与所有 NTTP 一样:每个不同的字符串都是一个不同的实例化。 用它来写程序里固定的常量,绝不要用它处理配置文件里来的字符串。

5. 变量模板:取决于类型的常量

变量模板是一个值取决于模板实参的常量。它是 inline constexpr, 所以每个翻译单元看到的是同一个对象,也不需要任何 .cpp 去定义它:


template <typename T>
inline constexpr ::std::size_t width = sizeof(T) * 8;

width<int> 是 32,width<char> 是 8。fast_io 里几乎每个常量都是这个形状: ::fast_io::io_reserve_type<char, rgb> 就是一个变量模板,它的值是 第15.11章用来分派的那个标签对象。

两个改进值得写。第一,加一个归一化别名,这样调用者传引用或 const 类型都不会 改变答案:


template <typename T>
inline constexpr ::std::size_t width_v = width<::std::remove_cvref_t<T>>;

第二,初值可以是一次函数调用。初值必须是常量表达式,所以那个函数是 constexpr 或 consteval (第7章),在编译期执行:


template <typename T>
inline constexpr ::std::size_t digits = count_digits<T>();

width_v<int const &> 是 32,不是别的什么 —— 这就是 _v 形式存在的全部意义。

6. 完整程序


#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);
	}
}

输出:


green
alpha
width<int>=32 width<char>=8
doubled<21>=42
width_v<int const &>=32

7. 经验法则