第15.11章 fast_io 如何找到你的打印与扫描代码
1. 不是运算符,也不是成员函数
::fast_io::io::println(rgb{7, 128, 255}) 能打印出
(7,128,255),只因为你写了两个自由函数。没有
operator<<、没有 to_string()、没有虚函数
format()。fast_io 调用自由函数,并且靠对标签做实参依赖查找
找到它们。
定制点的目录在 第11.2章。本页讲的是它 底下的机制,因为这个机制是纯粹的泛型编程:一个变量模板、一个概念,加一次重载解析。
2. 标签:一个变量模板
每个定制点的第一个实参都不是被打印的值,而是一个标签,说明这份定义是给哪个字符类型 与哪个值类型的:
::fast_io::io_reserve_type<char, rgb> // the tag for rgb, printed as char
::fast_io::io_reserve_type_t<char, rgb> // its type
io_reserve_type 是一个变量模板
(第15.4章),一个
inline constexpr 对象,唯一的工作就是被按值传递。这个选择解决了一个函数模板
解决不了的问题:函数模板不能偏特化,所以“特化 rgb 的打印”需要别的东西来特化 ——
而一个类类型标签可以被重载。之后,普通的重载解析加上 ADL 就会选中你的函数:
template <::std::integral char_type>
inline constexpr ::std::size_t print_reserve_size(::fast_io::io_reserve_type_t<char_type, rgb>) noexcept;
由此有两个后果,都重要。你的定义必须位于与 rgb 或 ::fast_io
关联的命名空间里 —— 通常是 rgb 旁边的全局命名空间。而且
char_type 是模板参数,所以一份定义服务 char、
char8_t、wchar_t 以及其他所有字符类型。
3. 打印:先给大小,再写字节
fast_io 最快的打印路径写进一块它已经预留好的缓冲区。两个函数来描述它:一个说最多需要 多少个字符,另一个把它们填进去。
template <::std::integral char_type>
inline constexpr ::std::size_t print_reserve_size(::fast_io::io_reserve_type_t<char_type, rgb>) noexcept
{
return 12;
}
template <::std::integral char_type>
inline constexpr char_type *print_reserve_define(::fast_io::io_reserve_type_t<char_type, rgb>, char_type *iter, rgb c) noexcept
{
*iter++ = static_cast<char_type>(u8'(');
iter = write_uint(iter, c.r);
*iter++ = static_cast<char_type>(u8',');
iter = write_uint(iter, c.g);
*iter++ = static_cast<char_type>(u8',');
iter = write_uint(iter, c.b);
*iter++ = static_cast<char_type>(u8')');
return iter;
}
契约刻意写得很窄,也正是这条路快的原因:
print_reserve_size返回上界,计算时拿不到值。 十二个字符足够(255,255,255)。print_reserve_define最多写这么多个字符并返回新的末尾指针。 两者之差就是输出。- 两者都不能失败。都是
noexcept,这只可能,因为缓冲区已经存在、 什么都不分配(第15.8章)。
如果你的类型不看值就不知道要多大,这条路就走不通:改用 print_define,
它把输出设备交给你,而不是缓冲区;或者用 print_scatter_define(文本已在内存里)。
两者在 第11.2章都有演示。
4. 扫描:一个状态、一步、一个结尾
扫描可能需要的输入比缓冲区里现有的多,所以它不可能是单次调用。fast_io 的答案与手写解析器 同形:一份跨调用存活下来的状态、一个消费现有输入的步骤、 以及给出最终裁决的 EOF 处理器。
struct rgb_scan_state
{
::std::uint_least32_t value{};
::std::uint_least8_t field{};
::std::uint_least8_t digits{};
};
template <::std::integral char_type>
inline constexpr ::fast_io::io_type_t<rgb_scan_state>
scan_context_type(::fast_io::io_reserve_type_t<char_type, ::fast_io::parameter<rgb &>>) noexcept
{
return {};
}
scan_context_type 通过 ::fast_io::io_type_t<S>
给状态类型命名,但不构造它。步骤函数随后返回一个解析结果:
指向它消费掉的最后一个字符之后的迭代器,加一个码。
template <::std::integral char_type>
inline constexpr ::fast_io::parse_result<char_type const *> scan_context_define(
::fast_io::io_reserve_type_t<char_type, ::fast_io::parameter<rgb &>>, rgb_scan_state &st,
char_type const *begin, char_type const *end, ::fast_io::parameter<rgb &> token) noexcept
| 码 | 含义 | fast_io 怎么做 |
|---|---|---|
parse_errc::ok |
值已经完整 | 推进缓冲区,继续处理下一个实参 |
parse_errc::partial |
目前合法,还需要更多输入 | 保留状态、填充缓冲区、再次调用 |
parse_errc::invalid |
无法解析 | 抛赫布异常,[parse](3)Invalid format |
parse_errc::end_of_file |
输入在值中间结束 | 由 scan_context_eof_define 上报 |
形参是 ::fast_io::parameter<rgb &> 而不是 rgb,
这才是有意思的地方。传给 ::fast_io::io::scan 的左值是要被写入的,
所以 fast_io 把它包进 ::fast_io::parameter<rgb &> —— 一个只有一个
成员的 struct,里面是那个引用。把定制点建立在包装上,既让标签仍然从普通值类型
导出,又让步骤函数能写你的对象。载荷是 token.reference。
另外注意步骤函数不做的事:在整个颜色被校验通过之前,它从不写
c.r。中间结果待在状态里,目标对象只在解析成功的那一刻被修改 ——
这就是 第14.9章的强异常安全保证,
用在了解析上。
5. 把关的那些概念
没有任何地方手工检查你的签名。
fast_io_core_impl/concepts/operation.h 给每个定制点定义了一个概念,
println 只是要求其中之一:
template <typename char_type, typename T>
concept reserve_printable = ::std::integral<char_type> && requires(T t, char_type *ptr) {
{
print_reserve_define(io_reserve_type<char_type, T>, ptr, t)
} -> ::std::same_as<char_type *>;
constexpr ::std::size_t sz{print_reserve_size(io_reserve_type<char_type, T>)};
};
template <typename char_type, typename T>
concept context_scannable = requires(char_type const *begin, char_type const *end, T t) {
requires requires(
typename ::std::remove_cvref_t<decltype(scan_context_type(io_reserve_type<char_type, T>))>::type st) {
{
scan_context_define(io_reserve_type<char_type, T>, st, begin, end, t)
} -> ::std::same_as<parse_result<char_type const *>>;
{ scan_context_eof_define(io_reserve_type<char_type, T>, st, t) } -> ::std::same_as<::fast_io::freestanding::parse_errc>;
};
};
这就是为什么少写一个定制时,你得到的是
type not scannable. need context_scannable,而不是一页模板噪音:
要求在事先就被写下来了(第15.3章)。
也正因为如此,返回类型必须精确 —— -> ::std::same_as<char_type *>
是被检查的,不是猜的。
最后,整套东西按字符类型实例化,但不按输出设备实例化:
print_reserve_define 写的是 char_type *,所以同一份实例化服务
文件、套接字、管道和内存缓冲区。这是设计层面的去模板化
(第15.9章)。
6. 完整程序
#include <fast_io.h>
#include <fast_io_device.h>
#include <cstddef>
#include <cstdint>
#include <string_view>
struct rgb
{
::std::uint_least8_t r{}, g{}, b{};
[[nodiscard]] ::std::string_view name() const noexcept { return "rgb"; }
};
struct rgb_scan_state
{
::std::uint_least32_t value{};
::std::uint_least8_t field{};
::std::uint_least8_t digits{};
};
// ---- printing: a compile-time-known size, then a write into the buffer
template <::std::integral char_type>
inline constexpr ::std::size_t print_reserve_size(::fast_io::io_reserve_type_t<char_type, rgb>) noexcept
{
return 12;
}
template <::std::integral char_type>
inline constexpr char_type *write_uint(char_type *iter, ::std::uint_least32_t v) noexcept
{
char_type buf[3];
::std::size_t n{};
do
{
buf[n++] = static_cast<char_type>(u8'0' + v % 10u);
v /= 10u;
} while (v != 0);
for (; n != 0; --n)
{
*iter++ = buf[n - 1];
}
return iter;
}
template <::std::integral char_type>
inline constexpr char_type *print_reserve_define(::fast_io::io_reserve_type_t<char_type, rgb>, char_type *iter, rgb c) noexcept
{
*iter++ = static_cast<char_type>(u8'(');
iter = write_uint(iter, c.r);
*iter++ = static_cast<char_type>(u8',');
iter = write_uint(iter, c.g);
*iter++ = static_cast<char_type>(u8',');
iter = write_uint(iter, c.b);
*iter++ = static_cast<char_type>(u8')');
return iter;
}
// ---- scanning: a context type, a step function and an EOF handler
template <::std::integral char_type>
inline constexpr ::fast_io::io_type_t<rgb_scan_state>
scan_context_type(::fast_io::io_reserve_type_t<char_type, ::fast_io::parameter<rgb &>>) noexcept
{
return {};
}
template <::std::integral char_type>
inline constexpr ::fast_io::parse_result<char_type const *> scan_context_define(
::fast_io::io_reserve_type_t<char_type, ::fast_io::parameter<rgb &>>, rgb_scan_state &st,
char_type const *begin, char_type const *end, ::fast_io::parameter<rgb &> token) noexcept
{
rgb &c{token.reference};
auto it{begin};
for (; it != end; ++it)
{
char_type const ch{*it};
if (st.field == 0)
{
if (ch != static_cast<char_type>(u8'('))
{
return {it, ::fast_io::freestanding::parse_errc::invalid};
}
st.field = 1;
continue;
}
if (ch >= static_cast<char_type>(u8'0') && ch <= static_cast<char_type>(u8'9'))
{
if (++st.digits > 3)
{
return {it, ::fast_io::freestanding::parse_errc::invalid};
}
st.value = st.value * 10u + static_cast<::std::uint_least32_t>(ch - static_cast<char_type>(u8'0'));
continue;
}
if (ch == static_cast<char_type>(u8',') || ch == static_cast<char_type>(u8')'))
{
if (st.digits == 0 || st.value > 255u)
{
return {it, ::fast_io::freestanding::parse_errc::invalid};
}
auto const channel{static_cast<::std::uint_least8_t>(st.value)};
if (st.field == 1)
{
c.r = channel;
}
else if (st.field == 2)
{
c.g = channel;
}
else
{
c.b = channel;
}
if (ch == static_cast<char_type>(u8')'))
{
if (st.field != 3)
{
return {it, ::fast_io::freestanding::parse_errc::invalid};
}
return {it + 1, ::fast_io::freestanding::parse_errc::ok};
}
++st.field;
st.digits = 0;
st.value = 0;
continue;
}
return {it, ::fast_io::freestanding::parse_errc::invalid};
}
return {it, ::fast_io::freestanding::parse_errc::partial};
}
template <::std::integral char_type>
inline constexpr ::fast_io::freestanding::parse_errc scan_context_eof_define(
::fast_io::io_reserve_type_t<char_type, ::fast_io::parameter<rgb &>>, rgb_scan_state &,
::fast_io::parameter<rgb &>) noexcept
{
return ::fast_io::freestanding::parse_errc::end_of_file;
}
int main()
{
try
{
::fast_io::io::println(rgb{7, 128, 255});
rgb parsed{};
{
::fast_io::ibuf_file inpf(u8"/tmp/rgb.txt");
::fast_io::io::scan(inpf, parsed);
}
::fast_io::io::println("scanned: r=", parsed.r, " g=", parsed.g, " b=", parsed.b);
try
{
::fast_io::ibuf_file bad(u8"/tmp/rgb_bad.txt");
::fast_io::io::scan(bad, parsed);
}
catch throws (::std::error)
{
::fast_io::io::println("bad input rejected");
}
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
当 /tmp/rgb.txt 里是 (7,128,255)、
/tmp/rgb_bad.txt 里是 (300,1,2) 时,输出是:
(7,128,255)
scanned: r=7 g=128 b=255
bad input rejected
300 超过了一路通道能装下的范围,于是步骤函数返回
parse_errc::invalid,fast_io 抛出
[parse](3)Invalid format;上面的处理器打印自己的消息,让运行记录只有一条流。
删掉那个 try,程序就会带着这条诊断终止 —— 对“不知道该怎么处理坏输入”的程序,
这正是正确行为。
7. 经验法则
- 把定义放在类型旁边、它的命名空间里;ADL 就是查找机制,没有注册这一步。
print_reserve_size是不许看值的上界。做不到就用print_define。- 两个打印函数都是
noexcept:会分配的打印函数,就是管道断了会失败的打印函数。 - 解析完成之前绝不修改目标对象;中间结果留在状态对象里。
- 只有“更多输入还有救”时才返回
partial。在输入末尾返回partial会变成end_of_file,而不是死循环,因为scan_context_eof_define存在。 - 用一种字符类型写成模板,就得到所有字符类型;只有文本真的是按字节处理时才写
char专用重载。 - 完整目录 —— 包括
print_alias_define与 scatter 路径, 它们往往比你刚写的那段更短 —— 在 第11.2章。