Skip to main content

Section 18.12 Self Aggregation

In C++, you cannot use composition to define a relationship between two items of the same class. If we try to compile this:
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
private:
    string m_name;
    Person m_spouse;
};
The compiler will tell us that we are trying to use an incomplete type for m_spouse:
test.cpp:9:12: error: field ‘m_spouse’ has incomplete type ‘Person’
    9 |     Person m_spouse;
      |            ^~~~~~~~
test.cpp:5:7: note: definition of ‘class Person’ is not complete until the closing brace
    5 | class Person {
      |       ^~~~~~
Until we have finished defining Person, it is not OK to use Person to name a variable. Even if this was allowed, we would have problems trying to construct a Person. To construct a Person, you would have to set up memory for their m_spouse, which is a Person. That Person object would need to allocate some space for its m_spouse, which in turn would need memory for another m_spouse, and so on forever.
This is not a problem if we use aggregation instead of composition. In the aggregation version of this code, we are not trying to store a Person inside of a Person. We are simply storing a memory address inside each Person. So this code will compile and run just fine:
Listing 18.12.1.
Here are a few of the interesting uses of the pointer:
  • When we construct a Person, their m_spouse is set to nullptr as we assume everyone starts unmarried.
  • The print() function checks to see if there is a spouse (remember that if (m_spouse) will be false if the spouse is a nullptr) to determine what to print. Only if there is a spouse do we use -> to access the name of that spouse.
  • The marry(Person* spouse) function takes the address of a Person object. It first sets the current object’s spouse to be that address. It then sets the spouse’s spouse spouse->m_spouse to be the current object’s address (this). Thus when we say anna.marry(&brian);, the anna object first stores the brian object’s address. It then sets the brian object’s m_spouse to be anna.
When the anna.marry(&brian) function is called, the memory for the call stack looks like the diagram below. The Anna and Brian objects in main still do not have spouses set. In the marry function, the this pointer has anna’s address, while the spouse parameter holds brian’s address:
In marry, this has the address 0x400 (that of the anna object in main) and spouse has the address 0x6f0 (that of the brian object in main).
Figure 18.12.2. Memory diagram at the start of anna.marry(&brian).
The first line of the marry function - m_spouse = spouse; - changes anna’s m_spouse to be 0x6f0. So Anna’s spouse now points at the Brian object. The second line - spouse->m_spouse = this; - starts from spouse, follows the pointer (->) to access m_spouse. Thus it reaches the brian object’s m_spouse. It sets that to the value of this, which is 0x400. So Brian’s spouse is now Anna.
anna’s m_spouse now has brian’s address (0x6f0). brian’s m_spouse now has anna’s address (0x400)
Figure 18.12.3. Memory diagram at the end of anna.marry(&brian).

Checkpoint 18.12.1.

Checkpoint 18.12.2.

Construct a Person function getDivorce(). If the Person executing the function has a spouse, it should clear the spouse variable of both the spouse and current Person by setting them to nullptr. You will not need all of the blocks.
Hint.
We can’t clear out the current object’s m_spouse before we use it to access the spouse and clear that object’s m_spouse!
You have attempted of activities on this page.