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.

Danger: You must compute the buffer size correctly. If 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:

  1. scan_context_type — declares the context type
  2. scan_context_define — parses from the buffer, returns partial/ok/invalid
  3. scan_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