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:
Constructor calls
The program begins by instantiating a Star object. The Star constructor calls and passes data to the Actor constructor, which calls and passes data to the Person constructor. The Address class is part of the Person class by composition and is constructed with the Person and receives some of the data passed through the constructor chain.
Member function calls
Although it's simple, the display demonstrates the syntax and typical patterns used when chaining member function calls.
I/O operators
The example adds overloaded operator<< and operator>> operators to each class. The inserter serves the same purpose as the display function, and a "real" program would typically have one or the other but not both. However, the two output functions require different syntax. Furthermore, as demonstrated later in the polymorphism chapter, they have different behaviors besides displaying data.
Program Architecture
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.
Program architecture: The Actor 4 UML class diagram.
The example demonstrates how programmers chain functions and operator calls using the class relationships that bind objects into a working program. The example begins by creating an instance of the Star class. The extractors and constructors "push" data up to the other classes, and the inserters and display functions "pull" data down.
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.
Support Functions
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;
}
Actor 4 support functions: Constructors, destructor, and setter functions.
The support functions don't play explicit roles in the chaining examples but are necessary for program operation.
The setter functions replace the aggregated Date part with a new object. They destroy or deallocate the old object before installing the new one. The first function constructs a Date object from its basic "ingredients," while the second installs one created elsewhere in the program. The functions perform the same task, so a class typically only has one.
When the program destroys a Person object, its destructor conditionally destroys or deallocates its Date part, preventing a memory leak.
The examples show two or three possible default constructors:
The first (light blue) illustrates the syntax preferred since the ANSI 2014 C++ standard allowed member initialization in the class specification. Although they perform no operations, these constructors are necessary. The compiler automatically creates default constructors if the class has no programmer-created constructors. By adding an explicit constructor, programmers effectively say, "This is the only way to create objects from my class." The empty default constructors "give client program permission" to create empty objects.
The second version (light green) illustrates the syntax necessary before the 2014 ANSI standard.
The third (violet) illustrates the explicit default constructor chaining.
Chaining General Constructors
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)
Actor 4: Chaining general constructors.
In this example, it's sufficient to chain constructors for classes related by composition and inheritance as the program initializes the aggregated class with a setter function. Each call in the chain retains some data for the defining class and passes the remaining data upward in the diagram. The example highlights the chained constructor calls and corresponding features to help identify and match them.
Composition is a one-way relationship. Consequently, the part class, Address, doesn't "know about" the whole class, Person, and its name doesn't appear in the part's specification. Throughout the example, strings is a class, and calls to its constructs form part of the constructor call chain, but, for simplicity, the example treats it as a fundamental type, ignoring its status as a class.
To call a constructor in a composed class, the program uses the name of the member variable implementing the relationship.
Inheritance is a one-way relationship where the subclass or child "knows about" the superclass or parent. A subclass calls its superclass's constructor using the superclass's name.
Chaining Member Functions
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)
Actor 4: Chaining member functions.
The driver instantiates and initializes an Actor object. When the program "displays" the actor, it "wants" to display all its information, including information saved in its related super and part classes, demonstrating a distributed problem and corresponding solution. Although the display functions are small and simple, they are member functions and exemplify the syntax necessary to chain member function calls. The example highlights the chained constructor calls and corresponding features to help identify and match them.
The part classes, Address and Date, define simple display functions.
The Person class "has" a Date (by aggregation) and an Address (by composition), so its display function calls the corresponding functions in the part classes. Calling the composed Address (green) is straightforward, using the implementing member variable's name and the dot operator. Calling a function in an aggregated class is problematic. Attempting to call a function with a null pointer is a runtime error. The Person display function must test for this condition and skip the call when the pointer is null.
The program defines five distinct display functions, one in each class; three of those functions are available to the Actor class through inheritance. Programmers disambiguate (clarify or specify) the intended function with the superclass name and scope resolution operator (pink and purple).
Chaining I/O Function Calls
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)
};
Actor 4: Part classes I/O functions. The example composes and aggregates the part classes, Address and Date, the Person class. Each part has inserters and extractors, following the basic operator patterns established previously. The functions are "leaves" on the class diagram tree and do not play a role in the chaining operations. Nevertheless, The Address extractor has one noteworthy feature demonstrating that the I/O functions may use whatever functions necessary to complete their tasks: it uses the string getline function to input the street and city:
getline(in, me.street);
getline(in, me.city);
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;
}
};
Actor 4: Actor and Star overloaded I/O operators.
Inheritance is a one-way relationship from the subclass to the superclass. So, a Star "knows about" an Actor, and an Actor "knows about" a Person. Said another way, a Star "is an" Actor, and an Actor "is a"Person. We can use the "is a" concept to chain I/O function calls together so that the subclass functions call their superclass's corresponding functions. Given two functions with the same name, the compiler determines which to call based on the second parameter, which is a reference to the befriending class and is, therefore, unique for every function.
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.
Actor 4 Driver
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
Actor 4 output.
The Actor 4 driver has four logical code groups demonstrating I/O operations with stack and heap objects. The third and fourth groups read user-entered data from the console, making their output dependent on the input. However, the data for the first two groups is "hard coded" in the driver, making it possible to demonstrate their output, which is the same for both groups. More significantly, the display function and inserter operator produce the same output.