Input, output, and formatting
Streams, std::format, and reading input without surprises.
By the end of this chapter you can
- Format values with std::format
- Read input robustly and detect failure
- Explain why iostreams are slow and when it matters
Every program in this book so far has printed with std::cout. That is the
right default, and it is also the part of the standard library with the most
accumulated history — some of it worth knowing, some of it worth avoiding.
Formatting: use std::format
C++20 added std::format, and it replaces both the stream-manipulator dance and
printf’s type-unsafe placeholders.
#include <format>
#include <iomanip>
#include <iostream>
#include <string>
int main() {
const std::string name = "ada";
const double value = 3.14159;
// printf: terse, and the format string is unchecked at compile time.
std::printf("%-8s %6.2f\n", name.c_str(), value);
// iostreams: type-safe, and the manipulators are sticky and verbose.
std::cout << std::left << std::setw(8) << name
<< std::right << std::setw(6) << std::fixed << std::setprecision(2)
<< value << '\n';
// std::format: type-safe, checked at compile time, and reads like the output.
std::cout << std::format("{:<8} {:>6.2f}\n", name, value);
}The format string is checked at compile time. A mismatched placeholder is an error, not a crash:
#include <format>
#include <string>
int main() {
// `{:d}` demands an integer; a std::string is not one.
return std::format("{:d}", std::string{"nope"}).size();
}printf("%d", some_string) compiles and corrupts the stack. That difference is
the reason to switch.
The syntax worth memorising
#include <format>
#include <iostream>
int main() {
std::cout << std::format("plain: {}\n", 42);
std::cout << std::format("width: [{:8}]\n", 42);
std::cout << std::format("left: [{:<8}]\n", 42);
std::cout << std::format("centre: [{:^8}]\n", 42);
std::cout << std::format("fill: [{:*>8}]\n", 42);
std::cout << std::format("precision: {:.3f}\n", 3.14159265);
std::cout << std::format("hex/oct/bin:{:x} {:o} {:b}\n", 255, 255, 255);
std::cout << std::format("with base: {:#x} {:#b}\n", 255, 255);
std::cout << std::format("sign: {:+} {:+}\n", 42, -42);
std::cout << std::format("index: {0}-{1}-{0}\n", "a", "b");
std::cout << std::format("escaped: {{literal braces}}\n");
}std::print (C++23) goes one step further and writes straight to the stream, so
std::print("{}\n", x) replaces std::cout << std::format(...).
Reading input, and detecting failure
Output is easy. Input is where programs go wrong, because input can be anything.
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::istringstream input{"42 notanumber 7"};
int a = 0, b = 0;
input >> a;
std::cout << "first read ok? " << std::boolalpha << static_cast<bool>(input)
<< ", a = " << a << '\n';
input >> b;
std::cout << "second read ok? " << static_cast<bool>(input)
<< ", b = " << b << " <- unchanged, and the stream is now failed\n";
int c = 99;
input >> c;
std::cout << "third read ok? " << static_cast<bool>(input)
<< ", c = " << c << " <- reads do nothing while failed\n";
}Three things to take from that. A failed extraction leaves the variable unchanged (since C++11 it is set to 0, but the value is not what you asked for). The stream enters a failed state. And every subsequent read is a no-op until you clear it — which is why a loop reading numbers without checking spins forever on bad input.
Always test the read:
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::istringstream input{"10 20 30"};
// The idiom: the extraction is the loop condition.
std::vector<int> numbers;
for (int n; input >> n; ) numbers.push_back(n);
std::cout << "read " << numbers.size() << " numbers\n";
// Recovering from bad input requires clearing AND discarding.
std::istringstream messy{"1 x 3"};
int total = 0;
for (int n; ; ) {
if (messy >> n) {
total += n;
} else if (messy.eof()) {
break;
} else {
messy.clear(); // leave the failed state
std::string junk;
messy >> junk; // discard the offending token
std::cout << "skipped \"" << junk << "\"\n";
}
}
std::cout << "total " << total << '\n';
}clear() alone is not enough: the bad characters are still in the buffer, so
the next read fails on the same input. You must consume them too — with a string
extraction as above, or input.ignore(...).
Lines, and the mixing trap
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::istringstream input{"42\nthe rest of the line\n"};
int n = 0;
input >> n; // stops AT the newline, leaving it there
std::string line;
std::getline(input, line); // reads the empty remainder of line 1
std::cout << "surprise: \"" << line << "\"\n";
std::getline(input, line); // now the line you wanted
std::cout << "wanted: \"" << line << "\"\n";
}operator>> skips leading whitespace and stops before the delimiter;
getline reads through the next newline. Mixing them leaves the newline in
the buffer, and the first getline returns empty. The fix is
input.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); after the
>>, or reading lines throughout and parsing each one.
Parsing without exceptions
std::stoi throws and accepts trailing garbage. std::from_chars does neither:
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>
std::optional<int> parse(std::string_view text) {
int value = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
if (error != std::errc{}) return std::nullopt; // not a number, or out of range
if (end != text.data() + text.size()) return std::nullopt; // trailing junk
return value;
}
int main() {
for (std::string_view input : {"42", " 42", "42abc", "abc", "99999999999999999999", "-7"}) {
auto result = parse(input);
std::cout << "\"" << input << "\" -> "
<< (result ? std::to_string(*result) : "rejected") << '\n';
}
}Note that " 42" is rejected — from_chars does not skip leading whitespace,
which is part of being strict. It is the fastest and most predictable way to
parse a number in the standard library, and it never allocates or throws.
Why iostreams are slow
Streams carry formatting state, locale handling, and a virtual interface, and
by default std::cout is kept synchronised with C’s stdout so the two can be
interleaved. That synchronisation is the expensive part, and you can turn it off:
#include <chrono>
#include <iostream>
#include <sstream>
int main() {
constexpr int n = 200'000;
using clock = std::chrono::steady_clock;
using ms = std::chrono::milliseconds;
// Write into a string stream so the measurement is not dominated by the terminal.
auto start = clock::now();
{
std::ostringstream out;
for (int i = 0; i < n; ++i) out << "line " << i << '\n';
}
auto mid = clock::now();
{
std::ostringstream out;
for (int i = 0; i < n; ++i) out << std::format("line {}\n", i);
}
auto finish = clock::now();
std::cout << "operator<<: " << std::chrono::duration_cast<ms>(mid - start).count() << " ms\n";
std::cout << "std::format: " << std::chrono::duration_cast<ms>(finish - mid).count() << " ms\n";
}The other common waste is std::endl, which writes a newline and flushes.
A flush is a system call. In a loop, that is thousands of them for no benefit:
#include <chrono>
#include <iostream>
#include <sstream>
int main() {
constexpr int n = 50'000;
using clock = std::chrono::steady_clock;
using us = std::chrono::microseconds;
std::ostringstream a, b;
auto start = clock::now();
for (int i = 0; i < n; ++i) a << i << std::endl;
auto mid = clock::now();
for (int i = 0; i < n; ++i) b << i << '\n';
auto finish = clock::now();
std::cout << "std::endl: " << std::chrono::duration_cast<us>(mid - start).count() << " us\n";
std::cout << "'\\n': " << std::chrono::duration_cast<us>(finish - mid).count() << " us\n";
std::cout << "(equal output: " << std::boolalpha << (a.str() == b.str()) << ")\n";
}Use '\n'. Flush deliberately with std::flush when you actually need the
output to appear — before a prompt, or before a long computation.
Files
std::ifstream and std::ofstream are RAII types: the destructor closes the
file, so Chapter 3.2’s rules apply and there is nothing to remember.
#include <format>
#include <fstream>
#include <iostream>
#include <string>
int main() {
const char* path = "/tmp/cpptb-io-demo.txt";
{
std::ofstream out{path};
if (!out) { std::cout << "could not open for writing\n"; return 1; }
for (int i = 1; i <= 3; ++i) out << std::format("line {}\n", i);
} // closed here, by the destructor
std::ifstream in{path};
if (!in) { std::cout << "could not open for reading\n"; return 1; }
for (std::string line; std::getline(in, line); ) {
std::cout << "read: " << line << '\n';
}
std::cout << "reached end of file: " << std::boolalpha << in.eof() << '\n';
}Always check the stream after opening. A missing file does not throw by default; it leaves the stream in a failed state, and every read from it silently does nothing.