Ch15.11: How fast_io Finds Your Print and Scan Code

中文(简体)

1. Not an Operator, and Not a Member

::fast_io::io::println(rgb{7, 128, 255}) prints (7,128,255) only because of two free functions you write yourself. There is no operator<<, no to_string(), no virtual format(). fast_io calls free functions, and it finds them by argument-dependent lookup on a tag.

The catalogue of customization points is Ch11.2. This page is about the mechanism underneath it, because that mechanism is pure generic programming: a variable template, a concept, and overload resolution.

2. The Tag: a Variable Template

The first argument of every customization point is not the value being printed. It is a tag that says which character type and which value type this definition is for:


::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 is a variable template (Ch15.4), an inline constexpr object whose only job is to be passed by value. That choice solves a problem a function template cannot: you cannot partially specialize a function template, so “specialise the printing of rgb” needs something else to specialise — and a class-type tag can be overloaded on instead. Then ordinary overload resolution, with ADL, picks your function:


template <::std::integral char_type>
inline constexpr ::std::size_t print_reserve_size(::fast_io::io_reserve_type_t<char_type, rgb>) noexcept;

Two consequences follow, and both matter. Your definition must live in a namespace associated with either rgb or ::fast_io — usually the global namespace next to rgb. And the char_type is a template parameter, so one definition serves char, char8_t, wchar_t and everything else.

3. Printing: Size First, Then Bytes

fast_io's fastest printing path writes into a buffer it has already reserved. Two functions describe it: one says how many characters are needed at most, the other fills them in.


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

The contract is deliberately narrow, and it is what makes this path fast:

If your type's size cannot be known without looking at the value, you cannot use this path: use print_define, which hands you the output device instead of a buffer, or print_scatter_define if the text is already in memory. Both are demonstrated in Ch11.2.

4. Scanning: a State, a Step, and an End

Scanning may need more input than is currently in the buffer, so it cannot be a single function call. fast_io's answer is the same shape as a hand-written parser: a state that survives between calls, a step that consumes what is available, and an EOF handler that gives the final verdict.


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 names the state type without constructing it, through ::fast_io::io_type_t<S>. The step function then returns a parse result: an iterator pointing just past what it consumed, plus a code.


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
Code Meaning What fast_io does
parse_errc::ok the value is complete advances the buffer and continues with the next argument
parse_errc::partial valid so far, needs more input keeps the state, fills the buffer, calls again
parse_errc::invalid cannot be parsed raises a herbception, [parse](3)Invalid format
parse_errc::end_of_file input ended inside the value reported by scan_context_eof_define

The parameter is ::fast_io::parameter<rgb &>, not rgb, and that is the interesting part. An lvalue you pass to ::fast_io::io::scan is meant to be written into, so fast_io wraps it in ::fast_io::parameter<rgb &>, a one-member struct holding the reference. Keying the customization on the wrapper keeps the tag derived from a normal value type while still letting the step function write to your object. The payload is token.reference.

Note also what the step function does not do: it never writes to c.r before the whole colour is validated. Partial results live in the state, and the destination is only modified at the moment the parse succeeds — the strong exception-safety guarantee of Ch14.9, applied to parsing.

5. The Concepts That Gate It

Nothing checks your signatures by hand. fast_io_core_impl/concepts/operation.h defines one concept per customization point, and println simply requires one of them:


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

This is why a missing customization produces type not scannable. need context_scannable rather than a page of template noise: the requirement was written down in advance (Ch15.3). It is also why the exact return types matter — -> ::std::same_as<char_type *> is checked, not guessed.

Finally, the whole thing is instantiated per character type but not per output device: print_reserve_define writes to a char_type *, so the same instantiation serves files, sockets, pipes and memory buffers. That is detemplatization (Ch15.9) at the design level.

6. The Whole Program


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

With /tmp/rgb.txt containing (7,128,255) and /tmp/rgb_bad.txt containing (300,1,2), the output is:


(7,128,255)
scanned: r=7 g=128 b=255
bad input rejected

300 is larger than a channel can hold, so the step function returns parse_errc::invalid and fast_io raises [parse](3)Invalid format; the handler above prints its own message so the transcript stays one stream. Delete the try and the program terminates with that diagnostic — which is the correct behaviour for malformed input in a program that has no idea what to do about it.

7. Rules of Thumb