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<<=
andoperator>>=
?
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.