Concepts / Overloading Operators in C++

Overloading Operators in C++

A custom output operator is a function that lets a user-defined type be written with the << operator.

  • Programming

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.

cpp
Output
{"apple", 3}
Notes on the code
  • 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.

ostream&returned stream, byreferenceoperator<<overloads the << operatorostream& osthe stream written toconst T& objthe value written, notcopied
What does each part of the signature do, and how do the parameter and return value connect to the stream being written to?

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.

Worked example

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.

  1. 1
    Piece 1

    os << "{\"" writes the characters {" to the stream.

  2. 2
    Piece 2

    os << e.name writes apple to the stream, after the opening quotation mark.

  3. 3
    Piece 3

    os << "\", " writes a closing quotation mark, a comma, and a space.

  4. 4
    Piece 4

    os << e.number writes 3 to the stream.

  5. 5
    Piece 5

    os << "}" writes the closing brace.

  6. 6
    Return

    return os hands back the same stream, which now holds the complete text.

Result

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.

coutthe original streamoperator<< with ewrites the Entry textreturned streamthe same coutnext << callreceives the stream as leftoperandleft operandreturnsleft operand of next <<
How does cout << e << endl become a sequence of operator calls, and how does each returned stream become the left operand of the next call?
Key point

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.

cout << estatement in mainoperator<<your Entry overloadcoutthe stream receiving textcall with cout and ewrite {" then namewrite quote, comma, spacewrite number, then }return the same stream
When cout << obj runs, which function is called first, which writes the characters, and in what order do the calls happen?

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 like
    ostream operator<<(ostream os, const Entry& e)
    Why it fails

    The source says the operator receives the stream by reference so that it writes to the original stream rather than a copy.

    Do this instead

    Use 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 like
    void operator<<(ostream& os, const Entry& e) { os << e.name; }
    Why it fails

    The 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 instead

    Declare 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 like

    Returning os from a function declared to return something other than the stream type

    Why it fails

    The 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 instead

    Keep 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 like
    ostream& operator<<(ostream& os, Entry& e)
    Why it fails

    The 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 instead

    Write 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?

  • {"apple", 3}

  • apple 3

  • {apple, 3}

  • {"3", apple}

Reveal answer
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.

EASY

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.

More concepts starting with O