11.5.4. Actor 4: Chaining Operators And Function Calls

Time: 00:6:34 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

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.

The Actor example UML class diagram. Inheritance connects Person, Actor, and Star. Composition connects Person, the whole, with Address, the part. Aggregation connects Person, the whole, with Date, the part.
Address class Person class Date class Actor class Star class
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;
}
(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)
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.
  1. 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.
  2. When the program destroys a Person object, its destructor conditionally destroys or deallocates its Date part, preventing a memory leak.
  3. The examples show two or three possible default constructors:
    1. 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.
    2. The second version (light green) illustrates the syntax necessary before the 2014 ANSI standard.
    3. 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.
  1. 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.
  2. To call a constructor in a composed class, the program uses the name of the member variable implementing the relationship.
  3. 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.
  1. The part classes, Address and Date, define simple display functions.
  2. 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.
  3. 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:
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;
        }
};
Actor 4: Person overloaded I/O operators. Person is a complex class playing two distinct roles. First, it is at the top of an inheritance hierarchy - the superclass to Actor and Star. Inheritance is unidirectional, and a superclass does not "know" about its subclasses. Therefore, Actor and Star don't appear in the Person class's I/O functions. However, Person also plays a second role as the whole to the Address and Date parts. Whole classes do "know" about their parts, and so the Person I/O functions reflect that knowledge by calling the corresponding functions in each of its parts in addition to printing its member data.

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;
        }
};
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

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;
}
Actor 4: main. A main function completes the Actor 4 example. It consists of four sections; each section instantiates a Star object and uses it to demonstrate the above functions. The first two sections create the Star on the stack, "pushing" the demonstration data through the constructor chain and "pulling" it back with the display and extractor functions. The last two sections create the Star on the heap with 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
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.

Downloadable Code

ViewDownloadComments
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