Overloading Operators in C++
A custom output operator is a function that lets a user-defined type be written with the << operator.
Why Built-in Output Stops Working for Your Types
The << operator already knows how to write the built-in types, such as numbers and strings, to an output stream like cout. Once you define your own data type, that knowledge no longer covers it. The built-in output operator has no meaningful default for a user-defined type, so writing that type directly to a stream does not produce a useful result.
A custom output operator solves this problem. It is a function that teaches the << operator how to write your new type in a format you choose. Once it exists, a value of that type can be written in an ordinary output statement, exactly as a built-in type would be written.
The Entry Struct and Its Target Format
Throughout this article we use a struct called Entry. It holds two members: a string called name and an int called number. Grouping a name with a number like this is a common reason to need a custom output format, because the struct has no printed form of its own.
The goal of the operator is to write an Entry in a format that resembles the brace notation used to initialize it in code. The name appears in quotation marks, followed by the number, all inside braces. The body of the operator is what fixes this printed format, so every string literal and member access in it contributes to the final text.
{"apple", 3}- struct Entry groups a string name with an int number
- the operator signature takes the stream by reference and the Entry by const reference
- each << in the body sends one piece to the same stream
- return os hands the same stream back to the caller
Reading the Operator Signature Piece by Piece
The standard form of the signature is ostream& operator<<(ostream& os, const T& obj), where T stands for the user-defined type. Every part of this line has a job. The keyword operator together with << tells the compiler that this function overloads the << operator. The return type ostream& gives back a stream, and the first parameter ostream& os is the stream the value will be written to.
The second parameter, const T& obj, is the value being written. It is passed by constant reference, which means the object is not copied and the operator cannot modify it. For the Entry struct, T is simply Entry, so the parameter becomes const Entry& e.
The stream parameter is taken by reference. This means the operator writes to the original stream, not to a copy of it. When the operator finishes, it returns that same stream. Because the return type ostream& matches the first parameter, the result of one << call is a stream that can serve as the left operand of the next << in the same expression.
Tracing the Body from Left to Right
Read the body of the operator from left to right, the same way the output is produced. The first piece, written by os << "{\"", sends an opening brace and a quotation mark to the stream. Next, e.name writes the name. The closing quotation mark, a comma, and a space follow. Then e.number is written, and finally a closing brace.
Tracing the pieces for one Entry
Trace what is written to the stream when the operator receives an Entry whose name is apple and whose number is 3.
- 1Piece 1
os << "{\"" writes the characters {" to the stream.
- 2Piece 2
os <<
e.namewrites apple to the stream, after the opening quotation mark. - 3Piece 3
os << "\", " writes a closing quotation mark, a comma, and a space.
- 4Piece 4
os << e.number writes 3 to the stream.
- 5Piece 5
os << "}" writes the closing brace.
- 6Return
return os hands back the same stream, which now holds the complete text.
The stream receives {"apple", 3}, and that text appears where the statement's output goes.
How Chaining Passes the Stream Along
A statement such as cout << e << endl is not a single action. It is a sequence of operator calls. The first call takes cout as its left operand and e as its right operand. Because the operator returns the stream, the result of that first call becomes the left operand of the next << in the chain.
Returning the stream is what makes chaining possible. Without a returned stream, there would be nothing for the next << to act on.
Where the Printed Text Is Actually Produced
When cout << e runs, the call goes to your operator, not to a built-in routine that handles structs. The operator receives cout by reference and e by constant reference. Each << inside its body then writes one piece to that same stream. The characters appear in order, and only after the body finishes does control return to the caller with the stream.
Mistakes When Writing the Output Operator
The mistakes below all involve the signature or the return value. Each one breaks a part of the design that the source describes: the stream is received by reference, the same stream is returned, and the returned stream feeds the next << in the chain.
Common mistake Taking the stream by value instead of by reference
What it looks likeostream operator<<(ostream os, const Entry& e)Why it failsThe source says the operator receives the stream by reference so that it writes to the original stream rather than a copy.
Do this insteadUse ostream& os as the first parameter, so the text goes to the stream the caller is using.
Common mistake Returning nothing from the operator
What it looks likevoid operator<<(ostream& os, const Entry& e) { os << e.name; }Why it failsThe source explains that the returned stream is what lets the operator be used in a longer chain. A void result gives the next << nothing to work with.
Do this insteadDeclare the return type as ostream& and end the body with return os.
Common mistake Returning the stream type with a mismatched return type
What it looks likeReturning os from a function declared to return something other than the stream type
Why it failsThe source states that the return type ostream& matches the first parameter, which is what makes the result usable as the left operand of the next <<.
Do this insteadKeep the return type matched to the stream parameter, ostream&, so the returned value is the same kind of stream.
Common mistake Dropping the const on the object parameter
What it looks likeostream& operator<<(ostream& os, Entry& e)Why it failsThe source says const T& passes the object without copying it and prevents the operator from modifying it. Without const, the operator loses that guarantee.
Do this insteadWrite const Entry& e so the value is passed by constant reference.
Common mistake Writing to cout directly inside the body instead of to os, which ignores the stream the caller actually passed in
Predicting the Output of a New Entry
What do you think happens?
If the body of the operator stays the same, what text does cout << e print when e has the name apple and the number 3?
Reveal answer
{"apple", 3}
The operator writes an opening brace and quotation mark, then the name, then the closing quotation mark, a comma and a space, then the number, then the closing brace. Only the body determines this format.
Suppose the name is kiwi and the number is 12. Write out the exact text the operator writes to the stream, piece by piece, before checking the output in your own program.
Hints
- Start with the opening brace and quotation mark, which are written first.
- The number is written after the comma and space, not before the name.
- Count the spaces: there is one space after the comma and none before the closing brace.
Key Takeaways
- A custom output operator is a function that lets a user-defined type be written with the << operator, because the built-in output operator has no default for new types.
- The standard signature is ostream& operator<<(ostream& os, const T& obj): the stream is taken by reference, the object by const reference, and the stream is returned.
- Returning the same stream is what lets the result of one << call serve as the left operand of the next, so chained output works.
- The body of the operator determines the printed format, and it writes its pieces in order to the stream it received.
- Common mistakes include taking the stream by value, returning nothing, and dropping const on the object parameter, and each breaks the signature design.