C++ Part I · Module 15 · Operator Overloading
Stream Insertion and Extraction
This is the overload that pays for itself fastest, because the first thing anyone does with an unfamiliar type is print one to find out what is in it. Without it, inspecting a transaction means naming every field at every call site, and a log line that should read as one expression becomes six. With it, a model dumps its state in one chained expression, and every later debugging session is shorter. It is also the overload with the oddest justification in the language: nothing whatsoever about a left-shift suggests output, so stream insertion fails Chapter 15.1's guessing test on its face and survives purely on a convention that every C++ programmer learns once and then never has to think about again — which makes it worth being explicit that the convention is the justification, because it is not transferable to anything else you might want to overload. The mechanics are small: a non-member function, a reference to the stream in and the same reference out, and the type taken by const reference. Everything interesting is in the two failures. A formatting manipulator applied inside the function does not stay inside the function, and this chapter measures one escaping and silently changing how an unrelated integer printed afterwards. And extraction can fail halfway through a multi-field read, which leaves the target object holding a measured mixture of three different things: a field that was read correctly, a field the failure zeroed, and a field nothing touched at all.
Intermediate18 min readC++Part IFundamentalsOperators
Part I · Module 15 · Chapter 15.5 · Stream Insertion and Extraction
1. The Signature
std::ostream& operator<<(std::ostream& os, const Transaction& t);Five decisions, and every one of them is load-bearing:
| Part | Why |
|---|---|
| non-member | the left operand is the stream — Chapter 15.2 §7 |
std::ostream& parameter | streams cannot be copied; and you must write to the caller's stream |
const Transaction& | printing does not modify, and a reference avoids a copy (Chapter 6.5) |
std::ostream& return | the next << in the chain needs a stream to apply to |
| returns the same stream | return os; — not a new one |
The implementation:
std::ostream& operator<<(std::ostream& os, const Transaction& t)
{
return os << t.kind << "@0x" << std::hex << t.addr << " = 0x" << t.data;
}That body is itself a chain of << calls on built-in types, each returning os, so return os << … returns exactly what it was given.
2. Chaining Is the Whole Point
Transaction t{0x40, 0xDEAD, "WRITE"};
std::cout << " " << t << " | count=" << 3 << '\n';WRITE@0x40 = 0xdead | count=3One expression mixing a string literal, a user-defined type, an int and a char. That works because every << returns the stream, so the expression is a left-associative chain — Chapter 4.9 §4's associativity — each link handing the same std::ostream& to the next.
Return void and the chain stops at your type. Return a new stream and you are writing somewhere else. return os; is the only correct ending.
3. Why It Cannot Be a Member
Chapter 15.2 §7 measured this, and the result is worth restating because it is the reason the signature has no alternative:
A. t << std::cout compiles, prints tx@64
B. std::cout << t error: invalid operands to binary expression
('ostream' (aka 'basic_ostream<char>') and 'Transaction')
C. a << b << std::cout error: invalid operands to binary expression
('Transaction' and 'Transaction')A member operator<< gives you an operator whose left operand is your type, so the call reads backwards (A), the natural order is a hard error (B), and it cannot chain at all (C). Since you cannot add members to std::ostream, non-member is the only placement.
4. The Strangest Convention in C++
Apply Chapter 15.1 §8's test honestly: would a competent reader, seeing os << t and not having read anything, guess that it writes t to os?
No. << is a left shift. Nothing about shifting bits suggests output. The notation was chosen because << has usefully low precedence and a suggestive visual direction, and it is one of the clearest cases in the language of an operator meaning something unrelated to its symbol.
It survives for a reason that is real and worth naming precisely:
The convention is the justification. Every C++ programmer learns
<<means output exactly once, and from then on it reads correctly everywhere.
Two consequences follow, and the second is the one that matters.
Follow the convention exactly. Because the justification is shared knowledge, any deviation destroys it. An operator<< that mutates its argument, or prints to somewhere other than the stream it was given, or returns something other than the stream, is worse than a named function — it borrows a reader's confidence and then violates it.
And the justification is not transferable. You cannot reason "<< means output despite its symbol, therefore my % can mean path-join despite its symbol." Chapter 15.7 is the catalogue of that mistake. Stream insertion is a grandfathered exception, not a precedent — a one-off budget that was spent before you arrived.
5. The Format State Leaks
The first failure, and it is easy to produce by accident because the natural implementation causes it.
std::ostream& operator<<(std::ostream& os, const Tx& t) { return os << "0x" << std::hex << t.addr; }std::hex is not an argument to this print — it is a persistent change to the stream's state. Measured, printing 255, then a Tx, then 255 again, on a fresh stream each time:
leaky overload : 255 | 0x40 | ff
safe overload : 255 | 0x40 | 255The second 255 printed as ff. The std::hex escaped the function and changed how an unrelated integer printed later, in code that never mentions hex. Nothing warns.
The fix is to save and restore the flags:
std::ostream& operator<<(std::ostream& os, const TxSafe& t)
{
std::ios_base::fmtflags saved = os.flags();
os << "0x" << std::hex << t.addr;
os.flags(saved);
return os;
}safe overload : 255 | 0x40 | 255If your
operator<<changes any formatting state, restore it before returning.
The affected state includes the base (hex, oct, dec), showbase, boolalpha, fill, width, precision and setf flags. width is the one exception that behaves differently — it resets itself after each insertion — but relying on that distinction is not worth it.
A method note, because my first attempt at this measurement was wrong. I printed a 255, labelled it "before", then the transaction, then another 255, and got before: ff. The label was false: a previous section of the same program had already leaked hex onto that stream. A leak measurement must use a fresh stream per case, or the thing you are measuring has already happened. Chapter 15.3 §7's lesson applies — the run can mislead where the structure does not.
(The stream hierarchy, manipulators and formatting in full are Module 23's subject. Here only the leak matters.)
6. Extraction: The Mirror Signature
std::istream& operator>>(std::istream& is, Transaction& t)
{
return is >> t.kind >> t.addr >> t.data;
}Two differences from <<, both forced: the stream is an std::istream&, and the object is a non-const reference because reading fills it in.
Working case:
C. extraction success:
ok=1 kind=READ addr=64 data=99Input "READ 64 99", three fields read, and ok=1 is the stream converting to true — the conventional way to ask whether it worked.
7. Extraction Failure Leaves a Mixture
The second failure, and it is the one worth knowing by heart. Same operator, input "READ notanumber 99", into an object pre-filled with recognisable values:
Transaction u{0xAAAA, 0xBBBB, "untouched"};
in >> u;D. extraction FAILURE -- what happens to the variable?
stream good=0 fail=1 eof=0
kind=READ addr=0 data=48059Three fields, three different outcomes:
| Field | Value after | Why |
|---|---|---|
kind | READ | read successfully, before the failure |
addr | 0 | this is the extraction that failed, and failure zeroes the target |
data | 48059 = 0xBBBB | untouched — its original value |
So the object is left holding new data, a zero, and a stale value, and u.kind being "READ" makes it look partially plausible. The two mechanisms, measured separately:
single failing extraction: fail=1 v=0
(was 43690 == 0xAAAA; C++11 sets the target to 0 on failure)
after a failed extraction, the NEXT one is a no-op: a=0 b=2222
(b untouched)A failing extraction writes 0 to its target — that is a C++11 rule; before C++11 the target was left alone. And every subsequent extraction on a failed stream does nothing at all, which is why data kept 0xBBBB.
The consequence for how you write operator>>:
Read into a local, and only write to the caller's object if the stream is still good.
std::istream& operator>>(std::istream& is, Transaction& t)
{
Transaction tmp;
if (is >> tmp.kind >> tmp.addr >> tmp.data) t = tmp; // all-or-nothing
return is;
}This is the same discipline as Chapter 13.3 §7's copy assignment — do the work that can fail first, and commit only once it has succeeded. A failed read now leaves the caller's object exactly as it was.
And set failbit yourself when the data is syntactically fine but semantically wrong. If a field parses as a number but is out of range, is.setstate(std::ios_base::failbit) is how you say so, because a caller checking if (in >> t) deserves a false.
8. What to Print
The signature is mechanical; the content is a judgement, and three rules cover most of it.
Print what identifies the object, not everything it contains. A Transaction printing its kind, address and data is useful; one printing an internal sequence number and a cached flag is noise.
Make it one line. operator<< output ends up inside log lines, assertion messages and container dumps, so an embedded newline corrupts all three. Let the caller add '\n'.
And make it round-trippable if you also write operator>>. If os << t produces something is >> t cannot read back, the pair is a trap. The useful test is the obvious one: print an object, read it back, compare — which requires Chapter 15.4's ==, and is a good reason to have it.
9. What This Buys
The frozen description of this chapter calls it the overload that pays for itself every time a model needs to log or dump its state, and the comparison is concrete:
// without the overload
std::cout << "tx " << t.kind << " addr=" << t.addr << " data=" << t.data << '\n';
// with it
std::cout << "tx " << t << '\n';The second form is shorter at every call site, and — more importantly — it has one definition of how a Transaction looks. Change the format once and every log line, dump and assertion message changes with it. The first form has the format copied into every site that prints one, which is the same multiple-truths problem Chapter 15.3 §2 measured for + and += and Chapter 15.4 §4 measured for ==.
It also composes. A std::vector<Transaction> printed in a loop needs nothing further, and a containing type's operator<< can just insert its members.
(C++20's std::format is a different and in many ways better answer to the same problem, and std::print follows in C++23. They are Module 21's subject. operator<< remains what you need for a C++17 baseline, for interoperating with existing stream code, and for anything that takes an std::ostream&.)
10. What to Actually Do
Write operator<< as a non-member taking std::ostream& and const T&, returning os.
Write operator>> taking std::istream& and a non-const T&.
Restore any formatting state you change — measured, std::hex escapes.
Read into a temporary and commit only if the stream is still good.
Set failbit yourself when the data parses but is semantically wrong.
Print one line, no trailing newline, containing what identifies the object.
Make the pair round-trippable, and test it by printing, reading back and comparing.
And measure a leak on a fresh stream — a shared one may already be dirty.
11. Mistakes Worth Making Once
Returning void from operator<<. The chain stops at your type.
Writing it as a member. Measured: t << std::cout reads backwards, std::cout << t is an error, and chaining is impossible.
Taking the stream by value. Streams are not copyable.
Taking the object by value. A needless copy of something you only read.
Leaving std::hex set. Measured: a later 255 printed as ff.
Embedding a newline. It corrupts log lines and container dumps.
Writing straight into the caller's object in operator>>. Measured: a failure left READ, 0 and 0xBBBB side by side.
Assuming a failed extraction leaves the target alone. C++11 sets it to 0.
Assuming later extractions still run after a failure. They are no-ops.
Measuring a format leak on a stream you already printed to. The leak has happened; the label lies.
Citing << as a precedent for an unrelated overload. It is grandfathered, not transferable.
12. Interview Relevance
"Write operator<< for a type." Non-member, std::ostream& and const T&, returning os. Being able to say why each part is that way is the discriminator.
"Why must it be a non-member?" The left operand is the stream, and you cannot add members to std::ostream. The strong answer adds that a member version cannot chain.
"Why does it return the stream?" So the next << has something to apply to — that is what makes chaining work.
"What is the hazard of using std::hex inside it?" It persists on the stream. Measured: a later unrelated 255 printed as ff. Save and restore os.flags().
"What happens to the object if operator>> fails halfway?" It is left partially written — measured as one field read, one zeroed by the failed extraction, one untouched because subsequent extractions are no-ops. Read into a temporary and commit.
"Does a failed extraction modify its target?" Yes — C++11 sets it to 0. Many candidates believe it is left alone.
"Isn't << for output a bad overload?" By the guessing test, yes — and it survives on a universal convention. Being able to say that, and that it is not a transferable precedent, shows you understand the rule rather than the exception.
13. What You Should Be Able to Say Now
std::ostream& operator<<(std::ostream& os, const T& t)— non-member, stream by reference, object by const reference, stream returned.return os;is what makes chaining work — measured, one expression mixing a literal, a user type, anintand achar.- Measured: a member version gives
t << std::cout, errors onstd::cout << t, and cannot chain at all. - Stream insertion fails Chapter 15.1 §8's guessing test — nothing about a left shift suggests output.
- It survives on a universal convention learned once, which is a real justification and not a transferable precedent.
- Measured:
std::hexinside the function escaped it — a later255printed asff. - Save and restore
os.flags()— measured, that fixed it. operator>>takesstd::istream&and a non-constT&.- Measured on failure:
fail=1, withkind=READ,addr=0,data=0xBBBB— read, zeroed, and untouched. - A failing extraction sets its target to 0 (a C++11 rule).
- Every extraction after a failure is a no-op, which is why the third field kept its old value.
- So read into a temporary and assign only if the stream is still good — Chapter 13.3 §7's discipline.
- Set
failbityourself for data that parses but is semantically wrong. - Print one identifying line with no trailing newline.
- Make the pair round-trippable and test it by printing, reading back and comparing.
- The real win is one definition of how the type looks — change it once, every log line follows.
- Measure a format leak on a fresh stream, or the leak has already happened.
14. Exercises
Exercise 1 — write the pair. For Register{name, width, value}, write operator<< and operator>> with correct signatures, and say what each part of each signature is for.
Exercise 2 — the chain. Explain why std::cout << "x " << r << '\n' works and what breaks if operator<< returns void.
Exercise 3 — the member form. Say what the three measured results are for a member operator<<, and which one is fatal.
Exercise 4 — the leak. Write an operator<< printing a value in hex that does not leak, and say which stream state you saved.
Exercise 5 — predict the leak. o << 255; o << tx; o << 255; with a leaky overload on one fresh stream. Give the three outputs.
Exercise 6 — the half-read object. For a three-field operator>> where field two fails, give each field's value afterwards and the reason for each.
Exercise 7 — fix it. Rewrite that operator>> so a failure leaves the caller's object untouched, and name the chapter whose discipline this matches.
Exercise 8 — semantic failure. A width field parses as 999 but the maximum is 64. Say how to report that through the stream.
Exercise 9 — round trip. Write the test that checks os << t and is >> t agree, and say which other operator it needs.
Exercise 10 — the convention. Explain why << for output is acceptable despite failing the guessing test, and why that reasoning cannot be reused for a new overload.
15. Summary
std::ostream& operator<<(std::ostream& os, const T& t)— non-member because the left operand is the stream, returningosso the chain continues.- Measured: a member version reads backwards, errors on the natural order, and cannot chain.
- It fails the guessing test and survives on a universal convention — which means conform exactly, and do not cite it as a precedent.
- Measured:
std::hexescaped the function and made a later255print asff; saving and restoringos.flags()fixed it. - Measured on a failed extraction:
kind=READ,addr=0,data=0xBBBB— read, zeroed, untouched, side by side in one object. - A failing extraction zeroes its target (C++11) and later extractions are no-ops.
- So read into a temporary and commit only on success — the most reusable idea here, and not about operators at all.
- Print one identifying line, make the pair round-trippable, and measure leaks on a fresh stream.
Four operator groups are now settled, and all of them had something in common that is about to stop being true: they produced values. + made a new value, == reported a fact, << wrote and returned a stream, and none of them had to answer a question about what happens when the operand is const, because none of them modified anything. The next group is the one that does. Assignment writes to its left operand, subscript hands back something the caller can write through, and the function-call operator turns an object into something callable — and each of them has to exist in two versions, one for a modifiable object and one for a const one, with the compiler silently choosing between them. The next chapter measures that choice happening, what a const object gets when you forget to provide for it, what happens when subscript returns the wrong thing, and a reason to prefer a callable object to a function pointer that has nothing to do with notation at all: measured, it was the difference between 87 indirect calls and none.
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.
