Overloading the Stream Insertion Operator (<<)


Rather than writing a print() member function for a class, it can be convenient to overload the stream insertion operator to work with our new data type.

Because the left operand of the stream insertion operator is an object of class ostream (like cout) and not an object of our new class, we must overload the operator as a standalone function.

Here is the skeleton code for overloading the stream insertion operator:

ostream& operator<<(ostream& os, const ClassName& obj)
{
    // Print the data members of obj using os like you would using cout
   
    // Return the output stream object so the operator may be cascaded correctly

    return os;
}

Example 1: The << operator overloaded as a standalone friend function of the Rational class

To designate our overloaded operator function as a friend of the Rational class, we need to include the function's prototype, preceded by the keyword friend, anywhere in the declaration of the Rational class:

class Rational
{
    friend ostream& operator<<(ostream&, const Rational&);

private:
    int numerator,
        denominator;

public:
    .
    .
    .
};

The function definition can then be coded like so:

ostream& operator<<(ostream& os, const Rational& obj)
{
    os << obj.numerator << '/' << obj.denominator;

    return os;
} 

Once we've overloaded this operator, we can use it to print an object of the Rational class using cout instead of calling a print() member function:

Rational r1(3, 5);     // Create r1 and set it = 3/5

cout << r1;            // Generates the function call operator<<(cout, r1) and prints 3/5

The Stream Insertion Operator and Manipulators

There's one problem with the code outlined above: it will break the setw manipulator, because setw only affects the next item inserted into the output stream, and the code above inserts multiple items into the output stream. So, for example, if you try to write something like this, you won't get the results that you might expect:

Rational r1(5, 10);
cout << left << '*' << setw(6) << r1 << '*' << endl;

Instead of the manipulator being applied to the entirety of the value of r1, the numerator will be inserted into the output stream with a width of 6, and then the '/' and denominator will be inserted, giving the following output:

*5     /10*

The result is that the total width of what was printed was 9 characters, not the requested 6.

A solution to this problem is to insert our multiple items into an output stringstream, and then convert the output stringstream into a string and insert that string into the output stream.

ostream& operator<<(ostream& os, const Rational& obj)
{
    ostringstream output_text;    // Declare an output stringstream
    
    // Insert our output into the output stringstream object
    output_text << obj.numerator << '/' << obj.denominator;
    
    // Convert the output stringstream object to a string and insert it into
    // the output stream
    os << output_text.str();

    return os;
}

Now the cout statement shown above will work as expected. Since we're only inserting a single string into the output stream, the setw manipulator is correctly applied to the single string, printing it 6 characters wide:

*5/10  *

Cascading the Stream Insertion Operator

The return value from the overloaded operator function is required in order to cascade the operator correctly. Consider the following typical use of the cascaded stream insertion operator. How does the compiler evaluate this statement?

cout << "The value of r1 is " << r1 << endl;

The << operator is evaluated from left to right, so this part of the expression is evaluated first:

cout << "The value of r1 is "

The text string is printed and then a reference to the ostream object cout is returned. The return value effectively replaces the sub-expression that has been evaluated, and as a result, the remainder of the expression becomes:

cout << r1 << endl;

The overloaded stream insertion operator function then generates a function call to print r1 and (if written correctly) returns a reference to the ostream object that was passed into the function (cout, in this case). The return value replaces the function call and the remaining part of the expression then becomes:

cout << endl;

If your overloaded stream insertion operator function doesn't return a reference to the ostream object passed in as the first argument, the last step would not work correctly - you'd get a syntax error on the statement instead since the left operand for the next call to the operator function would be the wrong data type.

Using an Overloaded Stream Insertion Operator for File Output

Writing an overloaded operator function to use the << operator to write to a file stream is trivial - simply replace the data type ostream with the data type ofstream in the function prototype and definition. Of course, you'll have to open the file stream object for output before you can write to it.