Functions can call other functions without a practical limit, creating function call chains (see visualizing function calls). The Actor 4 example uses the term chain as a special case of the general function calls illustrated in the Chapter 6 figure. Chaining in the Actor 4 example refers to a solution distributed over and shared by related classes. Programs implement the solution with a sequence of specific function calls: one constructor calling another constructor in a related class or one function calling another with the same name in a related class. The Actor 4 example begins with the Actor 3 example and adds default constructors, inserters, and extractors to each class. The string class members limit the operations the example can reliably demonstrate (please see the footnote at the end of the copy constructor discussion). Within this constraint, the example successfully demonstrates chaining the following operations:
UML class diagrams describe an object-oriented program's architecture, often called its static structure. A program's architecture concerns its design - its features and relationships with other classes - rather than its construction. We can think of a class diagram as a set of blueprints. Blueprints describe visible features, like doors and the location of electrical outlets, and invisible features, like the wiring and pipes hidden in walls. Similarly, class diagrams specify visible details such as public functions and class relationships - the class's public interface - and its hidden or private features. Just as blueprints guide the construction of houses and buildings, class diagrams guide the building of object-oriented programs.
![]() |
UML class diagrams are language agnostic (3), and so language-specific syntax, like pass and return by reference, is typically not included in them. Nevertheless, I have included the reference notation in the diagram to clarify and reinforce the patterns that help us overload the I/O operators for any class. The following figures organize the code as functional units rather than by classes. A link to the complete program, organized as classes with a driver, is at the bottom of the page.
void setDate(int y, int m, int d)
{
if (date != nullptr)
delete date;
date = new Date(y, m, d);
} |
void setDate(Date* d)
{
if (date != nullptr)
delete date;
date = d;
} |
| (a) | |
~Person()
{
if (date != nullptr)
delete date;
} |
Person() {} // (i)
//Person() : name(), addr(), date(nullptr) {} // (ii)
|
| (b) | (c) |
Date() {} // (i)
//Date() : year(0), month(0), day(0) {} // (ii) |
Address() {} // (i)
//Address() : street(), city() {} // (ii) |
| (c) | |
Actor() {} // (i)
//Actor() : agent() {} // (ii)
//Actor() : Person(), agent() {} // (iii) |
Star() {} // (i)
//Star() : balance(0) {} // (i)
//Star() : Actor(), balance(0) {} // (iii) |
| (c) | |
class Address
{
private:
string street;
string city;
public:
Address(string s, string c) : street(s), city(c) {}
};
|
class Person
{
private:
string name;
Date* date = nullptr;
Address addr;
public:
Person(string n, string s, string c)
: name(n), addr(s, c), date(nullptr) {}
}; |
| (a) | (b) |
class Actor : public Person
{
private:
string agent;
public:
Actor(string n, string a, string s, string c)
: Person(n, s, c), agent(a) {}
}; |
class Star : public Actor
{
private:
double balance;
public:
Star(string n, string a, double b, string s, string c)
: Actor(n, a, s, c), balance(b) {}
}; |
| (c) | |
class Address
{
public:
void display() { cout << street << ", " << city << endl; }
};
class Date
{
public:
void display()
{
cout << year << "/" << month << "/" << day << endl;
}
}; |
class Person
{
private:
string name;
Date* date = nullptr; // aggregation
Address addr; // composition
public:
void display()
{
cout << name << endl;
addr.display();
if (date != nullptr)
date->display();
}
}; |
| (a) | (b) |
class Actor : public Person
{
public:
void display()
{
Person::display();
cout << agent << endl;
}
}; |
class Star : public Actor
{
public:
void display()
{
Actor::display();
cout << balance << endl;
}
}; |
| (c) | |
class Address
{
public:
friend ostream& operator<<(ostream& out, Address& me);
friend istream& operator>>(istream& in, Address& me)
}; |
class Date
{
public:
friend ostream& operator<<(ostream& out, Date& me);
friend istream& operator>>(istream& in, Date& me)
}; |
class Person
{
private:
string name;
Date* date = nullptr; // aggregation
Address addr; // composition
public:
friend ostream& operator<<(ostream& out, Person& me)
{
out << me.name << endl;
out << me.addr << endl;
if (me.date != nullptr)
out << *me.date << endl;
out << endl;
return out;
}
friend istream& operator>>(istream& in, Person& me)
{
cout << "Name: ";
getline(in, me.name);
in >> me.addr;
Date* d = new Date;
in >> *d;
me.setDate(d);
return in;
}
};
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 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.
class Actor : public Person
{
private:
string agent;
public:
friend ostream& operator<<(ostream& out, Actor& me)
{
out << (Person &)me << me.agent << endl;
return out;
}
friend istream& operator>>(istream& in, Actor& me)
{
in >> (Person &)me;
cout << "Agent: ";
getline(in, me.agent);
return in;
}
}; |
class Star : public Actor
{
private:
double balance;
public:
friend ostream& operator<<(ostream& out, Star& me)
{
out << (Actor &)me << me.balance << endl;
return out;
}
friend istream& operator>>(istream& in, Star& me)
{
in >> (Actor &)me;
cout << "Balance: ";
in >> me.balance;
return in;
}
}; |
Programmers implement function chaining by casting subclass objects to their superclass (highlighted with light blue). The cast operation doesn't modify the argument but creates an expression that matches the second argument of the superclass I/O function. The casting operations temporarily make a Star an Actor and an Actor a Person, chaining the I/O functions.
int main()
{
// Automatic variable/object
Star s1("John Wayne", "Cranston Snort", 50000000, "123 Palm Springs", "California");
s1.setDate(1960, 12, 25);
s1.display();
cout << endl;
cout << s1 << endl;
// Dynamic variable/object
Star* s2 = new Star("John Wayne", "Cranston Snort", 50000000, "123 Palm Springs", "California");
s2->setDate(1960, 12, 25);
s2->display();
cout << endl;
cout << *s2 << endl;
// Automatic with console input
Star s3;
cin >> s3;
Date* d1 = new Date;
cin >> *d1;
s3.setDate(d1);
s3.display();
cout << s3;
// Dynamic with console input
Star* s4 = new Star;
cin >> *s4;
Date* d2 = new Date;
cin >> *d2;
s4->setDate(d2);
s4->display();
cout << *s4;
return 0;
}
new, read and save the test data with a chain of extractor calls, and retrieve it with the display and extractor functions.
| s1.display(); | cout << s1 << endl; |
|---|---|
| s2->display(); | cout << *s2 << endl; |
John Wayne 123 Palm Springs, California 1960/12/25 Cranston Snort 5e+07 |
John Wayne 123 Palm Springs, California 1960/12/25 Cranston Snort 5e+07 |
| View | Download | Comments |
|---|---|---|
| Address.h | Address.h | Added operator<<, operator>>, and an alternate default constructor |
| Date.h | Date.h | Added operator<< and operator>> |
| Person.h | Person.h | Added composition, aggregation, I/O operators, and an alternate default constructor |
| Actor.h | Actor.h | Added operator<< and operator>> |
| Star.h | Star.h | |
| Actor4.cpp | Actor4.cpp | Added tests of composition, aggregation, and I/O operators |