C++ Part I · Module 23 · Files, Streams, and Data Processing
Formatted Output and Manipulators
Output has two audiences and they want the same thing. A human reading a simulation report wants aligned columns, because a misaligned column is unreadable at a hundred lines. A script consuming the same report wants fixed field widths and a stable number format, because that is what makes a file greppable, sortable and diffable. Both audiences are served by the same small set of controls — width, fill, alignment, numeric base, and floating-point precision — and the whole chapter is about applying them deliberately rather than by trial and error. There is one mechanical fact that causes more output bugs than anything else, and it is that most manipulators change the stream permanently while one of them applies to a single value. Getting that backwards produces a report where a count appears in hexadecimal, or where only the first column is padded, and the symptom appears in a field you did not touch. Beyond that there is a correctness question that looks like a formatting question: a double written with the default settings does not come back as the number you wrote, which matters the moment a results file is compared against a golden copy.
Intermediate18 min readC++Part IFundamentalsStreams
Part I · Module 23 · Chapter 23.2 · Formatted Output and Manipulators
1. Two Audiences, One Set of Controls
A simulation report is read by a person on the day it is produced and by a script for the rest of the project's life. The person needs alignment. The script needs stability: the same field in the same place, the same number of characters, the same number format run after run. Those requirements do not conflict — a fixed-width column satisfies both — which is why the formatting controls are worth learning properly rather than adjusting until the output looks acceptable.
The controls are in <iomanip> and <ios>, and there are fewer than you might expect:
#include <iomanip>
std::setw(n) // field width for the NEXT value
std::setfill(c) // the padding character
std::left / right / internal // where the padding goes
std::dec / hex / oct // integer base
std::showbase // print 0x / 0 prefixes
std::uppercase // ABCDEF rather than abcdef
std::setprecision(n) // digits -- meaning depends on the next two
std::fixed / scientific / defaultfloat
std::boolalpha // true/false rather than 1/0
std::showpos // a leading + on positivesEach of them changes state on the stream. That is the whole mechanism, and §2 is about the one distinction within it that matters.
2. Sticky or One-Shot — the Measurement
Every manipulator applied once, then two values printed. If the second value is also affected, the manipulator changed the stream permanently:
std::ostringstream o;
o << std::setw(8) << 1 << "|" << 2; // is the 2 padded as well? setw(8) -> 1|2
hex -> ff|ff
setfill('0') -> 0007|0008
setprecision(3) -> 1.23|2.35
fixed -> 1.500000|2.500000
showbase+hex -> 0xff|0xff
left -> 1 |2 |
boolalpha -> true|falsestd::uppercase behaves the same way — §4 measures it producing 0xDEADBEEF then FF from a single application.
Read the | as the boundary between the two values. The result is unambiguous and worth memorising as a single sentence:
| manipulator | applies to | effect |
|---|---|---|
setw(n) | the next value only | one-shot |
hex, oct, dec | every later value | sticky |
setfill(c) | every later padded value | sticky |
setprecision(n) | every later float | sticky |
fixed, scientific, defaultfloat | every later float | sticky |
showbase | every later integer | sticky |
left, right, internal | every later padded value | sticky |
boolalpha | every later bool | sticky |
uppercase | every later hex digit | sticky |
setw is the only one-shot manipulator among the nine. Everything else persists until you change it back. That asymmetry is not arbitrary — a field width belongs to a specific field, while a numeric base is a property of how you are presenting numbers — but it is the source of two opposite bugs. Forgetting that setw is one-shot gives you a padded first column and unpadded ones after it. Forgetting that the others are sticky gives you §3.
3. The Leak
One std::hex, applied to an address, in a line that also prints a count:
std::ostringstream o;
std::uint32_t addr = 0x4000A010, count = 42;
o << "addr=0x" << std::hex << addr << " count=" << count; without restoring : addr=0x4000a010 count=2a <-- count 42 printed as 2a
with std::dec : addr=0x4000a010 count=42
saving the flags : addr=0x4000a010 count=42The count is 42. The report says 2a. Nothing failed, no warning was issued, and the defect is in a field the author of the std::hex was not thinking about. In a real report the hex field and the decimal field are often written by different functions, which means the leak crosses a function boundary and the fix has to be made by the function that caused it rather than the one that suffered.
There are two fixes and they are not equivalent:
// 1. restore what you changed -- fine when you know what you changed
o << "addr=0x" << std::hex << addr << std::dec << " count=" << count;
// 2. save and restore the flags -- correct even when you do not
const std::ios::fmtflags save = o.flags();
o << "addr=0x" << std::hex << addr;
o.flags(save);
o << " count=" << count;The second form is what you want in a function that takes an std::ostream& it does not own. A function that formats one field on somebody else's stream and leaves the base changed has modified shared state, and the caller has no way to know. If that sounds like a resource that must be released on every path, it is — and Chapter 22.4's guard pattern applies directly:
class FlagsGuard {
public:
explicit FlagsGuard(std::ios& s) : s_(s), f_(s.flags()), fill_(s.fill()) {}
~FlagsGuard() { s_.flags(f_); s_.fill(fill_); }
FlagsGuard(const FlagsGuard&) = delete;
FlagsGuard& operator=(const FlagsGuard&) = delete;
private:
std::ios& s_;
std::ios::fmtflags f_;
char fill_;
};That restores on every exit path including a throw, which is Chapter 22.3 §2's measured guarantee applied to formatting state. Note it saves the fill character separately: fill() is not part of fmtflags, so restoring the flags alone leaves a setfill('0') in place — a detail that makes the hand-rolled o.flags(save) version incomplete in exactly the case where you used both.
4. Hexadecimal, Which Is How Hardware Data Is Printed
The frozen curriculum gives this chapter one code sample, and it is the register idiom:
os << "0x" << std::hex << std::setw(8) << std::setfill('0') << value;Every part of it is doing something:
os << "0x" << hex << setw(8) << setfill('0') << value -> 0x0000a010
without setw/setfill -> 0xa010
with std::uppercase -> 0x0000A010std::hex gives the base. std::setw(8) and std::setfill('0') give the fixed width. Without the last two you get 0xa010, which is the same value and a much worse report, for a reason that is about tooling rather than aesthetics:
0x4000a010
0x40000004
0x400000c8Those three sort correctly as text, because every address has the same number of characters and hexadecimal is positional. Unpadded, 0xa010 sorts between 0x4000 and 0xb000 in ways that have nothing to do with the addresses. Fixed-width hex means a text sort is an address sort, a diff aligns, and a fixed-offset field extraction works. Chapter 22.9's log format used the same argument for the cycle number — zero-padded, so a text sort is a time sort.
std::uppercase is worth adding for the conventional appearance, and it is sticky like the rest, so 0xDEADBEEF is followed by FF rather than ff unless you reset it with std::nouppercase.
One caution about std::showbase: it emits the 0x for you, but it emits nothing for a value of zero in some bases and it interacts with setw by consuming part of the field width. Writing "0x" as a literal and padding the digits is more predictable, which is why the frozen idiom does exactly that.
5. Precision, and the Round Trip That Fails
This section is the correctness one. 0.1 + 0.2 is not 0.3, and the question is whether your output file preserves the difference:
the value is not 0.3; max_digits10 = 17
precision default (6) 0.3 round-trips: NO
precision 10 0.3 round-trips: NO
precision 15 0.3 round-trips: NO
precision 17 = max_digits10 0.30000000000000004 round-trips: YESThree of the four settings print 0.3, and reading 0.3 back gives a different double than the one written. Only std::numeric_limits<double>::max_digits10, which is 17 here, guarantees that writing then reading returns the identical value.
Notice that precision 10 and 15 also printed 0.3, which is the part that catches people. defaultfloat precision means significant digits, and it strips trailing zeros; at 15 significant digits the value is 0.300000000000000, which prints as 0.3. So you cannot get round-tripping by picking a comfortably large number — you need the specific one the standard provides.
#include <limits>
o << std::setprecision(std::numeric_limits<double>::max_digits10) << value;This matters the moment a results file is compared against a golden copy, which is Chapter 23.8's subject. A timing number written at the default precision and compared for equality will fail against itself. Two options are defensible: write at max_digits10 and compare exactly, or write at a deliberately reduced precision and compare with a tolerance. What is not defensible is writing at the default and comparing exactly, because that combination fails for reasons that have nothing to do with the model being tested. Chapter 23.8 §5 measures the round trip working at max_digits10.
For a human-facing report the opposite choice is right: six significant digits is easier to read and the precision you are discarding was noise. Make it a decision, write it down, and keep the machine-readable file separate from the human-readable one if they need different precisions.
6. What Precision Means Depends on the Float Format
The same setprecision(n) means two different things:
defaultfloat : 1.25e-07
fixed, 12 places : 0.000000125000
scientific, 3 : 1.250e-07
back to defaultfloat: 1.25e-07defaultfloat— precision is significant digits, trailing zeros are stripped, and the stream picks fixed or scientific notation by magnitude.fixed— precision is digits after the decimal point. For 125 picoseconds expressed in seconds you need twelve of them before any digit appears.scientific— precision is digits after the point in the mantissa, so the exponent handles the magnitude.
For engineering quantities spanning many orders of magnitude this is the argument for choosing units rather than fighting the format. A column of times in nanoseconds with fixed and three decimals is readable; the same column in seconds with fixed is a field of zeros, and with defaultfloat it is a mix of 1.25e-07 and 0.0041 that will not align. Pick the unit so the numbers are order-1, then fixed with a small precision gives you a readable, alignable, stable column.
7. A Column That Stays a Column
Putting the chapter together on a realistic report:
o << std::left << std::setw(6) << "unit"
<< std::right << std::setw(12) << "addr"
<< std::setw(9) << "cycles" << std::setw(10) << "us" << "\n";
for (const Row& r : rows) {
o << std::left << std::setw(6) << r.unit
<< std::right << " 0x" << std::hex << std::setw(8) << std::setfill('0') << r.addr
<< std::dec << std::setfill(' ')
<< std::setw(9) << r.cycles
<< std::fixed << std::setprecision(4) << std::setw(10) << r.us << "\n";
} unit addr cycles us
axi0 0x4000a010 412 1.2345
fetch 0x40000004 7 0.0009
lsu 0x400000c8 128000 54.9000Four things in that loop are deliberate.
std::setw appears before every single field, because it is one-shot. There is no way to set it once.
std::dec and std::setfill(' ') are restored immediately after the hex field. Without the first, cycles would print in hex. Without the second, the cycles and microseconds columns would be padded with zeros — 000000412 — which is legal, aligned, and wrong.
std::left and std::right are set where they change and not re-set per field, because they are sticky. The unit is left-aligned; everything numeric is right-aligned so the digits line up.
std::fixed with four decimals makes the microsecond column alignable. Without fixed, 0.0009 and 54.9 would print with different widths and different notations.
The result sorts, greps and diffs. And it is the direct ancestor of Chapter 23.8's results file, which adds a schema comment and a summary line to the same discipline.
8. std::format, and When to Prefer It
C++20 added std::format, and for building a line it is usually the better tool:
#include <format>
const std::string s = std::format("[{:012}] ERROR {:<6} addr 0x{:08X} master={}",
cycle, unit, addr, master);Chapter 23.5 §1 measures that this produces output byte-identical to the iostream version and to snprintf, and §2 measures it at about 1.8× faster than building the same line through an ostringstream. More important than the speed, three properties make it less error-prone:
- The format is one string you can read. The relationship between the width, the fill and the base is visible in
{:08X}instead of spread across four stream operations. - There is no sticky state. A
{:X}affects that field and nothing else. The §3 leak cannot happen. - It is checked at compile time. A format string whose placeholders do not match its arguments is a compile error, where
printf's equivalent is undefined behaviour and iostreams simply cannot express the mismatch.
The reason this chapter still teaches manipulators is that you will read and maintain iostream code for a long time, that std::format is not available in a codebase targeting C++17, and that the stream state model it replaces is the same model Chapter 23.1 established and Chapter 23.3 depends on for input. Where you have C++20 and a free choice, prefer std::format for assembling text and keep the stream for moving it.
9. The Table
| you want | write | measured |
|---|---|---|
| a fixed-width field | std::setw(n) before every field | §2, the only one-shot |
| a hex register value | "0x" << hex << setw(8) << setfill('0') | §4, 0x0000a010 |
| uppercase hex digits | std::uppercase (sticky) | §4, 0x0000A010 |
| to not corrupt a later field | std::dec, or a flags guard | §3, count=2a without it |
| a double that reads back exactly | setprecision(max_digits10) — 17 | §5, the only one that round-trips |
| a readable time column | choose the unit, then fixed with 3–4 places | §6 |
| an alignable float | std::fixed | §7 |
| no sticky state at all | std::format | §8, 1.8× faster too |
10. What to Actually Do
- Write
std::setwbefore every field. It is the only one-shot manipulator; everything else sticks. - Restore the base and the fill immediately after a hex field, or use a guard. Measured: one
std::hexprinted a count of 42 as2a. - Never change formatting on a stream you do not own without restoring it — and restore with a destructor, which covers the early return and the throw. Save
fill()separately fromflags(). - Pad hex to the type's width. Fixed-width hex makes a text sort an address sort and makes
diffalign. - Use
std::setprecision(std::numeric_limits<double>::max_digits10)in any file that will be compared. Measured: 6, 10 and 15 all print0.3and none round-trips. - Choose the unit so the numbers are order-1, then
std::fixedwith a small precision. - Prefer
std::formatfor assembling a line where C++20 is available: no sticky state, compile-time checking, and measured faster.
11. Mistakes Worth Making Once
One std::hex and a decimal field later in the same line. Measured count=2a. Silent, and still a legal number.
Expecting setw to stick. The first column is padded and the rest are not.
Expecting setfill('0') not to stick. The cycles column becomes 000000412.
Formatting on a borrowed std::ostream& without restoring. You have mutated a caller's state with no way for them to know.
Restoring flags() and forgetting fill(). The fill character is not in fmtflags.
Writing a results file at the default precision and comparing it exactly. It fails against itself.
Assuming a bigger precision round-trips. 15 is not enough; 17 is the answer and max_digits10 is where it comes from.
std::fixed on a value measured in seconds. A column of zeros.
12. Interview Relevance
The frozen curriculum marks this chapter High, and the reliable question is "print this 32-bit register value as hex". The answer that stands out writes the frozen idiom, then explains each piece: std::hex for the base, setw(8) and setfill('0') for the fixed width, and the padding so a text sort is an address sort. Adding that setw must be repeated per field while hex and setfill persist shows you have debugged this rather than copied it.
The strong follow-up is "what is wrong with this line?" over something that prints an address and then a count. Naming the leak, saying that the count is still a legal number so nothing downstream detects it, and offering a destructor-based guard rather than a manual std::dec is a complete answer.
If floating point comes up, the precision round trip is the thing worth knowing: the default prints 0.3 for a value that is not 0.3, and max_digits10 is the only setting that guarantees a write-then-read returns the same double.
13. What You Should Be Able to Say Now
- That
setwis the only one-shot manipulator among the common nine, and that the other eight change the stream until you change it back. - Why that asymmetry is correct — a width belongs to a field, a base belongs to a presentation — and why the sticky half fails invisibly.
- That one
std::hexprinted a later count of 42 as2a, with no warning and no detectable corruption downstream. - How to restore formatting state safely, and that
fill()must be saved separately fromflags(). - The register idiom and why the padding is about sorting and diffing, not appearance.
- That a
doublewritten at precision 6, 10 or 15 prints0.3and does not round-trip, and thatmax_digits10(17 here) is the setting that does. - That precision means significant digits for
defaultfloatand digits after the point forfixed. - That
std::formatremoves the sticky-state problem entirely and is measured faster.
14. Exercises
Exercise 1 — the sticky table. Reproduce §2 for all nine manipulators and confirm which one is one-shot. Then add std::showpos and std::internal and classify them.
Exercise 2 — the leak and the guard. Write a function void dumpAddr(std::ostream&, std::uint32_t) that leaks std::hex. Call it between two decimal prints and observe the corruption. Fix it with a manual std::dec, then with a FlagsGuard, then add an early return to the function and show which fix survives.
Exercise 3 — round trip. Sweep setprecision from 1 to 20 over 0.1 + 0.2, writing and reading each time, and find the smallest value that round-trips. Compare it with max_digits10. Repeat for float and for long double.
Exercise 4 — the column. Build §7's table, then deliberately remove the std::setfill(' ') restoration and the std::dec, and describe each resulting defect in terms of §2's rule.
15. Summary
- Output serves a human and a script, and fixed-width fields serve both. That is why the formatting controls are worth using deliberately.
setwis the only one-shot manipulator. Measured against eight others —hex,setfill,setprecision,fixed,showbase,left,boolalpha,uppercase— all of which stick until changed.- The sticky half corrupts fields you did not touch. One
std::hexprinted a count of 42 as2a: no warning, and the result is still a legal number, so nothing downstream can detect it. - Restore formatting state with a destructor, and save
fill()separately fromflags()— the fill character is not part offmtflags. - The register idiom is
"0x" << hex << setw(8) << setfill('0'). The padding exists so a text sort is an address sort and adiffaligns. - A
doubleat the default precision does not round-trip. Measured: 6, 10 and 15 all print0.3; onlymax_digits10— 17 — returns the value you wrote. Use it in any file that will be compared. - Precision means significant digits for
defaultfloatand decimal places forfixed. Choose the unit so numbers are order-1, thenfixed. std::formathas no sticky state, is compile-time checked, and is measured faster. Prefer it for assembling lines where C++20 is available.
Chapter 23.3 turns to the input side, where the same state model decides whether a read loop terminates — and where operator>> accepting a prefix silently is the defect that matters.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
