In the previous two lessons, we’ve looked at two types of object composition, composition and aggregation. Object composition is used to model relationships where a complex object is built from one or more simpler objects (parts).
In this lesson, we’ll take a look at a weaker type of relationship between two otherwise unrelated objects, called an association. Unlike object composition relationships, in an association, there is no implied whole/part relationship.
Association
To qualify as an association, an object and another object must have the following relationship:
- The associated object (member) is otherwise unrelated to the object (class)
- The associated object (member) can belong to more than one object (class) at a time
- The associated object (member) does not have its existence managed by the object (class)
- The associated object (member) may or may not know about the existence of the object (class)
Unlike a composition or aggregation, where the part is a part of the whole object, in an association, the associated object is otherwise unrelated to the object. Just like an aggregation, the associated object can belong to multiple objects simultaneously, and isn’t managed by those objects. However, unlike an aggregation, where the relationship is always unidirectional, in an association, the relationship may be unidirectional or bidirectional (where the two objects are aware of each other).
The relationship between doctors and patients is a great example of an association. The doctor clearly has a relationship with his patients, but conceptually it’s not a part/whole (object composition) relationship. A doctor can see many patients in a day, and a patient can see many doctors (perhaps they want a second opinion, or they are visiting different types of doctors). Neither of the object’s lifespans are tied to the other.
We can say that association models as “uses-a” relationship. The doctor “uses” the patient (to earn income). The patient uses the doctor (for whatever health purposes they need).
Implementing associations
Because associations are a broad type of relationship, they can be implemented in many different ways. However, most often, associations are implemented using pointers, where the object points at the associated object.
In this example, we’ll implement a bi-directional Doctor/Patient relationship, since it makes sense for the Doctors to know who their Patients are, and vice-versa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
#include <cstdint> #include <functional> // reference_wrapper #include <iostream> #include <string> #include <vector> // Since Doctor and Patient have a circular dependency, we're going to forward declare Patient class Patient; class Doctor { private: std::string m_name{}; std::vector<std::reference_wrapper<const Patient>> m_patient{}; public: Doctor(const std::string& name) : m_name{ name } { } void addPatient(Patient& patient); // We'll implement this function below Patient since we need Patient to be defined at that point friend std::ostream& operator<<(std::ostream &out, const Doctor &doctor); const std::string& getName() const { return m_name; } }; class Patient { private: std::string m_name{}; std::vector<std::reference_wrapper<const Doctor>> m_doctor{}; // so that we can use it here // We're going to make addDoctor private because we don't want the public to use it. // They should use Doctor::addPatient() instead, which is publicly exposed void addDoctor(const Doctor& doctor) { m_doctor.push_back(doctor); } public: Patient(const std::string& name) : m_name{ name } { } // We'll implement this function below Doctor since we need Doctor to be defined at that point friend std::ostream& operator<<(std::ostream &out, const Patient &patient); const std::string& getName() const { return m_name; } // We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor() friend void Doctor::addPatient(Patient& patient); }; void Doctor::addPatient(Patient& patient) { // Our doctor will add this patient m_patient.push_back(patient); // and the patient will also add this doctor patient.addDoctor(*this); } std::ostream& operator<<(std::ostream &out, const Doctor &doctor) { if (doctor.m_patient.empty()) { out << doctor.m_name << " has no patients right now"; return out; } out << doctor.m_name << " is seeing patients: "; for (const auto& patient : doctor.m_patient) out << patient.get().getName() << ' '; return out; } std::ostream& operator<<(std::ostream &out, const Patient &patient) { if (patient.m_doctor.empty()) { out << patient.getName() << " has no doctors right now"; return out; } out << patient.m_name << " is seeing doctors: "; for (const auto& doctor : patient.m_doctor) out << doctor.get().getName() << ' '; return out; } int main() { // Create a Patient outside the scope of the Doctor Patient dave{ "Dave" }; Patient frank{ "Frank" }; Patient betsy{ "Betsy" }; Doctor james{ "James" }; Doctor scott{ "Scott" }; james.addPatient(dave); scott.addPatient(dave); scott.addPatient(betsy); std::cout << james << '\n'; std::cout << scott << '\n'; std::cout << dave << '\n'; std::cout << frank << '\n'; std::cout << betsy << '\n'; return 0; } |
This prints:
James is seeing patients: Dave Scott is seeing patients: Dave Betsy Dave is seeing doctors: James Scott Frank has no doctors right now Betsy is seeing doctors: Scott
In general, you should avoid bidirectional associations if a unidirectional one will do, as they add complexity and tend to be harder to write without making errors.
Reflexive association
Sometimes objects may have a relationship with other objects of the same type. This is called a reflexive association. A good example of a reflexive association is the relationship between a university course and its prerequisites (which are also university courses).
Consider the simplified case where a Course can only have one prerequisite. We can do something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <string> class Course { private: std::string m_name; const Course *m_prerequisite; public: Course(const std::string &name, const Course *prerequisite = nullptr): m_name{ name }, m_prerequisite{ prerequisite } { } }; |
This can lead to a chain of associations (a course has a prerequisite, which has a prerequisite, etc…)
Associations can be indirect
In all of the previous cases, we’ve used either pointers or references to directly link objects together. However, in an association, this is not strictly required. Any kind of data that allows you to link two objects together suffices. In the following example, we show how a Driver class can have a unidirectional association with a Car without actually including a Car pointer or reference member:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#include <iostream> #include <string> class Car { private: std::string m_name; int m_id; public: Car(const std::string& name, int id) : m_name{ name }, m_id{ id } { } const std::string& getName() const { return m_name; } int getId() const { return m_id; } }; // Our CarLot is essentially just a static array of Cars and a lookup function to retrieve them. // Because it's static, we don't need to allocate an object of type CarLot to use it class CarLot { private: static Car s_carLot[4]; public: CarLot() = delete; // Ensure we don't try to create a CarLot static Car* getCar(int id) { for (int count{ 0 }; count < 4; ++count) { if (s_carLot[count].getId() == id) { return &(s_carLot[count]); } } return nullptr; } }; Car CarLot::s_carLot[4]{ { "Prius", 4 }, { "Corolla", 17 }, { "Accord", 84 }, { "Matrix", 62 } }; class Driver { private: std::string m_name; int m_carId; // we're associated with the Car by ID rather than pointer public: Driver(const std::string& name, int carId) : m_name{ name }, m_carId{ carId } { } const std::string& getName() const { return m_name; } int getCarId() const { return m_carId; } }; int main() { Driver d{ "Franz", 17 }; // Franz is driving the car with ID 17 Car *car{ CarLot::getCar(d.getCarId()) }; // Get that car from the car lot if (car) std::cout << d.getName() << " is driving a " << car->getName() << '\n'; else std::cout << d.getName() << " couldn't find his car\n"; return 0; } |
In the above example, we have a CarLot holding our cars. The Driver, who needs a car, doesn’t have a pointer to his Car -- instead, he has the ID of the car, which we can use to get the Car from the CarLot when we need it.
In this particular example, doing things this way is kind of silly, since getting the Car out of the CarLot requires an inefficient lookup (a pointer connecting the two is much faster). However, there are advantages to referencing things by a unique ID instead of a pointer. For example, you can reference things that are not currently in memory (maybe they’re in a file, or in a database, and can be loaded on demand). Also, pointers can take 4 or 8 bytes -- if space is at a premium and the number of unique objects is fairly low, referencing them by an 8-bit or 16-bit integer can save lots of memory.
Composition vs aggregation vs association summary
Here’s a summary table to help you remember the difference between composition, aggregation, and association:
Property | Composition | Aggregation | Association |
---|---|---|---|
Relationship type | Whole/part | Whole/part | Otherwise unrelated |
Members can belong to multiple classes | No | Yes | Yes |
Members existence managed by class | Yes | No | No |
Directionality | Unidirectional | Unidirectional | Unidirectional or bidirectional |
Relationship verb | Part-of | Has-a | Uses-a |
![]() |
![]() |
![]() |
Hi there!
I ask this question here because I've tried this with the last example of an indirect association: if I try to delete the memory pointed by the car pointer, then the program won't terminare succesfully.
I guess it is because I am trying to delete a memory location with static durage, but I'm not sure, let me know if I miss something or if I have to review some past lesson ^^
static storage duration* lol I mixed them up xD
Lol I'm sorry that I clutter up the comment section but I can't modify my comment :3
I've figured out that this happens because variable with static storage duration aren't allocated on the heap while delete only frees (sorry if I'm making a lot of english errors xD) memory allocated on the heap, I'm right?
You can only `delete` objects that you created with `new`.
Hello,
I was wondering if the following code makes sense to remove a car from car lot after the driver was given the car.
In the Reflexive association example, shouldn't the relation between prerequisites and university course be composition? When the university no longer exists, there hasn't to be any courses.
Is Linked List data structure an example of Reflexive association?
Hello,
1) I was wondering if I could define function member 'addPatient' in the way below so that we don't have to define 'addDoctor' anymore.
2) If there was a situation where we couldn't define operator<< at the end of the class, then would the function 'getPatientName' be fine?
Thank you.
Is there a common way to handle the circular dependencies with separate files? (I've had this same issue in 9.10 — Overloading typecasts with Cents/Dollars where I tried to make them castable to eachother).
I implemented the Doctor/Patient Classes keeping each class in separate files and failed to get it to compile. The problem I came up against is that the friend function
gives the error: "incomplete type is not allowed". Then if I include Doctor.h into Patient.h and Patient.h into Doctor.h it gives me the error: "use of undefined type Doctor" because I only have forward declarations
Things that I have tried are:
1.) Forward declaring both classes in each others files
2.) #including both classes in the .cpp files.
Doctor.h:
Patient.h
Doctor.cpp
Patient.cpp
I feel like the simple answer to my question is "you don't deal with circular dependencies," but that makes it seem like Association is not useful in some cases where it would make sense. Thank you for your time responding.
Sorry, it's too late to edit my comment. The solution I have is instead of making a friend function just friend the whole Doctor class
It results in an error because [Patient.h] doesn't yet know the definition of the [Doctor] class, you've only given it a declaration. Thus, it doesn't know whether there's a [addPatient()] function within the [Doctor] class.
Here association is implemented using vectors. However a special note should be given about the fact that list should be used instead of vector. I made a school time tabling program in c++ and the classes 1)classroom 2)teachers and 3)subjects were all in association with each other however since the objects of these classes were stores in a vector, all pointers would become invalid once the vector capacity was exceeded. This seems like the right place to mention iterator invalidity.
Hi everyone,
I've tried to type the example of Doctor and Patient in my IDE, but I could not compile the code. The compiler error message is "void Patient::addDoctor(const Doctor&) is private". But when I changed this method into public, I got SIGSEGV error.
Any help or explanation is highly appreciated.
Many thanks,
Tien
You must've made a mistake somewhere. Carefully compare the lesson's code with what you typed.
Thanks for your kind reply. Indeed, I forgot to type line 56.
I was in templates chapter and looked back at aggregation chapter to refer something and boom! its changed.
I thought that "Create a Patient outside the scope of the Doctor" means you should dynamically allocate Patient. But the updated code in the "chapter 10.3" suggest that dynamic allocation is not needed. So, what does "Create a Patient outside the scope of the Doctor" exactly mean ?
Could we allocate patient like this in the 1st example?
Dynamic allocation is not needed and shouldn't be used here. If we don't want an object to be non-existent (ie. be a nullptr), references should be used instead. On top of that, manual dynamic allocations are prone to memory leaks.
I updated this lesson to use `std::reference_wrapper` just like the previous lesson. Thanks for pointing out the inconsistency!
In last example line 63,
why are we creating the object car as a pointer type, but not Driver d in line 61 ? Why are we creating a pointer object without having a pointer member variable ?
`CarLot::getCar` potentially returns a `nullptr` (If there is no car with the given id). If we want to store the return value of `CarLot::getCar`, we have to use a pointer.
The driver on the other hand is created by us. We're certain it exist.
Hi,I don't understand the meaning "The associated object (member) is otherwise unrelated to the object (class)".
Dose the meaning of "otherwise unrelated to the object" be the same with "unrelated to the object"?
Thanks for replying
It means "Apart from being associated with the object, it's not related to the object in any other way".
Hi nascardriver! You have a typo near the bottom of this lesson (10.4) where you say "... referencing them by an 8-bit or 16-bit integer can save lots of memory." I believe you meant to say "... referencing them by a 32-bit or 64-bit integer can save lots of memory.".
BTW, wonderful content, this is helping me get back up to speed on C++ after several years of focusing solely on C#.
No, it's correct as written. If pointers took 4 or 8 bytes, there would be no space savings by replacing that with a 4 or 8 byte index. The space savings comes in if we can use an index that takes less memory (e.g. 8 or 16 bytes).
in Doctor-Patient example (line 86), we can just use pat.m_name instead of pat.getName() right?
And here's my full code if the patient is the one who add the doctors, not otherwise
You could, but you shouldn't. If a class offers you an access function, you should use it unless you're worried about performance loss of the function call.
The class might use the access function for more than just accessing the member, eg. counting calls or returning a modified member.
oh okay, thanks!
I have a slightly modified version of Doctor-Patient example in which I have tried to change the Patient::addDoctor function such that the Patient can also modify the doctor object and update its patient list. But when I run the code, it gives me Segmentation fault: 11
If I comment out line 96, the code runs just fine. Could you please help me find what's causing the Segmentation fault?
`addDoctor` and `addPatient` are calling each other. This causes an infinite loop and a segmentation fault, because your call stack is full.
Wanted to be pedantic and make the parameter const in
but as it turns out you cant push_back a pointer to const in a vector of regular pointers. Makes sense, I guess.
Hi @Alex. I had a hunch that rearranging the two classes will enable you to friend only void Doctor::addPatient(Patient *pat) instead of the whole class Doctor. Here is my adjusted code:
Now my question is, is there any benefit doing things the one way or another? I would guess that my adjusted code is better, since you expose less of the private innards of class Patient to the outside via Doctor's public interface. It is also expressing intent more clearly and directly in code. Am I reasoning correctly?
For this particular example, your solution is better because it exposes less. If Patient and Doctor were more intertwined, having to friend each individual function might be burdensome -- but that's not the case here. I've updated the lesson incorporating your adjustment. Thanks!
Hello.
I tried to code the Doctor-Patient program using header files but it doesn't compile.
My code:
1) Doctor.h
2) Patient.h
Note: Here I made the addDoctor(Doctor *) function public because I don't know how to friend a Patient class' function in Doctor class.
3) Doctor.cpp
4) Patient.cpp
5) association.cpp
And the output I get after building the project:
Is it possible to do resolve the circular dependency between Doctor and Patient this way? If not then can you please explain why?
And is there any other alternative than the forward declaration option because it just feels unnatural to code it like that.
Thank you!
Hi Lucieon!
> Is it possible to do resolve the circular dependency between Doctor and Patient this way? If not then can you please explain why?
No, now you have circular includes.
> is there any other alternative than the forward declaration
No, you need forward declarations. Use as few includes as possible in header files.
You need to forward declare class Doctor; in Patient.h and class Patient; in Doctor.h and then #include both Patient.h and Doctor.h in both of Patient.cpp and Doctor.cpp. You should also declare void Doctor::addPatient(Patient *pat) as a friend of class Patient. Then your code should be equivalent to the example given in the text.
Hello! I wonder why we don't also add this line of code to the function addDoctor:
doc->addPatient(this)
since if a patient sees a doctor then the doctor has a new patient!
Nope, @addPatient would call @addDoctor, resulting in an infinite recursive loop.
The interface for these classes is set up in such a way that users of the code would call Patient::addDoctor, which handles the bidirectional adding. Doctor::addPatient is just a private helper function to assist with that.
Hi Alex,
Taking the example of university course and its prerequisite course, will the relationship between them still be called reflexive if there are multiple prerequisites to a single course? A trivial example would be physics, for which one may have 2 prerequisites maths and english.
Yes, in your example the physics course would have two reflexive associations.
So linked list is implemented using reflexive association relation...
Hi Alex,
I see that for functions taking in std::string as an argument, you sometimes pass by value and at other times pass by reference (even though it isn't being modified.) I'd really like to know what the best practice is.
If the modification is to be seen in the calling function, then yes, pass by reference is the way to go. If not, what's the best practice? Pass by value, or a (const) reference?
Thanks,
Nitin
Hi Nitin!
If the argument isn't being modified, pass by const reference, unless it's a built-in type (int, float, double, ...).
If the argument is being modified, pass by pointer if the caller is supposed to see the change, otherwise pass by value.
Only pass by non-const reference, when passing by pointer is not possible or too complicated (eg. operators).
Thanks nascardriver! Appreciate the clarity.
hi nascardriver,
Can you please elaborate what does "caller is supposed to see the change" means?
Thank you
"caller is supposed to see that the variable is being modified" is what I meant to say.
Hi Alex! I wanted to implement the Patient/Doctor classes with each class' member functions defined in separate cpp files. Is the following code the best way to do this:
patient.hpp:
patient.cpp:
doctor.hpp:
doctor.cpp:
Hi David!
* @Patient::getName and @Doctor::getName should be defined in source files and should return const references.
* @Patient::Patient and @Doctor::Doctor should be defined in source files.
* @Patient doesn't ever edit it's doctors, you could store const doctors.
* @patient.hpp:20, @patient.cpp:10,18, @doctor.hpp:18, @doctor.cpp:12,20: Use uniform initizalization.
* The name printing loops could be for-each loops, because you don't need to index.
* @std::vector::size returns a @std::size_t, use that instead of unsigned long.
* Both operator<<s: Use @std::vector::empty.
Hi nascardriver! Thanks for all the great feedback. One question - if I define @Patient::Patient and @Doctor::Doctor in source files, how will the definitions "see" one another? Would I need to #include the source files?
Definition != Declaration
patient.hpp
patient.cpp