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:
"r"— read (file must exist)"w"— write (creates or truncates)"a"— append (creates or appends)"r+"— read and write (file must exist)"w+"— read and write (creates or truncates)"a+"— read and append (creates or appends)- Add
"b"for binary mode:"rb","wb", etc.
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:
%dor%i— signed integer%u— unsigned integer%x,%X— hexadecimal (lowercase/uppercase)%o— octal%f— floating-point (decimal notation)%e,%E— floating-point (scientific notation)%g,%G— floating-point (shortest representation)%s— string%c— character%p— pointer%%— literal%
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:
- Buffer overflows with
%s(usefgetsinstead) - Format string vulnerabilities (CWE-134: Use of Externally-Controlled Format String) if format is user-controlled — attackers can read/write arbitrary memory
- Difficult error handling (checking return values is complex)
- Whitespace handling is confusing and error-prone
- No bounds checking on numeric input
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:
- Type specifiers:
%d,%i,%u,%x,%X,%o,%f,%F,%e,%E,%g,%G,%a,%A,%s,%c,%p,%n - Length modifiers:
h,hh,l,ll,j,z,t,L - Flags:
-,+,,#,0 - Width and precision:
%10d,%.2f,%10.2f,%*d
Combinations like %#0+10.5lld are valid but nearly impossible to remember.
This complexity makes printf extremely error-prone:
- Wrong type specifier for the argument type (e.g.,
%dforlong) - Wrong length modifier (e.g., missing
lforlong) - Forgetting precision for floating-point
- Mixing up similar specifiers (
%xvs%X,%fvs%e)
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:
stdin— standard input (usually keyboard)stdout— standard output (usually terminal)stderr— standard error (usually terminal, unbuffered)
#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:
::fast_io::open_mode::in— read::fast_io::open_mode::out— write (creates or truncates)::fast_io::open_mode::append— append- Combine with
|:::fast_io::open_mode::in | ::fast_io::open_mode::out - Add
::fast_io::open_mode::binaryfor binary mode
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:
- You're in a single-threaded context
- You're synchronizing manually with
::fast_io::native_mutex - You need maximum performance and understand the threading implications
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
-
C stdio requires checking errors on most function calls (though
fclosefailure is typically not recoverable). Most code ignores these errors, leading to silent data loss. -
::fast_io::c_filewrapsFILE*with RAII — the file is closed automatically. -
::fast_io::c_io_observeris a non-owning view over an existingFILE*. -
The
_unlockedvariants skip internal locking. Use::fast_io::native_mutexto synchronize them manually when needed. -
Format strings should only contain format specifiers (
%s,%d, etc.), nothing else. Never include literal text, newlines, or other characters in the format string. Put all literal content in the arguments instead:printf("%s", "text\n"), notprintf("text\n")orprintf("%s\n", "text"). Or better, usefputsfor plain strings or::fast_io::io::printfor type-safe output. Never pass user-controlled strings as format strings (CWE-134: Use of Externally-Controlled Format String). -
Always use
::std::addressof()instead of&when passing pointers toscanf/fscanf/sscanf. This works correctly even if the type has overloadedoperator&. -
printfformat specifiers are extremely complex and nearly impossible to remember correctly (see cppreference). This complexity leads to constant bugs and security vulnerabilities. -
Prefer
::fast_io::io::print/println/scanover the C functions — they are type-safe, faster, handle errors explicitly, and immune to format string attacks.