21.4 — Overloading the I/O operators

For classes that have multiple member variables, printing each of the individual variables on the screen can get tiresome fast. For example, consider the following class:

class Point
{
private:
    double m_x{};
    double m_y{};
    double m_z{};

public:
    Point(double x=0.0, double y=0.0, double z=0.0)
      : m_x{x}, m_y{y}, m_z{z}
    {
    }

    double getX() const { return m_x; }
    double getY() const { return m_y; }
    double getZ() const { return m_z; }
};

If you wanted to print an instance of this class to the screen, you’d have to do something like this:

Point point { 5.0, 6.0, 7.0 };

std::cout << "Point(" << point.getX() << ", " <<
    point.getY() << ", " <<
    point.getZ() << ')';

Of course, it makes more sense to do this as a reusable function. And in previous examples, you’ve seen us create print() functions that work like this:

class Point
{
private:
    double m_x{};
    double m_y{};
    double m_z{};

public:
    Point(double x=0.0, double y=0.0, double z=0.0)
      : m_x{x}, m_y{y}, m_z{z}
    {
    }

    double getX() const { return m_x; }
    double getY() const { return m_y; }
    double getZ() const { return m_z; }

    void print() const
    {
        std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ')';
    }
};

While this is much better, it still has some downsides. Because print() returns void, it can’t be called in the middle of an output statement. Instead, you have to do this:

int main()
{
    const Point point { 5.0, 6.0, 7.0 };

    std::cout << "My point is: ";
    point.print();
    std::cout << " in Cartesian space.\n";
}

It would be much easier if you could simply type:

Point point{5.0, 6.0, 7.0};
cout << "My point is: " << point << " in Cartesian space.\n";

and get the same result. There would be no breaking up output across multiple statements, and no having to remember what you named the print function.

Fortunately, by overloading operator<<, you can!

Overloading operator<<

Overloading operator<< is similar to overloading operator+ (they are both binary operators), except that the parameter types are different.

Consider the expression std::cout << point. If the operator is <<, what are the operands? The left operand is the std::cout object, and the right operand is your Point class object. std::cout is actually an object of type std::ostream. Therefore, our overloaded function will look like this:

// std::ostream is the type for object std::cout
friend std::ostream& operator<< (std::ostream& out, const Point& point);

Implementation of operator<< for our Point class is fairly straightforward -- because C++ already knows how to output doubles using operator<<, and our members are all doubles, we can simply use operator<< to output the data members of our Point. Here is the above Point class with the overloaded operator<<.

#include <iostream>

class Point
{
private:
    double m_x{};
    double m_y{};
    double m_z{};

public:
    Point(double x=0.0, double y=0.0, double z=0.0)
      : m_x{x}, m_y{y}, m_z{z}
    {
    }

    friend std::ostream& operator<< (std::ostream& out, const Point& point);
};

std::ostream& operator<< (std::ostream& out, const Point& point)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')'; // actual output done here

    return out; // return std::ostream so we can chain calls to operator<<
}

int main()
{
    const Point point1 { 2.0, 3.0, 4.0 };

    std::cout << point1 << '\n';

    return 0;
}

This is pretty straightforward -- note how similar our output line is to the line in the print() function we wrote previously. The most notable difference is that std::cout has become parameter out (which will be a reference to std::cout when the function is called).

The trickiest part here is the return type. With the arithmetic operators, we calculated and returned a single answer by value (because we were creating and returning a new result). However, if you try to return std::ostream by value, you’ll get a compiler error. This happens because std::ostream specifically disallows being copied.

In this case, we return the left hand parameter as a reference. This not only prevents a copy of std::ostream from being made, it also allows us to “chain” output commands together, such as std::cout << point << '\n'.

Consider what would happen if our operator<< returned void instead. When the compiler evaluates std::cout << point << '\n', due to the precedence/associativity rules, it evaluates this expression as (std::cout << point) << '\n';. std::cout << point would call our void-returning overloaded operator<< function, which returns void. Then the partially evaluated expression becomes: void << '\n';, which makes no sense!

By returning the out parameter as the return type instead, (std::cout << point) returns std::cout. Then our partially evaluated expression becomes: std::cout << '\n';, which then gets evaluated itself!

Any time we want our overloaded binary operators to be chainable in such a manner, the left operand should be returned (by reference). Returning the left-hand parameter by reference is okay in this case -- since the left-hand parameter was passed in by the calling function, it must still exist when the called function returns. Therefore, we don’t have to worry about referencing something that will go out of scope and get destroyed when the operator returns.

Just to prove it works, consider the following example, which uses the Point class with the overloaded operator<< we wrote above:

#include <iostream>

class Point
{
private:
    double m_x{};
    double m_y{};
    double m_z{};

public:
    Point(double x=0.0, double y=0.0, double z=0.0)
      : m_x{x}, m_y{y}, m_z{z}
    {
    }

    friend std::ostream& operator<< (std::ostream& out, const Point& point);
};

std::ostream& operator<< (std::ostream& out, const Point& point)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';

    return out;
}

int main()
{
    Point point1 { 2.0, 3.5, 4.0 };
    Point point2 { 6.0, 7.5, 8.0 };

    std::cout << point1 << ' ' << point2 << '\n';

    return 0;
}

This produces the following result:

Point(2, 3.5, 4) Point(6, 7.5, 8)

In the above example, operator<< is a friend because it needs direct access to the member of Point. However, if the members could be accessed via getters, then operator<< could be implemented as a non-friend.

Overloading operator>>

It is also possible to overload the input operator. This is done in a manner analogous to overloading the output operator. The key thing you need to know is that std::cin is an object of type std::istream. Here’s our Point class with an overloaded operator>> added:

#include <iostream>

class Point
{
private:
    double m_x{};
    double m_y{};
    double m_z{};

public:
    Point(double x=0.0, double y=0.0, double z=0.0)
      : m_x{x}, m_y{y}, m_z{z}
    {
    }

    friend std::ostream& operator<< (std::ostream& out, const Point& point);
};

std::ostream& operator<< (std::ostream& out, const Point& point)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';

    return out;
}

// note that parameter point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
    double x{};
    double y{};
    double z{};
    
    // This version subject to partial extraction issues (see below)
    in >> point.m_x >> point.m_y >> point.m_z;

    return in;
}

int main()
{
    std::cout << "Enter a point: ";

    Point point{};
    std::cin >> point;

    std::cout << "You entered: " << point << '\n';

    return 0;
}

Assuming the user enters 3.0 4.5 7.26 as input, the program produces the following result:

You entered: Point(3, 4.5, 7.26)

In this implementation, we use operator= to overwrite the values in our point. Because operator= is publicly available, this means we don’t need our operator>> to be a friend.

Guarding against partial extraction

When we’re extracting a single value, there are only two possible outcomes: the extraction fails, or it is successful. However, when we’re extracting more than one value as part of an operation, things get a bit more complicated.

The above implementation of operator>> can result in a partial extraction. Consider what would happen if the user were to enter “3.0 a b” as input. 3.0 would be extracted to m_x. The extraction to m_y and m_z would both fail, meaning m_y and m_z would be set to 0.0. Our point would be partially overwritten with user input and partially zero’d.

With a Point object (or any object with no class invariants), that might be an acceptable outcome. But imagine we were writing an operator>> for a Fraction object instead. A failed extraction to the denominator would set the denominator to 0.0, which might later cause a divide by zero and cause the application to fail.

So how might we avoid this? One way is to make our operations transactional. A transactional operation must either completely succeed or completely fail -- no partial successes or failures are allowed. This is sometimes known as “all or nothing”. If a failure occurs at any point during the transaction, prior changes made by the operation must be undone.

Key insight

Transactions occur all the time in real life. Consider the case where I want to transfer money from one bank account to another. This requires two steps: First the money must first be deducted from one account, and then it must be credited to the other account. In the execution of this operation, there are three possibilities:

  • The deduction step fails (e.g. not enough funds). The transaction fails, and neither account balance reflects the transfer.
  • The crediting step fails (e.g. due to a technical problem). In this case, the deduction (which has already succeeded) must be undone. The transaction fails, and neither account balance reflects the transfer.
  • Both steps succeed. The transaction is successful, and both account balances reflect the transfer.

The end result is that there are only two possible outcomes: the transfer fully fails and the account balances are unchanged, or the transfer succeeds and the account balances are both changed.

Let’s reimplement our overloaded Point operator>> as a transactional operation:

// note that parameter point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
    double x{};
    double y{};
    double z{};
    
    if (in >> x >> y >> z)      // if all extractions succeeded
        point = Point{x, y, z}; // overwrite our existing point
        
    return in;
}

In this implementation, we’re not overwriting the data members directly with the user’s input. Instead, we’re extracting the user’s input to temporary variables. Once all extraction attempts have completed, we check whether all extractions were successful. If so, then we update all the members of Point together. Otherwise, we do not update any of them.

Tip

if (in >> x >> y >> z) is equivalent to in >> x >> y >> z; if (in). Remember, each extraction returns in so that multiple extractions can be chained together. The single-statement version uses the in returned from the last extraction in the condition of the if-statement, whereas the multi-statement version uses in explicitly.

Tip

Transactional operations can be implemented using a number of different strategies. For example:

  • Alter on success: Store the result of each sub-operation. If all sub-operations succeed, replace the relevant data with the stored results. This is the strategy we use in the Point example above.
  • Restore on failure: Copy any data that can be altered. If any sub-operation fails, the changes made by prior sub-operations can be reverted using the data from copy.
  • Rollback on failure: If any sub-operation fails, each prior sub-operation is reversed (using an opposite sub-operation). This strategy is often used in databases, where the data is too large to back up, and the result of sub-operations can’t be stored.

While this prevents partial extractions, it is inconsistent with how operator>> works for fundamental types. When extraction to an object with a fundamental type fails, the object isn’t left unaltered -- it is copy assigned the value 0 (this ensures the object has some consistent value in case it wasn’t initialized before the extraction attempt). Therefore, for consistency, you may wish to have a failed extraction reset the object to its default state (at least in cases where such a thing exists).

Here’s an alternate version of operator>> that resets Point to its default state if any extraction fails:

// note that parameter point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
    double x{};
    double y{};
    double z{};
    
    in >> x >> y >> z;
    point = in ? Point{x, y, z} : Point{};
        
    return in;
}

Author’s note

Such an operation is technically no longer transactional (because failure doesn’t “do nothing”). There doesn’t appear to be a general term for operations that guarantees no partial results. Perhaps “indivisible operation”.

Handling semantically invalid input

Extraction can fail in different ways.

In cases where operator>> simply fails to extract anything to a variable, std::cin will automatically be placed in failure mode (which we discuss in lesson 9.5 -- std::cin and handling invalid input). The caller of this function can then check std::cin to see if it failed and handle that case as appropriate.

But what about cases where the user inputs a value that is extractable but semantically invalid (e.g. a Fraction with a denominator of 0)? Because std::cin did extract something, it won’t go into failure mode automatically. And then the caller probably won’t realize something went wrong.

To address this, we can have our overloaded operator>> determine whether any of the values that were extracted are semantically invalid, and if so, manually put the input stream in failure mode. This can be done by calling std::cin.setstate(std::ios_base::failbit);.

Here’s an example of a transactional overloaded operator>> for Point that will cause the input stream to enter failure mode if the user inputs an extractable negative value:

std::istream& operator>> (std::istream& in, Point& point)
{
    double x{};
    double y{};
    double z{};
    
    in >> x >> y >> z;
    if (x < 0.0 || y < 0.0 || z < 0.0)       // if any extractable input is negative
        in.setstate(std::ios_base::failbit); // set failure mode manually
    point = in ? Point{x, y, z} : Point{};
       
    return in;
}

Conclusion

Overloading operator<< and operator>> make it easy to output your class to screen and accept user input from the console.

Quiz time

Question #1

Take the Fraction class below and add an overloaded operator<< and operator>> to it. Your operator>> should avoid partial extractions and fail if user inputs a denominator of 0. It should not reset the Fraction to default on failure.

The following program should compile:

int main()
{
	Fraction f1{};
	std::cout << "Enter fraction 1: ";
	std::cin >> f1;

	Fraction f2{};
	std::cout << "Enter fraction 2: ";
	std::cin >> f2;

	std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n'; // note: The result of f1 * f2 is an r-value

	return 0;
}

And produce the result:

Enter fraction 1: 2/3
Enter fraction 2: 3/8
2/3 * 3/8 is 1/4

Here’s the Fraction class:

#include <iostream>
#include <numeric> // for std::gcd
 
class Fraction
{
private:
	int m_numerator{};
	int m_denominator{};
 
public:
	Fraction(int numerator=0, int denominator=1):
		m_numerator{numerator}, m_denominator{denominator}
	{
		// We put reduce() in the constructor to ensure any new fractions we make get reduced!
		// Any fractions that are overwritten will need to be re-reduced
		reduce();
	}

	void reduce()
	{
		int gcd{ std::gcd(m_numerator, m_denominator) };
		if (gcd)
		{
			m_numerator /= gcd;
			m_denominator /= gcd;
		}
	}
 
	friend Fraction operator*(const Fraction& f1, const Fraction& f2);
	friend Fraction operator*(const Fraction& f1, int value);
	friend Fraction operator*(int value, const Fraction& f1);
 
	void print() const
	{
		std::cout << m_numerator << '/' << m_denominator << '\n';
	}
};
 
Fraction operator*(const Fraction& f1, const Fraction& f2)
{
	return Fraction { f1.m_numerator * f2.m_numerator, f1.m_denominator * f2.m_denominator };
}
 
Fraction operator*(const Fraction& f1, int value)
{
	return Fraction { f1.m_numerator * value, f1.m_denominator };
}
 
Fraction operator*(int value, const Fraction& f1)
{
	return Fraction { f1.m_numerator * value, f1.m_denominator };
}

If you’re on a pre-C++17 compiler, you can replace std::gcd with this function:

#include <cmath>
 
int gcd(int a, int b) {
    return (b == 0) ? std::abs(a) : gcd(b, a % b);
}

Show Solution

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
383 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments