Ch11.9: Relationship with String

One System, Everywhere

The concat_fast_io and to<T> APIs introduced in Ch9.12 are not a separate string-formatting pipeline — they are the same I/O system that powers print, println, and scan. Every string operation in fast_io funnels through the library's core I/O layer.

This means the customisation points you learned for console and file output — print_define and print_reserve_define — automatically make your types work with string APIs too. There is nothing extra to implement.

concat_fast_io Uses print_reserve_define

When you call concat_fast_io, the library formats each argument into a string by invoking the same print_reserve_define (or print_define) that println uses. If your type is printable to the console, it is printable into a string — for free.

For example, suppose we defined print_reserve_define for a Point struct back in Ch11.2:


#include <fast_io.h>
#include <fast_io_dsal/string.h>

struct Point {
    double x;
    double y;
};

inline constexpr void print_reserve_define(::fast_io::io_reserve_type_t<char, Point>,
                                           char* iter,
                                           Point const& p) {
    // Format: "(x, y)"
    *iter++ = '(';
    iter = ::fast_io::detail::print_reserve_integral(iter, p.x);
    *iter++ = ',';
    *iter++ = ' ';
    iter = ::fast_io::detail::print_reserve_integral(iter, p.y);
    *iter++ = ')';
}

int main() {
    using namespace ::fast_io::iomnp;
    Point p{3.0, 4.0};

    // Works on the console
    println("Console: ", p);

    // Also works in concat_fast_io — same customisation point
    ::fast_io::string s = ::fast_io::concat_fast_io("Point: ", p);
    // s == "Point: (3, 4)"

    println(s);
}

No separate to_string function, no std::format specialisation, no operator<< overload. One definition, every output target.

to<T> Uses the Same I/O Pipeline

to<T> works in two stages, both of which go through the standard I/O pipeline:

  1. Format — the arguments are formatted into a temporary string using concat_fast_io (and therefore print_reserve_define).
  2. Parse — the resulting string is scanned into the target type T using scan_define, the same customisation point that scan uses.

#include <fast_io.h>
#include <fast_io_dsal/string.h>

int main() {
    using namespace ::fast_io::iomnp;
    // Format 42 as a string ("42"), then parse it back as a double
    double d = ::fast_io::to<double>(42);  // d == 42.0

    // Concatenate multiple arguments, then parse the result
    ::std::size_t n = ::fast_io::to<::std::size_t>(1zu, 23zu);
    // "1" + "23" -> "123" -> 123

    println("d = ", d, ", n = ", n);
}

Because both stages reuse the standard pipeline, any custom type that implements print_reserve_define and scan_define can round-trip through to<T> without any additional glue code.

The Unified Design

The takeaway is that fast_io has one set of customisation points for every output target:

Customisation Point Used By
print_define / print_reserve_define println, print, concat_fast_io, concatln_fast_io, to<T>
scan_define / scan_context_define scan, to<T> (parse stage)

Console, file, buffered I/O, string, socket — they all share the same formatting and parsing layer. Define your type once and it works everywhere.

Writing to Existing Strings: {chtype}ostring_ref_fast_io

While concat_fast_io creates a new string, sometimes you want to format output directly into an existing string buffer. The ::fast_io::{chtype}ostring_ref_fast_io family of types lets you do exactly that. These are non-owning reference types that wrap an existing string and expose it as an output device for print, println, and other output operations.

The {chtype} placeholder is replaced with the character type:

You construct a reference by passing a pointer to an existing string. The string is not cleared before writing — output is appended at the current position. When you are done, the string contains everything that was written.


#include <fast_io.h>
#include <fast_io_dsal/string.h>

int main()
{
    using namespace ::fast_io::iomnp;

    ::fast_io::u8string result;

    // Wrap the existing string as an output device
    ::fast_io::u8ostring_ref_fast_io ref(result);

    // Write to the string using print/println
    print(ref, "Hello, ");
    print(ref, "World!");
    println(ref);  // adds newline

    // The string now contains "Hello, World!\n"
    println("Result: ", result);

    // You can also write structured data
    ::fast_io::u8string buffer;
    ::fast_io::u8ostring_ref_fast_io buf_ref(buffer);

    ::std::size_t const x{42zu};
    ::std::size_t const y{100zu};
    println(buf_ref, "Point: (", x, ", ", y, ")");

    // buffer now contains "Point: (42, 100)\n"
    println(buffer);
}

This is particularly useful when you need to build a string incrementally, or when you want to reuse a string buffer to avoid repeated allocations. The reference type itself is lightweight — it just holds a pointer to the string.

Note that these types are different from ::fast_io::u8string and similar owning string types. The _ref_ in the name indicates that these are non-owning references — they do not manage the lifetime of the underlying string. You must ensure the string outlives the reference.

Key Takeaways