Ch11.11: C-Style I/O

Overview

C provides a set of I/O facilities built around FILE*, along with functions such as fopen, fclose, printf, scanf, fprintf, and fscanf. These are the oldest and most widely used I/O primitives in the C and C++ ecosystems.

fast_io wraps the C FILE* interface so that you can use it with print/println/scan while keeping RAII semantics, type safety, and performance.

The Problem with Error Handling

One of the biggest issues with C stdio is that most functions can fail, and proper error handling requires checking the return value of most calls:


#include <stdio.h>

int main() {
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("fopen failed");
        return 1;
    }

    if (fprintf(fp, "%s", "Hello\n") < 0) {
        perror("fprintf failed");
        fclose(fp);
        return 1;
    }

    // fclose can fail, but there's nothing you can do about it
    fclose(fp);

    return 0;
}

This is extremely error-prone. Most C programs don’t check errors from fprintf or even printf itself. Even printf can fail (e.g., broken pipe, disk full, I/O error), but almost no one checks its return value.

fast_io makes error handling explicit. Functions throw exceptions on error by default, or return error codes that you can’t accidentally ignore. This makes it much harder to write buggy I/O code.

Standard C I/O: <cstdio>

Before discussing how fast_io wraps C I/O, let’s review the standard C I/O API itself. All C I/O functions are declared in <cstdio> (or <stdio.h> in C). The core abstraction is FILE*, an opaque pointer to a stream that represents an open file, pipe, or device.

Opening and Closing Files


#include <cstdio>

int main() {
    // Open a file for writing
    FILE* fp = std::fopen("output.txt", "w");
    if (!fp) {
        return 1;  // Failed to open
    }

    // ... use the file ...

    // Always close when done
    std::fclose(fp);
}

The mode string controls how the file is opened:

Formatted Output: printf, fprintf


#include <cstdio>

int main() {
    int x = 42;
    double pi = 3.14159;
    char const* name = "Alice";

    // printf: write to stdout
    std::printf("%s%s%s%d%s%.2f%s", "Name: ", name, ", Age: ", x, ", Pi: ", pi, "\n");

    // fprintf: write to a FILE*
    FILE* fp = std::fopen("data.txt", "w");
    std::fprintf(fp, "%s%d%s%f%s", "x = ", x, ", pi = ", pi, "\n");
    std::fclose(fp);

    // sprintf: write to a string buffer
    char buffer[100];
    std::sprintf(buffer, "%s%d", "Value: ", x);
}

Common format specifiers:

Formatted Input: scanf, fscanf


#include <cstdio>
#include <memory>

int main() {
    int x;
    double y;

    // scanf: read from stdin
    std::fputs("Enter two numbers: ", stdout);
    std::scanf("%d%lf", ::std::addressof(x), ::std::addressof(y));

    // fscanf: read from a FILE* (format string has only specifiers)
    FILE* fp = std::fopen("data.txt", "r");
    std::fscanf(fp, "%d%lf", ::std::addressof(x), ::std::addressof(y));  // Input file should contain: 42 3.14
    std::fclose(fp);

    // sscanf: read from a string
    char const* input = "42 3.14";
    std::sscanf(input, "%d%lf", ::std::addressof(x), ::std::addressof(y));
}

Warning: The scanf family is extremely dangerous and should be avoided. Problems include:

Use ::fast_io::io::scan instead, which is type-safe, handles errors explicitly, and immune to these issues.

The Complexity of printf Format Specifiers

The printf family has an extremely complex format specifier syntax. From cppreference, format specifiers include:

Combinations like %#0+10.5lld are valid but nearly impossible to remember. This complexity makes printf extremely error-prone:

Most programmers cannot remember all these rules correctly. This is why format string bugs and I/O buffer overflows are among the most common security vulnerabilities. Nearly all command-line I/O buffer exploits come from stdio misuse.

fast_io eliminates this complexity entirely. There are no format strings to remember. The library automatically determines the correct formatting based on the argument type, making it impossible to mismatch types and format specifiers.

Character and Line I/O


#include <cstdio>

int main() {
    FILE* fp = std::fopen("example.txt", "r");

    // Read a single character
    int ch = std::fgetc(fp);

    // Read a line (up to n-1 characters or newline)
    char buffer[256];
    std::fgets(buffer, sizeof(buffer), fp);

    // Write a single character
    std::fputc('A', stdout);

    // Write a string
    std::fputs("Hello, World!\n", stdout);

    std::fclose(fp);
}

Binary I/O


#include <cstdio>

int main() {
    // Write binary data
    FILE* fp = std::fopen("data.bin", "wb");
    int numbers[] = {1, 2, 3, 4, 5};
    std::fwrite(numbers, sizeof(int), 5, fp);
    std::fclose(fp);

    // Read binary data
    fp = std::fopen("data.bin", "rb");
    int buffer[5];
    std::size_t count = std::fread(buffer, sizeof(int), 5, fp);
    std::fclose(fp);
}

Standard Streams

C provides three predefined streams:


#include <cstdio>

int main() {
    std::fputs("This goes to stdout\n", stdout);
    std::fputs("This goes to stderr\n", stderr);

    // printf() is equivalent to fputs(..., stdout)
    std::fputs("Same as stdout\n", stdout);
}

Using C Files with fast_io

fast_io provides RAII wrappers for FILE* so you can use print/println/scan with C files while ensuring proper resource management.

::fast_io::c_file: RAII Wrapper

::fast_io::c_file wraps a FILE* and automatically closes it when the object goes out of scope. This prevents resource leaks.


#include <fast_io.h>
#include <fast_io_device.h>

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

    // Open a file for writing (RAII)
    ::fast_io::c_file cf(u8"output.txt", ::fast_io::open_mode::out);

    // Use fast_io's print/println
    println(cf, "Hello, " "World!"
        "The answer is: ", 42);

    // File is automatically closed when cf goes out of scope
}

The mode parameter works the same as fopen:

Reading from C Files


#include <fast_io.h>
#include <fast_io_device.h>

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

    // Open for reading
    ::fast_io::c_file cf(u8"data.txt", ::fast_io::open_mode::in);

    // Read values
    int x;
    double y;
    ::fast_io::string name;

    scan(cf, x);
    scan(cf, y);
    scan(cf, ::fast_io::mnp::str_line_get(name));  // read a line

    println("x = ", x, "\n"
      "y = ", y, "\n"
      "name = ", name);
}

::fast_io::c_io_observer: Non-Owning View

Sometimes you have a FILE* from another source (e.g., passed as a parameter) and you want to use fast_io's API without taking ownership. Use ::fast_io::c_io_observer for this:


#include <fast_io.h>
#include <fast_io_device.h>
#include <cstdio>

void process_file(::fast_io::c_io_observer observer) {
    using namespace ::fast_io::iomnp;
    // Use fast_io's API without taking ownership
    print(observer, "Processing...\n");
}

int main() {
    // Open with C API
    FILE* fp = std::fopen("data.txt", "w");
    ::fast_io::c_file cf(fp);
    // Wrap it in an observer (non-owning)
    ::fast_io::c_io_observer observer{fp};

    process_file(observer);
}

The observer does not close the file when it goes out of scope — the original owner is responsible for that.

Accessing the Underlying FILE*

You can access the underlying FILE* via the .fp member:


#include <fast_io.h>
#include <fast_io_device.h>
#include <cstdio>

int main() {
    ::fast_io::c_file cf(u8"data.txt", ::fast_io::open_mode::out);

    // Use fast_io's API
    println(cf, "fast_io output");

    // Access the underlying FILE* for C API calls
    std::fprintf(cf.fp, "%s", "C API output\n");

    // File is closed automatically
}

Thread Safety: _unlocked Variants

C's FILE* operations are thread-safe by default — each operation acquires an internal lock. However, this can be slow. fast_io provides _unlocked variants that skip the internal locking:


#include <fast_io.h>
#include <fast_io_device.h>

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

    // Unlocked variant (no internal locking)
    ::fast_io::c_file_unlocked cf(u8"data.txt", ::fast_io::open_mode::out);

    println(cf, "Fast, but not thread-safe by default");

    // If you need thread safety, use ::fast_io::native_mutex
    {
        ::fast_io::native_mutex_lock lock(cf);
        println(cf, "Thread-safe output");
    }
}

Use c_file_unlocked when:

Interoperability with C Libraries

Many C libraries expect FILE* parameters. You can pass the underlying FILE* from c_file or c_io_observer:


#include <fast_io.h>
#include <fast_io_device.h>
#include <cstdio>

// C library function
void c_library_function(FILE* fp) {
    std::fprintf(fp, "%s", "From C library\n");
}

int main() {
    ::fast_io::c_file cf(u8"data.txt", ::fast_io::open_mode::out);

    // Use fast_io
    println(cf, "From fast_io");

    // Pass to C library
    c_library_function(cf.fp);

    // More fast_io
    println(cf, "Back to fast_io");
}

Appendix

For the complete C I/O function reference, see Ch11.11.1: Complete C I/O Function Reference.

Key Takeaways