第15.11章 fast_io 如何找到你的打印与扫描代码

English

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_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. 经验法则