Ch11.2: Defining Custom Types for I/O
Overview
fast_io uses customization points to let
you define how your types are printed and scanned. There are four main
customization points, each with different trade-offs.
1. Customization Points Overview
| Customization Point | Best For | Speed | Safety |
|---|---|---|---|
print_reserve_define |
Fixed-size types (ints, floats, enums, small structs) | Fastest | Unsafe if size is computed wrong |
print_scatter_define |
Types already stored as contiguous strings (string literals, enum names) | Zero-copy | Safe |
print_define |
Complex/composite types (most flexible) | Moderate | Safe |
scan_context_define |
Types that span multiple buffer fills (streaming parse) | N/A (input) | Safe |
2. print_reserve_define — Fastest (but dangerous)
print_reserve_define writes directly into the I/O buffer.
It is the fastest method — even faster than
returning an array, because it reuses the existing I/O buffer memory
with zero extra allocation or copying.
print_reserve_size returns a value that is too small,
you will overflow the buffer — this is undefined behaviour and
can corrupt memory.
#include <fast_io.h>
struct Point {
int x;
int y;
};
// Step 1: Tell fast_io the maximum buffer size needed
template <::std::integral char_type>
inline constexpr ::std::size_t print_reserve_size(
::fast_io::io_reserve_type_t<char_type, Point>) noexcept
{
// Two ints, a comma, parentheses, and a space
// Max int: 10 digits + sign = 11 chars each
// "(x, y)" = 1 + 11 + 2 + 11 + 1 = 26
return 26;
}
// Step 2: Write into the buffer, return pointer past last written char
template <::std::integral char_type>
inline constexpr char_type* print_reserve_define(
::fast_io::io_reserve_type_t<char_type, Point>,
char_type* iter, Point p) noexcept
{
// Use character literals appropriate for each character type
// This handles EBCDIC and other non-ASCII encodings correctly
if constexpr (::std::same_as<char_type, char>) {
*iter++ = '(';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.x);
*iter++ = ',';
*iter++ = ' ';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.y);
*iter++ = ')';
} else if constexpr (::std::same_as<char_type, char8_t>) {
*iter++ = u8'(';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.x);
*iter++ = u8',';
*iter++ = u8' ';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.y);
*iter++ = u8')';
} else if constexpr (::std::same_as<char_type, char16_t>) {
*iter++ = u'(';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.x);
*iter++ = u',';
*iter++ = u' ';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.y);
*iter++ = u')';
} else if constexpr (::std::same_as<char_type, char32_t>) {
*iter++ = U'(';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.x);
*iter++ = U',';
*iter++ = U' ';
iter = ::fast_io::detail::print_reserve_define_integer<10>(iter, p.y);
*iter++ = U')';
}
return iter;
}
int main() {
using namespace ::fast_io::iomnp;
Point p{42, 100};
println("Point: ", p); // Output: Point: (42, 100)
}
When to use: When your type has a fixed, small textual representation and you can compute the size precisely at compile time.
3. print_scatter_define — Zero-copy
print_scatter_define returns a pointer and length to
existing data. No copying is needed — the I/O system references
your data directly.
#include <fast_io.h>
enum class Color { Red, Green, Blue };
template <::std::integral char_type>
inline constexpr ::fast_io::basic_io_scatter_t<char_type>
print_scatter_define(::fast_io::io_reserve_type_t<char_type, Color>, Color c) noexcept
{
// Use character type-specific string literals
if constexpr (::std::same_as<char_type, char>) {
switch (c) {
case Color::Red: return {"Red", 3};
case Color::Green: return {"Green", 5};
case Color::Blue: return {"Blue", 4};
}
return {"Unknown", 7};
} else if constexpr (::std::same_as<char_type, char8_t>) {
switch (c) {
case Color::Red: return {u8"Red", 3};
case Color::Green: return {u8"Green", 5};
case Color::Blue: return {u8"Blue", 4};
}
return {u8"Unknown", 7};
} else if constexpr (::std::same_as<char_type, char16_t>) {
switch (c) {
case Color::Red: return {u"Red", 3};
case Color::Green: return {u"Green", 5};
case Color::Blue: return {u"Blue", 4};
}
return {u"Unknown", 7};
} else if constexpr (::std::same_as<char_type, char32_t>) {
switch (c) {
case Color::Red: return {U"Red", 3};
case Color::Green: return {U"Green", 5};
case Color::Blue: return {U"Blue", 4};
}
return {U"Unknown", 7};
}
}
int main() {
using namespace ::fast_io::iomnp;
println("Color: ", Color::Green); // Output: Color: Green
}
When to use: When your type’s textual representation is already a contiguous string in memory (string literals, enum names, etc.).
4. print_define — Most flexible
print_define writes to an output stream using
print_freestanding. It is the most flexible but also the
slowest of the three print methods (still fast — just not as fast
as print_reserve_define).
#include <fast_io.h>
struct Person {
::fast_io::string name;
int age;
};
template <::std::integral char_type, typename T>
inline constexpr void print_define(
::fast_io::io_reserve_type_t<char_type, Person>,
T& out, Person const& p)
{
print_freestanding(out, p.name, " (age: ", p.age, ")");
}
int main() {
using namespace ::fast_io::iomnp;
Person alice{::fast_io::string{"Alice"}, 30};
println("Person: ", alice); // Output: Person: Alice (age: 30)
}
When to use: For complex types where the output depends on runtime data, or when you are composing multiple sub-objects.
5. scan_context_define — Multi-call scanning
scan_context_define is for types whose textual
representation may span multiple buffer fills. It uses a
context to track parsing state across calls.
You must provide three functions:
scan_context_type— declares the context typescan_context_define— parses from the buffer, returns partial/ok/invalidscan_context_eof_define— handles end-of-input
#include <fast_io.h>
struct MyType {
int value;
};
// Context type holds partial parse state
struct MyContext {
int accumulated{};
bool started{};
};
// Step 1: Declare context type
template <::std::integral char_type>
inline constexpr auto scan_context_type(
::fast_io::io_reserve_type_t<char_type, MyType>) noexcept
{
return ::fast_io::io_type_t<MyContext>{};
}
// Step 2: Parse from buffer
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, MyType>,
MyContext& ctx,
char_type const* first, char_type const* last,
MyType& t) noexcept
{
// Parse digits, accumulating into ctx.accumulated
for (auto it = first; it != last; ++it) {
char_type ch = *it;
if (ch >= static_cast<char_type>('0') &&
ch <= static_cast<char_type>('9')) {
ctx.accumulated = ctx.accumulated * 10 +
(static_cast<int>(ch) - static_cast<int>('0'));
ctx.started = true;
} else {
t.value = ctx.accumulated;
return {it, ::fast_io::parse_code::ok};
}
}
return {last, ::fast_io::parse_code::partial}; // need more data
}
// Step 3: Handle EOF
template <::std::integral char_type>
inline constexpr ::fast_io::parse_code
scan_context_eof_define(
::fast_io::io_reserve_type_t<char_type, MyType>,
MyContext& ctx, MyType& t) noexcept
{
if (ctx.started) {
t.value = ctx.accumulated;
return ::fast_io::parse_code::ok;
}
return ::fast_io::parse_code::invalid;
}
6. Which One Should I Use?
| Your type is... | Use... |
|---|---|
| Fixed-size, computable at compile time (ints, small structs) | print_reserve_define (fastest) |
| Already a contiguous string (enum names, literals) | print_scatter_define (zero-copy) |
| Complex, composed of multiple parts | print_define (most flexible) |
| May span multiple buffer reads (streaming parse) | scan_context_define |
Key takeaways
print_reserve_defineis the fastest — writes directly into the I/O buffer with zero extra allocation. But compute the size correctly or you will overflow the buffer.print_scatter_definereturns a pointer to existing data — zero copy. Best for enum names and string-like types.print_defineis the most flexible — writes to a stream usingprint_freestanding.scan_context_definehandles types that span multiple buffer fills — uses a context to track parse state.- Choose the simplest method that fits your type. Use
print_reserve_defineonly when you can compute the size precisely.