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:
print_reserve_sizereturns an upper bound, computed with no access to the value. Twelve characters covers(255,255,255).print_reserve_definewrites at most that many characters and returns the new end pointer. The difference between the two is the output.- Neither may fail. Both are
noexcept, which is only possible because the buffer already exists and nothing is allocated (Ch15.8).
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
- Put the definitions next to your type, in its namespace; ADL is the lookup mechanism and there is no registration step.
print_reserve_sizeis an upper bound that must not look at the value. If it cannot, useprint_define.- Both print functions are
noexcept: a printing function that allocates is a printing function that fails on a broken pipe. - Never modify the destination before the parse is complete; keep partial results in the state object.
- Return
partialonly when more input can help. Returningpartialat end of input turns intoend_of_file, not an infinite loop, becausescan_context_eof_defineexists. - Customize one character type with a template and get all of them; do not write
char-only overloads unless the text really is byte-oriented. - The full catalogue — including
print_alias_defineand the scatter path, which are often shorter than what you just wrote — is Ch11.2.