The Actor 4 example begins where the Actor 3 example ends. As illustrated in the following UML class diagram, Actor 4 adds an overloaded inserter operator (operator<<) to each of the Actor 3 classes. The inserter serves the same purpose as the display function, and a "real" program would typically have one or the other but not both.
operator<<
, to each class. The example demonstrates how programmers can chain inserter calls together by using the class relationships that bind the objects into a working program.
The example begins execution when the program creates an instance of the Star class in Actor 3, and the constructors "push" data from the Star constructor call to the other classes. When called, the Star inserter "pulls" the information from the other classes.
UML class diagrams are language agnostic (3), and so language-specific syntax, like pass and return by reference, is typically not included in the diagrams. Nevertheless, I have included the reference notation in the Actor example to clarify and reinforce some of the patterns that help us overload the inserter operator for any class. As you study the following examples, look for these elements. The inserter always:
friend
functionostream&
ostream& out
Date& me
, etc.return out
All the inserters in the Actor example are very sort. So, each function is implemented inside its respective class. Place these function inside the Actor 3 class specifications to create the complete, final classes.
friend ostream& operator<<(ostream& out, Address& me) { out << me.street << ", " << me.city << endl; return out; }Date
friend ostream& operator<<(ostream& out, Date& me) { out << me.year << "/" << me.month << "/ " << me.day << endl; return out; }
friend ostream& operator<<(ostream& out, Person& me) { out << me.name << " " << me.addr << endl; // composition and Person data if (me.date != nullptr) out << *me.date << endl; // aggregation return out; }
Aggregation and its pointers are the source of much of the whole class's complexity. Attempting to use a null pointer (e.g., as the left-hand operand to either the dot or arrow operator) causes a runtime error. So, we conditionally call the aggregated part only if the pointer is not null: if (me.date != nullptr)
. Next, no inserter function takes a pointer argument. So, we must dereference the pointer so the call matches the function's parameter list: *me.date
.
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; }
To do this, we cast the second argument into an instance of the superclass: (Actor &)me
and (Person &)me
. Not every cast involving a reference requires the ampersand, but it is needed sometimes and it never causes a problem to include it. The cast operation doesn't modify the argument but creates an expression that matches the the second argument of the superclass constructor. So, out << (Actor &)me
matches the Actor inserter's parameter list and calls that function. Similarly, out << (Person &)me
matches the Person inserter's parameter list, and calls that function.