11.5.3. operator<< And operator>> With Whole-Part

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

We begin by reviewing some details about whole-part relationships:

How we build the whole-part relationship determines how we overload the inserter and extractor operators.

It is common practice for the whole object to send messages to its parts (i.e., for the whole's functions to call the part's functions). In this section, we focus on the syntax needed to chain the whole class's inserter, operator<<, and the extractor, operator>> functions to the corresponding functions in the part class. The I/O functions are always implemented as non-member friend functions, without a this pointer. Therefore, when a whole object sends a message to one of its parts, it must include a reference to the part receiving the message. All access to the members of the part must be through the parameter reference. A program may use many classes that define overloaded inserters and extractors, and the compiler chooses between them based on the second parameter.

Embedded Whole-Part (Composition)

PartWholeUML
class part
{
    private:
        string    name;
        double    cost;
    public:
	. . .
};
class whole
{
    private:
        part    my_part;
        int     simple;
    public:
	. . .
};
UML class diagram showing a Whole class connected to a Part by composition.
friend ostream& operator<<(ostream& out, part& me)
{
	out << me.name << endl;
	out << me.cost << endl;
	return out;
}
friend ostream& operator<<(ostream& out, whole& me)
{
	out << me.my_part << endl;
	out << me.simple << endl;
	return out;
}
friend istream& operator>>(istream& in, part& me)
{
	in >> me.name;
	in >> me.cost;
	return in;
}
friend istream& operator>>(istream& in, whole& me)
{
	in >> me.my_part;
	in >> me.simple;
	return in;
}
 
Chaining the I/O operators in composition. Execution begins with the whole operator functions, which call the corresponding operator functions in the part. The syntax for calling an overloaded operator defined in the part class is:
  1. the name of the second parameter in the whole class function (highlighted in blue)
  2. the dot operator
  3. the name of the part member variable in the whole class (highlighted in light pink)
Significantly, out << me.my_part and in >> me.my_part are function calls, calling the corresponding functions in the part class.

Pointer Whole-Part (Aggregation)

Unlike composition relationships, programs can establish and change the aggregation connecting whole and part objects at any time. Consequently, the constructor or a setter function may set an aggregating pointer to nullptr at any point in program execution. It is a runtime error for a program to attempt to access data through a nullptr. Safe programming practices require the I/O functions to test for and avoid this error. In the following examples, if my_part doesn't point to a valid part object, the program skips the output statement.

PartWholeUML
class part
{
    private:
        string    name;
        double    cost;
    public:
	. . .
};
class whole
{
    private:
        part*   my_part;
        int     simple;
    public:
	. . .
};
UML class diagram showing a Whole class connected to a Part by aggregation.
friend ostream& operator<<(ostream& out, part& me)
{
	out << me.name << endl;
	out << me.cost << endl;
	return out;
}
 
friend ostream& operator<<(ostream& out, whole&  me)
{
    if (me.my_part != nullptr)
        out << *me.my_part << endl;
    out << me.simple << endl;
    return out;
}
friend istream& operator>>(istream& in, part& me)
{
	in >> me.name;
	in >> me.cost;
	return in;
}
 
friend istream& operator>>(istream& in, whole& me)
{
    if (me.my_part != nullptr)
        in >> *me.my_part;
    in >> me.simple;
    return in;
}
 
Chaining the I/O operations in aggregation. The chaining sequence begins when a program calls one of the whole classes' I/O functions, which calls the corresponding part functions. Chaining the I/O functions requires the whole-class function to match its parameters to the parameters in the part-class function. The whole class uses a pointer member variable to build the whole-part relationship. However, the part-class I/O functions require a reference or non-pointer. So, the whole-class functions must dereference the pointer to match the part's I/O functions. The complete matching process is as follows:
  1. The dereference operator, *, has a higher precedence than the dot selection operator and operates first, forming the sub-expression *me. The sub-expression evaluates to a reference to the object.
  2. The dot selection operator completes the expression *me.my_part, accessing the part's my_part member.