11.5.2. operator<< And operator>>With Inheritance

Time: 00:02:51 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

operator<< and operator>> (the inserter and extractor operators, respectively) are frequently overloaded for new classes. The implementing functions follow a strict pattern, so overloading them is relatively easy. However, some unusual syntax is required when implementing them in classes forming an inheritance hierarchy, especially when chaining the calls together. Fortunately, we can extend the patterns to cover operations chained through inheritance.

Individually, each I/O operator follows the pattern outlined previously. Each is a friend function. The inserter always returns an ostream reference and has an ostream reference as its first parameter; the extractor always returns an istream reference and has an istream reference as its first parameter. Each operator function has a reference to the befriending class as its second parameter. However, the subclasses, Actor and Star, add expressions calling their immediate superclass' I/o operator functions. Forming that expression is the focus of this section.

Person
friend ostream& operator<<(ostream& out, Person& me)
{
    out << me.name << endl;
    return out;
}
A picture of a UML class diagram showing three classes related by inheritance.

Person
-------------------
-name : string
-------------------
+Person(n : string)
+operator<<(out : ostream&, me Person&) : ostream&

Actor
-------------------
-agent : string
-------------------
+Actor(n : string, a : string)
+operator<<(out : ostream&, me Actor&) : ostream&

Star
-------------------
-balance : double
-------------------
+Star(n : string, a : string, b : double)
+operator<<(out : ostream&, me : Star&) : ostream&
Actor
friend ostream& operator<<(ostream& out, Actor& me)
{
    out << (Person &)me << " " << me.agent << endl;
    return out;
}
Star
friend ostream& operator<<(ostream& out, Star& me)
{
    out << (Actor &)me << " " << me.balance << endl;
    return out;
}
A picture illustrating the inserter operator's operands passed to overloaded inserter functions' parameters.
Chaining inserter calls from subclasses to superclasses. The compiler always selects which overloaded function to call by matching the arguments in the function call with the parameters in the function's signature. The first parameter (i.e., the left-hand operand) is always an ostream reference. Therefore, the compiler matches a function call to the corresponding function based on the second parameter (i.e., the right-hand operand).

A Star is an Actor and an Actor is a Person. So, a Star object has a balance and, by inheritance, an agent and a name. If a program prints a Star object, it should print all three data members. However, the inherited members are private in the superclasses, and the Star object can't access their members directly; it must use their public interfaces, specifically operator<<. Calling the superclass inserter requires converting a subclass object to a superclass. For example, an Actor is a Person by inheritance, so converting an Actor to a Person makes sense.

Class Function Signature Casting To Superclass
Person friend ostream& operator<<(ostream& out, Person& me) N/A
Actor friend ostream& operator<<(ostream& out, Actor& me) (Person &)me
Person friend ostream& operator<<(ostream& out, Star& me) (Actor &)me

The casting operations form expressions converting a Star reference to an Actor reference and an Actor reference to a Person reference, but they don't alter the values saved in me. The expressions have statement scope, and the program discards their effect when the statement ends. Following the cast, the expressions match the second or right-hand operand of the << operator (i.e., the expression matches the second parameter in the superclass operator<< function).

Person
friend istream& operator>>(istream& in, Person& me)
{
    getline(in, me.name);
    return in;
}
A picture of a UML class diagram showing three classes related by inheritance.

Person
-------------------
-name : string
-------------------
+Person(n : string)
+operator>>(out : istream&, me Person&) : istream&

Actor
-------------------
-agent : string
-------------------
+Actor(n : string, a : string)
+operator>>(out : istream&, me Actor&) : istream&

Star
-------------------
-balance : double
-------------------
+Star(n : string, a : string, b : double)
+operator>>(out : istream&, me : Star&) : istream&
Actor
friend istream& operator>>(istream& in, Actor& me)
{
    in >> (Person &)me;
    getline(in, me.agent);
    return in;
}
Star
friend istream& operator>>(istream& in, Star& me)
{
    in >> (Actor &)me;
    in >> me.balance;
    return in;
}
Chaining extractor calls from subclasses to superclasses. Minimal edits change the inserters to extractors: Change the operator symbols, istream replaces ostream and, for clarity, in replaces out. The bodies of the extractor functions use operations appropriate for the data they read, but the crucial casting operations are the same.