10.5.3. Using Aggregation

Review

Aggregation's weak binding, implemented with class-scope pointer variables, affords us more options for building and maintaining a relationship than composition. But it also burdens us with the necessity of validating the pointers before using them and carefully managing dynamic memory to prevent a memory leak. Like composition, aggregation does not circumvent the access restrictions imposed by the private and protected keywords. So, programmers access these restricted part-class features through the class's public interface. The following examples demonstrate the whole class sending messages to (i.e., calling public functions in) their part classes. In an aggregation relationship, we send a message to a part object using the pointer variable name, the arrow operator, and the message or function name.

A UML diagram of two classes joined by aggregation:
Part
--
- member : int
--
+ display() : void

Whole
--

--
+ display() : void
class Part
{
    private:
        int member;
    public:
        void display()			// (a)
        {
            cout << member << endl;
        }
};
 
class Whole
{
    private:
        Part* my_part = nullptr;	// (b)
    public:
        void display()			// (c)
        {
            if (my_part != nullptr)	// (d)
                my_part->display();
        }
};
Using simple aggregation. The two partial and generically named classes demonstrate how to use a simple aggregation relationship. The example also illustrates the common situation where a whole class function calls a part class function with the same name.
  1. The Part class has a member function, display, that can access its private features.
  2. The Whole class implements aggregation with a pointer member variable, my_part, that can point to an instance of the Part class. Programmers may initialize the pointer here or in the constructor.
  3. The Whole class has a display function that can call the Part's display function through the pointer member. In the object-oriented vernacular, the Whole object sends the display message to the part.
  4. The if-statement validates the pointer before making the function call. Attempting to call a function through a null or otherwise invalid pointer causes a runtime error aborting the program (i.e., the program "crashes and burns"). my_part is not automatically initialized and may contain a random value that is neither null nor a valid pointer. So, the program must initialize it to nullptr, either in the class specification or the constructor.
class Address
{
    private:
	string city;
	string state;
    public:
	Address(string c, string s) : city(c), state(s) {}

	void display()
	{
	    cout << city << ", " << endl;
	}
};

class Person
{
    private:
	string name;
	Address* addr = nullptr;			// (a)
    public:
	Person(string n, string c, string s)
            : addr(new Address(c, s)), name(n)		// (b)
	Person(string n) : addr(nullptr), name(n) {}	// (c)

	void display()
	{
	    cout << name << end;
	    if (addr != nullptr)			// (d)
	        addr->display();			// (e)
	}
	
	void setAddress(string c, string s)
	{
	    if (addr != nullptr)			// (f)
	        delete addr;
	    addr = new Address(c, s);			// (g)
	}
};

class Student : public Person				// (h)
{
    private:
	double gpa;
    public:
	Student(string n, double g, string c, string s)
	    : Person(n, c, s), gpa(g) {}		// (i)

	void display()
	{
	    Person::display();				// (j)
	    cout << gpa << endl;
	}
}
Three classes connected by two relationships. Student is a subclass of Person, and Person has an Address by aggregation.
  1. Building aggregation: The part class name, Address, is a data type, and addr is the object's name. The asterisk makes addr a pointer.
  2. C++ uses the part object's name to call its constructors. The arguments in the constructor calls match its parameters.
  3. If the pointer is not initialized in the class specification (a) and not assigned an object in a constructor (b), it must be initialized to nullptr.
  4. Calling a function with a null or otherwise invalid pointer is a runtime error. The if-statement prevents this error, increasing the class's security and robustness.
  5. Using aggregation: The part class name and the arrow operator, adder->, call the Address display function (i.e., sends the "display" message to the addr object).
  6. The if-statement determines if the Person has an aggregated Address and deallocates it if it does. Failing to deallocate an unneeded aggregated part creates a memory leak. The if-test is probably not required with most modern compilers - only the delete operation is required.
  7. Creates a new Address object and aggregates it to the Person.
  8. Building inheritance: Student is the subclass and Person the superclass.
  9. Initialize inheritance with a superclass constructor call. Always the first initializer operation, the arguments in the call match the constructor parameters.
  10. Using inheritance: The superclass name and scope resolution operator call the Person display function.
Using inheritance and aggregation (example 1). Programmers implement aggregation by translating the UML aggregation symbol to a class-scope pointer in the whole class and initializing it with the address of a part object. They can complete the initialization in the whole-class constructor or a setter function. All member variables are private and only accessible outside the defining class through its public interface. The example begins by instantiating a Student object and sending it a "display" message - i.e., calling its display function:
Student valedictorian("Alice", 4.0, "Ogden", "Utah");
valedictorian.display();
class Pet
{
    private:
	string name;
	string vaccinations;
    public:
	Pet(string n, string v) : name(n), vaccinations(v) {}

	void display()
	{
	    cout << name << " " << vaccinations << endl;
	}
};

class Person
{
    private:
	string name;
	string phone;
    public:
	Person(string n, string p) : name(n), phone(p) {}

	void display()
	{
	    cout << name << endl;
	    cout << phone << endl;
	}
};

class Owner : public Person				// (a)
{
    private:
	int account;
	Pet* my_pet = nullptr;				// (b)
    public:
	Owner(string n, string p, int a, string pn, string v)
	    : Person(n, p),				// (c)
              my_pet(pn, v), account(a) {}		// (d)

	void display()
	{
            Person::display();				// (e)
            cout << account << endl;
	    if (my_pet != nullptr)
                my_pet->display();			// (f)
	}
};
Class Owner is a subclass of Person. An Owner has a Pet by aggregation.
  1. Building inheritance: Owner is the subclass and Person the superclass.
  2. Building aggregation: C++ uses the part class name, Pet, as a data, and my_pet as the object's name. The asterisk makes my_pet a pointer.
  3. Person calls the superclass constructor.
  4. my_pet calls the Owner constructor.
  5. Uses inheritance: Calls the Person display function.
  6. Uses aggregation: Calls the Pet display function.
Using inheritance and aggregation (example 2). Like the previous example, this one also has three classes related by inheritance and composition, but it moves the part from the superclass to the subclass. Moving the part object to the subclass changes where the program calls the part constructor and the display functions. The example runs when it instantiates an Owner object and then sends that object a "display" message - that is, it calls the display function through the Owner object:
Owner pet_owner("Dilbert", "801-555-1234", 123456, "Fido", "2020/06/01");
pet_owner.display();