C++ / CLASSES AND OBJECT LIFECYCLE
Overloading stream insertion for your own types
Write a non-member operator<< for your own class that prints into any std::ostream and chains correctly with other insertions.
What you will learn
- Write insertion as a non-member: std::ostream& operator<<(std::ostream&, const T&)
- Return the same stream by reference so std::cout << a << b << '\n' keeps chaining
- Use friend only when the operator needs private members; const getters work too
- Write through os with no trailing newline so files and stringstreams work as well
Understanding Overloading stream insertion for your own types
`std::cout << value` is a function call: the compiler looks for an overload of `operator<<` whose left operand is a stream and whose right operand is your type. For a member operator the left operand is always the object the function belongs to, so a member `operator<<` on your class would only support `myObject << std::cout`, and you cannot add a member to `std::ostream` because that class is not yours. That is why insertion is written as a free function at namespace scope, with the stream as the first parameter and your type as the second.
Every part of `std::ostream& operator<<(std::ostream& os, const Fraction& f)` is forced by something concrete. The stream is taken by non-const reference because inserting characters mutates it (buffer contents, error flags, the width that `setw` just set) and because streams are not copyable. Your type is taken by `const&` so that const objects and temporaries bind to it and nothing is copied. The return type is a reference to that same stream because `<<` is left-associative: `std::cout << a << b` means `(std::cout << a) << b`, so the first call has to hand a stream back for the second one to write into.
The mental model is that your operator is a guest writing into a stream somebody else owns and configured. It should emit exactly the characters that represent the value and nothing more: no trailing newline, no `std::endl`, no headers or labels the caller did not ask for. It must also write through `os` rather than `std::cout`, otherwise the same object cannot be sent to a file or an `std::ostringstream`. Whether you need `friend` is a separate decision: use it when the operator reads private members, and skip it when public const accessors already expose everything it prints.
<iostream>
<sstream>
<string>
class Fraction {
public:
Fraction(int numerator, int denominator)
: num_(numerator), den_(denominator) {}
// Grants the free function access to num_ and den_.
friend std::ostream& operator<<(std::ostream& os, const Fraction& f);
private:
int num_;
int den_;
};
// Non-member: the left operand is the stream, not a Fraction.
std::ostream& operator<<(std::ostream& os, const Fraction& f) {
os << f.num_ << '/' << f.den_; // the value only, no newline
return os; // hand the stream back for chaining
}
int main() {
Fraction half(1, 2);
Fraction third(1, 3);
std::cout << half << " and " << third << '\n';
std::ostringstream text;
text << "sum of " << half << " and " << third;
std::cout << text.str() << '\n';
}
Stream insertion for your own type is a non-member function that takes the stream by reference, writes only the value, and returns that same stream so insertions can chain.
Worked examples
Hidden friend, and the caller's format flags
Defines the operator inside the class body and shows how stream settings made by the caller reach the members you insert.
<iomanip>
<iostream>
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
// A hidden friend: still a non-member, just written in the class body.
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << '(' << p.x_ << ", " << p.y_ << ')';
}
private:
double x_;
double y_;
};
int main() {
std::cout << std::fixed << std::setprecision(2);
std::cout << Point(1.5, -0.25) << '\n';
std::cout << '[' << std::setw(8) << Point(0, 0) << "]\n";
}
Example explained
Line 1The `friend` definition inside the class body declares a non-member function in the enclosing namespace; the class body only controls access and how the name is found.
Line 2`std::fixed` and `setprecision(2)` are stored on the stream, so `os << p.x_` prints `1.50` even though the operator never mentions precision.
Line 3`Point(1.5, -0.25)` is a temporary and still binds, because the second parameter is `const Point&` rather than `Point&`.
Line 4`setw(8)` pads only the next single insertion, which is the `'('` inside the operator, so the seven spaces land before the parenthesis instead of around the whole point.
Insertion without friend, into any stream
Prints through a public const accessor and sends the same object to std::cout and to an in-memory stream.
<iostream>
<sstream>
<string>
class Tag {
public:
explicit Tag(const std::string& name) : name_(name) {}
const std::string& name() const { return name_; }
private:
std::string name_;
};
std::ostream& operator<<(std::ostream& os, const Tag& t) {
return os << '<' << t.name() << '>';
}
// Works with any output stream because it only touches the one it is given.
void describe(std::ostream& os, const Tag& t) {
os << "tag " << t << " has " << t.name().size() << " letters";
}
int main() {
const Tag div("div");
describe(std::cout, div);
std::cout << '\n';
std::ostringstream buffer;
describe(buffer, div);
std::cout << buffer.str().size() << " chars captured\n";
}
Example explained
Line 1No `friend` is needed here: the operator reads the value through the public const member function `name()`.
Line 2`os << "tag " << t << ...` mixes built-in and user-defined insertions in one chain because both forms return `std::ostream&`.
Line 3`describe` takes `std::ostream&`, so the identical code drives the console and an `std::ostringstream`; nothing inside hardcodes a destination.
Line 4`buffer.str().size()` is 23, exactly the text with no stray newline, because the operator never inserted one.
Important notes
Ending the operator with `std::endl` or `'\n'` takes line breaks and flushing away from the caller, and makes the value unusable in the middle of a larger line; let the caller decide.
A friend defined in the class body is found only by argument-dependent lookup on its parameters, which is fine for `std::cout << p` but means there is no qualified name you can call it by.
Common mistakes
Writing it as a member, `std::ostream& operator<<(std::ostream& os) const;`: that makes `myObject << std::cout` legal and `std::cout << myObject` a compile error, because a member operator's left operand is always `*this`.
Forgetting `return os;`. With a `void` return, `std::cout << obj << '\n'` no longer compiles since the second insertion has no stream on its left; declaring the return type as `std::ostream` by value fails too, because streams cannot be copied.
Inserting into `std::cout` inside the body instead of `os`: `logfile << obj` then prints to the terminal and leaves the file empty, and an `std::ostringstream` captures nothing.
Try it yourself
Change, predict, then run
Write a `Duration` class holding private `int minutes_` and `int seconds_` and give it an `operator<<` that prints `4:05`, padding seconds below ten with an explicit `if` that inserts `'0'`. Confirm that `std::cout << Duration(4, 5) << ' ' << Duration(0, 30) << '\n';` produces `4:05 0:30` on one line.
Open the C++ workspaceCheck your understanding
A colleague defines `void operator<<(std::ostream& os, const Point& p)` and the definition itself compiles. What happens at the call site `std::cout << p << '\n';`?
- It fails to compile, because `std::cout << p` now yields `void` and there is no insertion operator taking `void` on the left
- It compiles and prints the point then a newline, since each insertion in the chain is evaluated independently
- It compiles but the newline appears first, because a void-returning operator is sequenced after the rest of the expression
- It fails to compile, because an overloaded `operator<<` must always be declared `friend` of the class
Show answer
`<<` is left-associative, so the expression means `(std::cout << p) << '\n'`; the inner call returns `void`, leaving nothing valid as the left operand of the second insertion. Option 2 is tempting because the operator works fine on its own, but the insertions are one expression, not separate statements, and the result of each feeds the next, which is precisely why the overload must return `std::ostream&`. `friend` is unrelated: it only controls access to private members.