As a more in depth example of using aggregation, let us model a family tree. The basis of our program will be a Person class. Every Person has a name. They may have a mother that we know about and they may also have a number of children we need to represent. (For brevity, we will skip representing fathers... the logic would be the same as for mothers.) A Person will thus need a Person* to store the mother, it will start out as a nullptr. It will also need a vector<Person*> to store any children. An empty vector will indicate no children, so we donโt need to explicitly add any nullptrs to it.
The implementation for this is shown below. Much of this code is similar to the Person class we used on the previous page, here are the key new features:
getMother and getChild both return Person*s as we want to return the memory address to the appropriate object instead of a copy of the object. If getMother looked like Person getMother() const it would return a copy of the object that m_mother points at.
getChild takes a parameter to specify which child to retrieve. The parameter is a size_t (not an int) as we will use it to specify a location in the vector.
setMother is similar to the marry function from our spouse sample. When we set a Personโs mother, we also make sure that the mother object has the child Person in their list of children. mother->m_children.push_back(this) tells the mother object to add the current object to its list of children.
To work with this class, we will need to set up some Persons and establish links between them. The figure below shows the relations we will set up. Each person has an arrow pointing at their mother, which corresponds to the `m_mother. (This is not a true memory diagram - it does not visualize the pointers that point back at the children from the mother.)
Henry and George have Fay as mother. Fay has Erin as Mother. Erin has Diana as mother. Diana, Carl and Bob have Anna as mother.
stateDiagram-v2
direction BT
Anna: Anna (p0)
Bob: Bob (p1)
Carl: Carl (p2)
Diana: Diana (p3)
Erin: Erin (p4)
Fay: Fay (p5)
George: George (p6)
Henry: Henry (p7)
Bob --> Anna
Carl --> Anna
Diana --> Anna
Erin --> Diana
Fay --> Erin
George --> Fay
Henry --> Fay
Figure18.13.2.The family relations set up in the programs below.
Notice that because we are using aggregation, when we change Fayโs name to Fiona (line 32), the Henry object โseesโ the change. On line 35, as we print out Henry, we see
Recall that this is the key distinction of storing a Person* as the m_mother instead of a Person object. Because we are using Person*s, objects โseeโ changes that are made to the object that was set as their m_mother. If the Henry object stored a Person, it would make a copy of the Fay object when their relationship was set up. In that case, the Henry object would never โseeโ future changes to the Fay object.
To do this, we need a way to track โthe current personโ. However, we need to be careful about how we do this. If we use a Person object to store โthe current personโ, we will be making a copy of whatever Person we assign to it. But we do not want to make a copies of any Person once the family is set up. There should only ever be one โHenryโ (the variable named p7) in the program.We need currentPerson to be an alias for one of the existing Person objects, and not a new Person. Thus we will make Person* currentPerson. Using that, we can point at p7 and have access to the Henry object without copying it.
This program starts with the same family set up code. Skip past it and examine lines 28+. They set up some pointers to point to Henry and his ancestors.
Rather than write out each next step in accessing Henryโs ancestors by hand, we could use a loop to โwalk upโ the family tree. The next program does exactly that. It reuse the currentPerson pointer to always store the person we are currently at. To โstep upโ the tree, we can change the currentPerson to be the mother of the old currentPerson. Once the currentPerson is null, we know we have run out of family members!
The tricky bit is the last line of the loop (40). We change the memory address stored in currentPerson to be a copy of the address stored in currentMother. This updates the currentPerson pointer to point at the mother of the old currentPerson. Then, we bounce up to the start of the loop to check if we have reached an unknown mother (nullptr) - that will be our sign that we have reached the top of the tree and it is time to stop.
Construct a Person function getSiblings that returns a vector containing all of the siblings of the current Person. If the current Personโs mother is unknown, we will return an empty vector.