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

Review

Let's begin by recalling some detail 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 inserter and the extractor functions are always implemented as non-member friend functions, so whole must access its parts through the second function parameter. Similarly, the parts must also access their members through the second parameter. 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)

PartWhole
class part
{
    private:
        string    name;
        double    cost;
    public:
	. . .
};
class whole
{
    private:
        part    my_part;
        int     simple;
    public:
	. . .
};
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 operator calls in an embedded whole-part (composition) relationship. 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 yellow)
  2. the dot operator
  3. the name of the part member variable in the whole class (highlighted in light blue)
It's important to recognize that out << me.my_part and in >> me.my_part are function calls - they call the corresponding functions in the part class.

Pointer Whole-Part (Aggregation)

PartWhole
class part
{
    private:
        string    name;
        double    cost;
    public:
	. . .
};
class whole
{
    private:
        part*   my_part;
        int     simple;
    public:
	. . .
};
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 operator calls in an pointer-implemented whole-part (aggregation) relationship. Execution begins with the whole operator functions, which call the corresponding part operator functions. The challenge of chaining the operator functions is making the arguments in the call match the parameters in the function. The whole class uses a pointer member variable to build the whole-part relationship. But the part class doesn't have an operator function with a pointer parameter. So, the whole operator functions must dereference the pointer with the dereference or indirection operator. The syntax for calling an overloaded operator defined in the part class is:
  1. the dereference operator (*)
  2. the name of the second parameter in the whole class function (highlighted in yellow)
  3. the dot operator
  4. the name of the part member variable in the whole class (highlighted in light blue)
There are two bits of each whole function that require further elaboration:
  1. The if-statement is necessary because any attempt to call a function through a null pointer will result in a runtime error. So, if my_part doesn't currently point to a part object, the program skips the output statement.
  2. Given that my_pointer is a pointer variable, it may be surprising that the output portion of the if-statement does not use the -> (arrow) operator. To understand why, first notice that the output statement has three operators: << or >> (inserter or extractor), * (dereference), and . (dot).
    • When performing member selection with the arrow or dot operator, look at the variable the left of the selection operator. In this example, the variable is me, which is NOT a pointer, so use the dot operator to select a member in the object.
    • The expression me.my_part, returns the value stored in my_part, which IS a pointer. The pointer must be dereferenced to match the parameters of the corresponding part function.
    • Finally, the correct inserter or extractor function in the part class is called.