07 - stream operators

operator<< and operator>> are officially named bitwise shift operators but you might also encounter the name stream insertion/extraction operators. At this point you should be very accustomed to using std::cout and std::cin with these operators.

In other words, these operators have 2 common usage patterns:

  • bit shift operations (multiplication/division by powers of 2)

  • reading or writing data to a stream

In this lesson you will learn how to write operator overloads that work with standard library streams.

Operator chaining

Streams allow operator chaining - you can use << and >> multiple times in one statement. This feature is powered by the return type having the same overloaded operator. To illustrate:

1
2
3
4
5
6
7
int x = 1 + 2 + 3 + 4;
int y = ((1 + 2) + 3) + 4; // equivalent
int y = operator+(operator+(operator+(1, 2), 3), 4); // pseudo-code

std::cout << 1 << 2 << 3 << 4;
(((std::cout << 1) << 2) << 3) << 4; // equivalent
operator<<(operator<<(operator<<(operator<<(std::cout, 1), 2), 3), 4); // equivalent

Simply put, the trick is to return a reference to the stream. Then, the same operation can be repeated.

Canonical implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::ostream& operator<<(std::ostream& os, fraction fr)
{
	return os << fr.numerator() << "/" << fr.denominator();
}

std::istream& operator>>(std::istream& is, fraction& fr)
{
	// implementations can vary a lot
	// this one works with the output overload but does no error handling
	int value = 0;
	is >> value;
	fr.numerator(value);
	is.ignore(); // ignores next character
	is >> value;
	fr.denominator(value);
	return is;
}

Standard argument passing recommendations apply:

  • In both operators stream is taken by non-const reference because any work on the stream changes its state.

  • Stream insertion only reads the data so should it should take the object by value or by const reference (here: by value because fraction type is cheap to copy).

  • Stream extraction stores the result in the object so the object must be passed by non-const reference.

Example with string stream:

1
2
3
4
5
std::stringstream ss("1/2 3/4"); // requires <sstream>
fraction fr1;
fraction fr2;
ss >> fr1 >> fr2;
std::cout << fr1 << " " << fr2;

How about operator<<= and operator>>=?

Their implementation should follow the same guidelines as other compound assignment operators but these operators only make sense when operator<< and operator>> are implemented to perform mathematical operations. If you implement stream insertion/extraction, these operators should be left unimplemented.

Exercise

What's wrong with the implementation below?

1
2
3
4
std::ostream& operator<<(std::ostream& os, fraction fr)
{
	return std::cout << fr.numerator() << "/" << fr.denominator();
}
Answer

The function does not use the stream provided as an argument. Instead, it always inserts data to std::cout. This is a bug because someone might want to output a fraction to a different stream. Even worse that this function will also return a reference to the wrong stream object.