I/O: Streams, Formatting & std::print
Master C++ I/O from classic iostream to modern std::format and std::print, including stream states, file I/O, and common buffer pitfalls.
The Evolution of C++ I/O
C++ I/O has evolved significantly across the language's history. The classic iostream library (std::cin, std::cout) has been the standard since C++98, but it has well-known ergonomic and performance issues. C++20 introduced std::format, a type-safe formatting library inspired by Python's str.format(). C++23 added std::print, which combines formatting and output in a single call.
Understanding all three approaches is important: iostream is everywhere in existing code, std::format is the modern way to format strings, and std::print is the future of C++ output.
Classic I/O: cout, cin, cerr
The iostream library provides stream objects for standard input, output, and error output:
#include <iostream>
#include <string>
#include <limits> // For numeric_limits
int main() {
// Output with cout
std::cout << "Hello, " << "World!" << '\n'; // Chained insertion
std::cout << "The answer is: " << 42 << '\n'; // Automatic type formatting
// Error output — cerr is unbuffered (immediate), clog is buffered
std::cerr << "Error: something went wrong\n";
// Input with cin
std::cout << "Enter your name: ";
std::string name;
std::getline(std::cin, name); // Read entire line (including spaces)
std::cout << "Enter your age: ";
int age{};
std::cin >> age; // Read a single value (stops at whitespace)
// Check if input succeeded
if (std::cin.fail()) {
std::cerr << "Invalid input!\n";
std::cin.clear(); // Reset error flags
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // Discard bad input
}
// The getline-after-cin trap:
// After cin >> age, a '\n' remains in the buffer.
// A subsequent getline() would read an empty string!
// Fix: call cin.ignore() before getline():
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::string city;
std::cout << "Enter your city: ";
std::getline(std::cin, city); // Now works correctly
std::cout << name << " (" << age << ") from " << city << '\n';
}Stream State Flags
Every stream maintains a set of state flags that indicate its current condition:
- goodbit — No errors. The stream is ready for I/O operations.
- eofbit — End of file/input has been reached. Set when the stream attempts to read past the end.
- failbit — A logical error occurred (e.g., trying to read an integer but encountering text). The stream is still usable after calling clear().
- badbit — An irrecoverable I/O error occurred (e.g., disk failure). The stream is typically unusable.
You check these with stream.good(), stream.eof(), stream.fail(), stream.bad(), or by using the stream in a boolean context: if (std::cin) is equivalent to if (!std::cin.fail()).
A critical pattern: always check the stream state after input operations, not before. The common idiom while (std::cin >> value) reads and checks in one step — it stops when the read fails.
The most common I/O bug in C++ involves mixing >> (formatted extraction) with std::getline(). The >> operator reads a value but leaves the trailing newline in the buffer. A subsequent std::getline() then reads that leftover newline as an empty string, appearing to skip the input.
The fix is to call std::cin.ignore(std::numeric_limits between >> and std::getline() to discard the leftover newline.
Another approach is to use std::getline() for all input and parse the strings yourself with std::istringstream or std::stoi/std::stod. This avoids the problem entirely and gives you better error handling.
- After cin >> x, a newline remains in the buffer — the next getline() reads it as an empty string
- Fix: call cin.ignore(numeric_limits
::max(), '\n') between >> and getline() - Alternative: use getline() for ALL input and parse strings with stoi/stod or istringstream
- Always check stream state after input: if (cin >> x) rather than reading blindly
File I/O
File I/O uses the same stream interface as console I/O, with std::ifstream for reading and std::ofstream for writing:
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
int main() {
// Writing to a file
{
std::ofstream out("scores.txt"); // Opens for writing (creates or truncates)
if (!out) {
std::cerr << "Failed to open file for writing\n";
return 1;
}
out << "Alice 95\n";
out << "Bob 87\n";
out << "Charlie 92\n";
} // File is automatically closed when 'out' goes out of scope (RAII!)
// Reading from a file
std::ifstream in("scores.txt");
if (!in) {
std::cerr << "Failed to open file for reading\n";
return 1;
}
std::string name;
int score{};
std::vector<std::pair<std::string, int>> results;
while (in >> name >> score) { // Read until failure (EOF or bad data)
results.emplace_back(name, score);
}
for (const auto& [n, s] : results) {
std::cout << n << ": " << s << '\n';
}
// Appending to a file
std::ofstream append("scores.txt", std::ios::app); // Open in append mode
append << "Diana 98\n";
// Reading entire file line by line
std::ifstream in2("scores.txt");
std::string line;
while (std::getline(in2, line)) {
std::cout << "Line: " << line << '\n';
}
}Modern Formatting: std::format & std::print
C++20's std::format and C++23's std::print provide type-safe, Python-like formatting that is both safer and more readable than iostream manipulators:
#include <format> // C++20
#include <print> // C++23
#include <string>
#include <vector>
#include <numbers> // C++20: mathematical constants
int main() {
// std::format (C++20) — returns a formatted std::string
std::string msg = std::format("Hello, {}!", "World");
// msg == "Hello, World!"
// Positional arguments
std::string pos = std::format("{1} comes before {0}", "second", "first");
// pos == "first comes before second"
// Formatting specifiers
std::string nums = std::format(
"int: {:>10d}\n" // Right-aligned, width 10, decimal
"hex: {:#06x}\n" // '0x' prefix, width 6, zero-padded hex
"float: {:.3f}\n" // 3 decimal places, fixed notation
"sci: {:.2e}\n" // 2 decimal places, scientific notation
"pi: {:.10f}", // 10 decimal places
42, 255, 3.14159, 12345.6789, std::numbers::pi
);
// std::print (C++23) — format + output in one step
std::print("Name: {}, Age: {}\n", "Alice", 30);
std::print("Pi is approximately {:.6f}\n", std::numbers::pi);
// std::println (C++23) — like print but adds a newline
std::println("Score: {:>5}", 95); // "Score: 95"
std::println("Score: {:>5}", 100); // "Score: 100"
// Formatting containers — print each element
std::vector<int> values = {10, 20, 30, 40, 50};
for (std::size_t i = 0; i < values.size(); ++i) {
std::println("[{:2}] = {:>4}", i, values[i]);
}
// Why prefer std::format/print over iostream?
// 1. Type-safe (unlike printf)
// 2. Readable format strings (unlike iostream manipulators)
// 3. No state leakage (iostream manipulators are sticky!)
// 4. Better performance in many cases
}Modern C++ gives you three I/O approaches. Here is when to use each:
Use std::print/std::println (C++23) for new code when your compiler supports it. It is the cleanest syntax, type-safe, and avoids the stateful manipulator problems of iostream.
Use std::format (C++20) when you need a formatted string (not direct output), or when your compiler supports C++20 but not C++23.
Use iostream when interacting with code that expects stream objects, when you need the stream abstraction (e.g., reading from cin, file I/O), or when working with legacy codebases.
Avoid printf/scanf from C — they are not type-safe (passing the wrong format specifier is undefined behavior) and do not work with C++ types like std::string.
- Prefer std::print/println (C++23) for new output code — cleanest and safest
- Use std::format (C++20) when you need a formatted string, not direct output
- iostream is still needed for cin, file I/O, and stream-based abstractions
- Avoid printf/scanf — they are not type-safe and cannot handle std::string directly
- Always check stream state after input operations — use 'if (cin >> x)' or 'while (getline(in, line))'
- Mixing cin >> and getline() causes the newline-in-buffer bug — call cin.ignore() between them
- File streams use RAII — they close automatically when they go out of scope
- std::format (C++20) provides type-safe, Python-like string formatting
- std::print/println (C++23) combines formatting and output — the preferred way for new code
- iostream manipulators are stateful (sticky) — std::format avoids this problem entirely
Quiz — Test Your Knowledge
(10 XP)1. What happens if you call `std::getline(std::cin, str)` immediately after `std::cin >> number`?
2. What does `std::format("{:#06x}", 255)` produce?
3. Why do file streams (ifstream/ofstream) automatically close when they go out of scope?