第15.4章 非类型模板参数:枚举、结构化类型、变量模板
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. 经验法则
- NTTP 必须是编译期常量;不是常量就用函数参数,不要用模板参数。
- 每个不同的值都是一个不同的实例化。十个
fixed_name字符串就是模板碰到的一切的十份拷贝。 - 同一套逻辑要服务
char、char8_t、wchar_t时, 优先用autoNTTP,免得把字符类型写两遍。 - “每个类型一个常量”的惯用归处是变量模板:不要写带
static constexpr成员的struct,也不要写_v特征的那些样板。 - 调用函数的
inline constexpr初值会强制那个函数是constexpr;让这类函数保持noexcept,常量就不会在编译期失败。