Source: LearnCpp.com by Alex

Object Relationships

There are many different kinds of relationships two objects may have in real-life, and we use specific “relation type” words to describe these relationships. For example:

  • a square “is-a” shape.
  • A car “has-a” steering wheel.
  • A computer programmer “uses-a” keyboard.
  • A flower “depends-on” a bee for pollination.
  • A student is a “member-of” a class.
  • Your brain exists as “part-of” you (at least, we can reasonably assume so if you’ve gotten this far).

Composition (part-of)

This process of building complex objects from simpler ones is called object composition.

In C++, you’ve already seen that structs and classes can have data members of various types (such as fundamental types or other classes). When we build classes with data members, we’re essentially constructing a complex object from simpler parts, which is object composition. For this reason, structs and classes are sometimes referred to as composite types.

Types of object composition

There are two basic subtypes of object composition: composition and aggregation. We’ll examine composition in this lesson, and aggregation in the next.

A note on terminology: the term “composition” is often used to refer to both composition and aggregation, not just to the composition subtype. In this tutorial, we’ll use the term "object composition" when we’re referring to both, and “composition” when we’re referring specifically to the composition subtype.

To qualify as a composition, an object and a part must have the following relationship:

  • The part (member) is part of the object (class)
  • The part (member) can only belong to one object (class) at a time
  • The part (member) has its existence managed by the object (class)
  • The part (member) does not know about the existence of the object (class)

As for the fourth point, the part doesn’t know about the existence of the whole. Your heart operates blissfully unaware that it is part of a larger structure. We call this a unidirectional relationship, because the body knows about the heart, but not the other way around.

While object composition models has-a type relationships (a body has-a heart, a fraction has-a denominator), we can be more precise and say that composition models part-of relationships (a heart is part-of a body, a numerator is part of a fraction). Composition is often used to model physical relationships, where one object is physically contained inside another.

In general, if you can design a class using composition, you should design a class using composition. Classes designed using composition are straightforward, flexible, and robust (in that they clean up after themselves nicely).

Variants on the composition theme

Although most compositions directly create their parts when the composition is created and directly destroy their parts when the composition is destroyed, there are some variations of composition that bend these rules a bit.

For example:

  • A composition may defer creation of some parts until they are needed. For example, a string class may not create a dynamic array of characters until the user assigns the string some data to hold.
  • A composition may opt to use a part that has been given to it as input rather than create the part itself.
  • A composition may delegate destruction of its parts to some other object (e.g. to a garbage collection routine).

Composition and subclasses

One question that new programmers often ask when it comes to object composition is, “When should I use a subclass instead of direct implementation of a feature?”. For example, instead of using the Point2D class to implement the Creature’s location, we could have instead just added 2 integers to the Creature class and written code in the Creature class to handle the positioning.

A good rule of thumb is that each class should be built to accomplish a single task. That task should either be the storage and manipulation of some kind of data (e.g. Point2D, std::string), OR the coordination of subclasses (e.g. Creature). Ideally not both.

In this case of our example, it makes sense that Creature shouldn't have to worry about how Points are implemented, or how the name is being stored. Creature’s job isn’t to know those intimate details. Creature’s job is to worry about how to coordinate the data flow and ensure that each of the subclasses knows what it is supposed to do. It’s up to the individual subclasses to worry about how they will do it.

Aggregation (has-a)

To qualify as an aggregation, a whole object and its parts must have the following relationship:

  • The part (member) is part of the object (class)
  • The part (member) can belong to more than one object (class) at a time
  • The part (member) does not have its existence managed by the object (class)
  • The part (member) does not know about the existence of the object (class)

Like a composition, an aggregation is still a part-whole relationship, where the parts are contained within the whole, and it is a unidirectional relationship. However, unlike a composition, parts can belong to more than one object at a time, and the whole object is not responsible for the existence and lifespan of the parts. When an aggregation is created, the aggregation is not responsible for creating the parts. When an aggregation is destroyed, the aggregation is not responsible for destroying the parts.

For example, consider the relationship between a person and their home address. In this example, for simplicity, we’ll say every person has an address. However, that address can belong to more than one person at a time: for example, to both you and your roommate or significant other. However, that address isn’t managed by the person – the address probably existed before the person got there, and will exist after the person is gone. Additionally, a person knows what address they live at, but the addresses don’t know what people live there. Therefore, this is an aggregate relationship.

We can say that aggregation models “has-a” relationships (a department has teachers, the car has an engine).

Similar to a composition, the parts of an aggregation can be singular or multiplicative.

Implementing aggregations

Because aggregations are similar to compositions in that they are both part-whole relationships, they are implemented almost identically, and the difference between them is mostly semantic. In a composition, we typically add our parts to the composition using normal member variables (or pointers where the allocation and deallocation process is handled by the composition class).

In an aggregation, we also add parts as member variables. However, these member variables are typically either references or pointers that are used to point at objects that have been created outside the scope of the class. Consequently, an aggregation usually either takes the objects it is going to point to as constructor parameters, or it begins empty and the subobjects are added later via access functions or operators.

Pick the right relationship for what you’re modeling

For example, if you’re writing a body shop simulator, you may want to implement a car and engine as an aggregation, so the engine can be removed and put on a shelf somewhere for later. However, if you’re writing a racing simulation, you may want to implement a car and an engine as a composition, since the engine will never exist outside of the car in that context.

Rule: Implement the simplest relationship type that meets the needs of your program, not what seems right in real-life.

Summarizing composition and aggregation

Compositions:

  • Typically use normal member variables
  • Can use pointer members if the class handles object allocation/deallocation itself
  • Responsible for creation/destruction of parts

Aggregations:

  • Typically use pointer or reference members that point to or reference objects that live outside the scope of the aggregate class
  • Not responsible for creating/destroying parts

While aggregations can be extremely useful, they are also potentially more dangerous. Because aggregations do not handle deallocation of their parts, that is left up to an external party to do so. If the external party no longer has a pointer or reference to the abandoned parts, or if it simply forgets to do the cleanup (assuming the class will handle that), then memory will be leaked.

For this reason, compositions should be favored over aggregations.

A few warnings/errata

For a variety of historical and contextual reasons, unlike a composition, the definition of an aggregation is not precise – so you may see other reference material defines it differently from the way we do. That’s fine, just be aware.

One final note: In the lesson Structs, we defined aggregate data types (such as structs and classes) as data types that groups multiple variables together. You may also run across the term aggregate class in your C++ journeys, which is defined as a struct or class that has no provided constructors, destructors, or overloaded assignment, has all public members, and does not use inheritance – essentially a plain-old-data struct. Despite the similarities in naming, aggregates and aggregation are different and should not be confused.

Association (uses-a)

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.

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)

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).

many-to-many

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.

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
class Doctor;
class Patient {
private:
std::string m_name;
std::vector<Doctor *> m_doctor; // so that we can use it here

void addDoctor(Doctor *doc) {
m_doctor.push_back(doc);
}
};
class Doctor {
private:
std::string m_name;
std::vector<Patient *> m_patient;
public:
void addPatient(Patient *pat) {
m_patient.push_back(pat);
pat->addDoctor(this);
}
};

int main() {
// Create a Patient outside the scope of the Doctor
Patient *p1 = new Patient("Dave");
Patient *p2 = new Patient("Frank");
Patient *p3 = new Patient("Betsy");

Doctor *d1 = new Doctor("James");
Doctor *d2 = new Doctor("Scott");

d1->addPatient(p1);
d2->addPatient(p1);
d2->addPatient(p3);

delete p1;
delete p2;
delete p3;
delete d1;
delete d2;

return 0;
}

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).

1
2
3
4
5
6
7
8
9
10
class Course {
private:
std::string m_name;
Course *m_prerequisite;

public:
Course(std::string &name, Course *prerequisite=nullptr):
m_name(name), m_prerequisite(prerequisite) {
}
};

Associations can be indirect

In all of the above cases, we’ve used a pointer 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 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
class Car {
private:
std::string m_name;
int m_id;
public:
Car(std::string name, int id)
: m_name(name), m_id(id) {
// foo
}
};
// 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 allocate a CarLot
};

class Driver {
private:
std::string m_name;
int m_carId; // we're associated with the Car by ID rather than pointer
public:
std::string getName() { return m_name; }
int getCarId() { return m_carId; }
};
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
Implementation Normal members Ref or Pointers Pointers
Example Person-Heart Person-Address Doctor-Patient

Dependencies

In casual conversation, we use the term dependency to indicate that an object is reliant upon another object for a given task. For example, if you break your foot, you are dependent on crutches to get around (but not otherwise). Flowers are dependent upon bees to pollinate them, in order to grow fruit or propagate (but not otherwise).

A dependency occurs when one object invokes another object's functionality in order to accomplish some specific task. This is a weaker relationship than an association, but still, any change to object being depended upon may break functionality in the (dependent) caller. A dependency is always a unidirectional relationship.

A good example of a dependency that you’ve already seen many times is std::cout (of type std::ostream). Our classes that use std::cout use it in order to accomplish the task of printing something to the console, but not otherwise.

Dependencies vs Association in C++

In C++, associations are a relationship between two classes at the class level. That is, one class keeps a direct or indirect “link” to the associated class as a member. For example, a Doctor class has an array of pointers to its Patients as a member. You can always ask the Doctor who its patients are. The Driver class holds the id of the Car the driver object owns as an integer member. The Driver always knows what Car is associated with it.

Dependencies typically are not represented at the class level – that is, the object being depended on is not linked as a member. Rather, the object being depended on is typically instantiated as needed (like opening a file to write data to), or passed into a function as a parameter (like std::ostream in the overloaded operator<< above).

Container classes

Container classes implement a member-of relationship. For example, elements of an array are members-of (belong to) the array. Note that we’re using “member-of” in the conventional sense, not the C++ class member sense.

Types of containers

Container classes generally come in two different varieties. Value containers are compositions that store copies of the objects that they are holding (and thus are responsible for creating and destroying those copies). Reference containers are aggregations that store pointers or references to other objects (and thus are not responsible for creation or destruction of those objects).

  • Value containers (compositions, part-of)
  • Reference containers (aggregations, has-a)

Write an array container class

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
#ifndef INTARRAY_H
#define INTARRAY_H

#include <cassert> // for assert()

class IntArray {
private:
int m_length;
int *m_data;
public:
IntArray() : m_length(0), m_data(nullptr) {
// foo
}
IntArray(int length) : m_length(length) {
assert(length >= 0);

if (length > 0)
m_data = new int[length];
else
m_data = nullptr;
}
~IntArray() {
delete[] m_data;
}

void erase() {
delete[] m_data; // be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}

// overload [] - return ref. support assignment!
int& operator[] (int index) {
assert(index >= 0 && index < m_length);
return m_data[index];
}

int getLength() { return m_length; }

void reallocate(int newLength) {
erase();

if (newLength <= 0)
return;
m_data = new int[newLength];
m_length = newLength;
}

void resize(int newLength) {
if (newLength == m_length)
return;
if (newLength <= 0) {
erase();
return;
}
int *data = new int[newLength];
if (m_length > 0) {
int elementsToCopy = (newLength > m_length) ? m_length : newLength;
// now copy
for (int index=0; index < elementsToCopy; ++index)
data[index] = m_data[index];
}

delete[] m_data;
m_data = data;
m_length = newLength;
}

void insertBefore(int value, int index) {
assert(index >= 0 && index <= m_length);

int *data = new int[m_length + 1];
// copy all of the elements up to the index
for (int before=0; before < index; ++before)
data[before] = m_data[before];
// insert new value
data[index] = value;

for (int after=index; after < m_length; ++after)
data[after+1] = m_data[after];

delete[] m_data;
m_data = data;
++m_length;
}

void remove(int index) {
assert(index >= 0 && index < m_length);

if (m_length == 1) {
erase();
return;
}

int *data = new int[m_length - 1];
for (int before=0; before < index; ++before)
data[before] = m_data[before];
for (int after=index+1; after < m_length; ++after)
data[after-1] = m_data[after];

delete[] m_data;
m_data = data;
--m_length;
}

void insertAtBeginning(int value) {
insertBefore(value, 0);
}

void insertAtEnd(int value) {
insertBefore(value, m_length);
}
};

#endif

std::initializer_list

What happens if we try to use an initializer list with the container class above?

1
IntArray array { 5, 4, 3, 2, 1 }; // this line won't compile

This code won’t compile, because the IntArray class doesn’t have a constructor that knows what to do with an initializer list.

Prior to C++11, list initialization could only be used with static or dynamic arrays. However, as of C++11, we now have a solution to this problem.

When a C++11 compiler sees an initializer list, it automatically converts it into an object of type std::initializer_list. Therefore, if we create a constructor that takes a std::initializer_list parameter, we can create objects using the initializer list as an input.

std::initializer_list has a (misnamed) size() function which returns the number of elements in the list. This is useful when we need to know the length of the list passed in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <initializer_list>
class IntArray {
public:
IntArray(const std::initializer_list<int> &list)
: IntArray(list.size()) { // use delegating constructor to set up initial array
int count = 0;
for (auto &element : list) {
m_data[count] = element;
++count;
}
}
};
int main() {
IntArray array { 5, 4, 3, 2, 1 }; // initializer list

return 0;
}

One caveat: Initializer lists will always favor a matching initializer_list constructor over other potentially matching constructors. Thus, this variable definition:

1
IntArray array {5};  // would match to IntArray(initializer_list)

Class assignment using std::initializer_list

You can also use std::initializer_list to assign new values to a class by overloading the assignment operator to take a std::initializer_list parameter. This works analogously to the above. We’ll show an example of how to do this in the quiz solution below.

Note that if you implement a constructor that takes a std::initializer_list, you should ensure you do at least one of the following:

  1. Provide an overloaded list assignment operator
  2. Provide a proper deep-copying copy assignment operator
  3. Make the constructor explicit, so it can’t be used for implicit conversions

Here’s why: consider the above class (which doesn’t have an overloaded list assignment or a copy assignment), along with following statement:

1
array = { 1, 3, 5, 7, 9, 11 };  // overwrite the elements of array with the elements from the list

First, the compiler will note that an assignment function taking a std::initializer_list doesn’t exist. Next it will look for other assignment functions it could use, and discover the implicitly provided copy assignment operator. However, this function can only be used if it can convert the initializer list into an IntArray. Because the constructor that takes a std::initializer_list isn’t marked as explicit, the compiler will use the list constructor to convert the initializer list into a temporary IntArray. Then it will call the implicit assignment operator, which will shallow copy the temporary IntArray into our array object.

At this point, both the temporary IntArray’s m_data and array->m_data point to the same address (due to the shallow copy). You can already see where this is going.

Rule: If you provide list construction, it’s a good idea to provide list assignment as well.

Inheritance

Making BaseballPlayer a derived class

1
2
3
4
5
6
7
8
9
10
class BaseballPlayer : public Person {
public:
double m_battingAverage;
int m_homeRuns;

BaseballPlayer(double battingAverage=0.0, inthomeRuns=0)
: m_battingAverage(battingAverage), m_homeRuns(homeRuns) {
// foo
}
};

Order of construction of derived classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
int m_id;

Base(int id=0)
: m_id(id) {
std::cout << "Base" << "\n";
}

int getId() const { return m_id; }
};

class Derived : public Base {
public:
double m_cost;

Derived(double cost=0.0)
: m_cost(cost) {
std::cout << "Derived" << "\n";
}
double getCost() const { return m_cost; }
};

Now let’s take a look at what happens when we instantiate a derived class:

1
2
3
4
5
6
7
8
int main() {
std::cout << "Instantiating Base" << "\n";
Base base;
std::cout << "Instantiating Derived" << "\n";
Derived derived;

return 0;
}

Behind the scenes (in terms of Derived class), things happen slightly different. As mentioned above, Derived is really two parts: a Base part, and a Derived part. When C++ constructs derived objects, it does so in phases.

  • First, the most-base class (at the top of the inheritance tree) is constructed first.
  • Then each child class is constructed in order, until the most-child class (at the bottom of the inheritance tree) is constructed last.

So when we instantiate an instance of Derived, first the Base portion of Derived is constructed (using the Base default constructor). Once the Base portion is finished, the Derived portion is constructed (using the Derived default constructor). At this point, there are no more derived classes, so we are done.

Output:

1
2
3
4
5
Instantiating Base
Base
Instantiating Derived
Base
Derived

Remember that C++ always constructs the “first” or “most base” class first. It then walks through the inheritance tree in order and constructs each successive derived class.

Constructors and initialization of derived classes

Previous Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
int m_id;

Base(int id=0)
: m_id(id) {
std::cout << "Base" << "\n";
}

int getId() const { return m_id; }
};

class Derived: public Base {
public:
double m_cost;

Derived(double cost=0.0)
: m_cost(cost) {
std::cout << "Derived" << "\n";
}
double getCost() const { return m_cost; }
};

Consider:

1
2
3
4
int main() {
Base base(5); // use Base(int) constructor
return 0;
}

Here’s what actually happens when base is instantiated:

  1. Memory for base is set aside
  2. The appropriate Base constructor is called
  3. The initialization list initializes variables (go first)
  4. The body of the constructor executes
  5. Control is returned to the caller
1
2
3
4
int main() {
Derived derived(1.3); // use Derived(double) constructor
return 0;
}

Here’s what actually happens when derived is instantiated:

  1. Memory for derived is set aside (enough for both the Base and Derived portions)
  2. The appropriate Derived constructor is called
  3. The Base object is constructed first using the appropriate Base constructor. If no base constructor is specified, the default constructor will be used.
  4. The initialization list initializes variables (Derived)
  5. The body of the constructor executes
  6. Control is returned to the caller

One of the current shortcomings of our Derived class as written is that there is no way to initialize m_id when we create a Derived object. What if we want to set both m_cost (from the Derived portion of the object) and m_id (from the Base portion of the object) when we create a Derived object?

New programmers often attempt to solve this problem as follows:

1
2
3
4
5
6
7
8
9
10
class Derived: public Base {
public:
double m_cost;
Derived(double cost=0.0, int id=0) // does not work
: m_cost(cost), m_id(id) {
// foo
}

double getCost() const { return m_cost; }
};

This is a good attempt, and is almost the right idea. We definitely need to add another parameter to our constructor, otherwise C++ will have no way of knowing what value we want to initialize m_id to.

However, C++ prevents classes from initializing inherited member variables in the initialization list of a constructor. In other words, the value of a variable can only be set in an initialization list of a constructor belonging to the same class as the variable.

Why does C++ do this? The answer has to do with const and reference variables. Consider what would happen if m_id were const. Because const variables must be initialized with a value at the time of creation, the base class constructor must set its value when the variable is created. However, when the base class constructor finishes, the derived class constructors initialization lists are then executed. Each derived class would then have the opportunity to initialize that variable, potentially changing its value! By restricting the initialization of variables to the constructor of the class those variables belong to, C++ ensures that all variables are initialized only once.

The end result is that the above example does not work because m_id was inherited from Base, and only non-inherited variables can be changed in the initialization list.

However, inherited variables can still have their values changed in the body of the constructor using an assignment. Consequently, new programmers often also try this:

1
2
3
4
5
6
7
8
9
class Dervied : public Base {
public:
double m_cost;

Derived(double cost=0.0, int id=0)
: m_cost(cost) {
m_id = id; // you just cannot do it in the initialization list
}
};

While this actually works in this case, it wouldn’t work if m_id were a const or a reference (because const values and references have to be initialized in the initialization list of the constructor). It’s also inefficient because m_id gets assigned a value twice: once in the initialization list of the Base class constructor, and then again in the body of the Derived class constructor. And finally, what if the Base class needed access to this value during construction? It has no way to access it, since it’s not set until the Derived constructor is executed (which pretty much happens last).

In all of the examples so far, when we instantiate a Derived class object, the Base class portion has been created using the default Base constructor. Why does it always use the default Base constructor? Because we never told it to do otherwise!

Fortunately, C++ gives us the ability to explicitly choose which Base class constructor will be called! To do this, simply add a call to the base class constructor in the initialization list of the derived class:

1
2
3
4
5
6
7
8
class Derived : public Base {
public: // with the feature below, we can set private
double m_cost;
Derived(double cost=0.0, int id=0)
: Base(id), m_cost(cost) { // call Base(int) constructor with value id!
// foo
}
};

Note: it doesn’t matter where in the Derived constructor initialization list the Base constructor is called – it will always execute first.

It is worth mentioning that constructors can only call constructors from their immediate parent/base class. Consequently, the C constructor could not call or pass parameters to the A constructor directly. The C constructor can only call the B constructor (which has the responsibility of calling the A constructor).

Destructors

When a derived class is destroyed, each destructor is called in the reverse order of construction. In the above example, when C is destroyed, the C destructor is called first, then the B destructor, then the A destructor.

Inheritance and access specifiers

The protected access specifier

When dealing with inherited classes, things get a bit more complex.

C++ has a third access specifier that we have yet to talk about because it’s only useful in an inheritance context. The protected access specifier allows members, friends, and derived classes to access the member. However, protected members are not accessible from outside the class.

Different kinds of inheritance, and their impact on access

First, there are three different ways for classes to inherit from other classes: public, private, and protected.

1
2
3
4
5
6
7
8
9
10
11
// Inherit from Base publicly
class Pub: public Base {};

// Inherit from Base privately
class Pri: private Base {};

// Inherit from Base protectedly
class Pro: protected Base {};

// Defaults to private inheritance
class Def: Base {};

That gives us 9 combinations: 3 member access specifiers (public, private, and protected), and 3 inheritance types (public, private, and protected).

So what’s the difference between these? In a nutshell, when members are inherited, the access specifier for an inherited member may be changed (in the derived class only) depending on the type of inheritance used. Put another way, members that were public or protected in the base class may change access specifiers in the derived class.

Keep in mind the following rules as we step through the examples:

  • A class can always access its own (non-inherited) members.
  • The public (out of class) accesses the members of a class based on the access specifiers of the class it is accessing.
  • A class accesses inherited members based on the access specifier inherited from the parent class. This varies depending on the access specifier and type of inheritance used.

inheritance type –> member access specifiers –> whether it can be accessed

1. Public inheritance

Public inheritance is by far the most commonly used type of inheritance. In fact, very rarely will you see or use the other types of inheritance, so your primary focus should be on understanding this section. Fortunately, public inheritance is also the easiest to understand.

When you inherit a base class publicly, inherited public members stay public, and inherited protected members stay protected. Inherited private members, which were inaccessible because they were private in the base class, stay inaccessible. Stay the same!

Rule: Use public inheritance unless you have a specific reason to do otherwise.

2. Private inheritance

With private inheritance, all members from the base class are inherited as private. This means inherited private members stay private, and protected and public members become private.

Note: this does not affect the way that the derived class accesses members inherited from its parent! It only affects the code trying to access those members through the derived class. (???)

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
class Base {
public:
int m_public;
private:
int m_private;
protected:
int m_protected;
};

class Pri : private Base {
// Public inherited members become private (so m_public is treated as private)
// Private inherited members stay inaccessible (so m_private is inaccessible)
// Protected inherited members become private (so m_protected is treated as private)
public:
Pri() {
m_public = 1; // okay: m_public is private in Pri
m_private = 2; // not okay: stay private
m_protected = 3; // okay: m_protected is private in Pri
}
};

int main() {
Base base;
base.m_public = 1; // okay
base.m_private = 2; // not okay
base.m_protected = 3; // not okay

Pri pri; // all becomes private
pri.m_public = 1; // not okay
pri.m_private = 2; // not okay
pri.m_protected = 3; // not okay
}

Private inheritance can be useful when the derived class has no obvious relationship to the base class, but uses the base class for implementation internally. In such a case, we probably don’t want the public interface of the base class to be exposed through objects of the derived class (as it would be if we inherited publicly).

Note that in practice, private inheritance is rarely used.

3. Protected inheritance

Protected inheritance is the last method of inheritance. It is almost never used, except in very particular cases. With protected inheritance, the public and protected members become protected, and private members stay inaccessible.

  • Public -> Protected
  • Private -> Inaccessible (Public > Protected > Private)
  • Protected -> Protected

Summary

  • First, a class (and friends) can always access its own non-inherited members. The access specifiers only affect whether outsiders and derived classes can access those members.

  • Second, when derived classes inherit members, those members may change access specifiers in the derived class. This does not affect the derived classes’ own (non-inherited) members (which have their own access specifiers). It only affects whether outsiders and classes derived from the derived class can access those inherited members.

As a final note, although in the examples above, we’ve only shown examples using member variables, these access rules hold true for all members (e.g. member functions and types declared inside the class).

Adding new functionality to a derived class

You can inherit the base class functionality and then add new functionality, modify existing functionality, or hide functionality you don’t want.

There may be times when we have access to a base class but do not want to modify it. Consider the case where you have just purchased a library of code from a 3rd party vendor, but need some extra functionality. You could add to the original code, but this isn’t the best solution. What if the vendor sends you an update? Either your additions will be overwritten, or you’ll have to manually migrate them into the update, which is time-consuming and risky.

There may be times when it’s not even possible to modify the base class. Consider the code in the standard library. We aren’t able to modify the code that’s part of the standard library. But we are able to inherit from those classes, and then add our own functionality into our derived classes. The same goes for 3rd party libraries where you are provided with headers but the code comes precompiled.

In either case, the best answer is to derive your own class, and add the functionality you want to the derived class.

Calling inherited functions and overriding behavior

Note that when you redefine a function in the derived class, the derived function does not inherit the access specifier of the function with the same name in the base class. It uses whatever access specifier it is defined under in the derived class. Therefore, a function that is defined as private in the base class can be redefined as public in the derived class, or vice-versa!

To have a derived function call a base function of the same name, simply do a normal function call, but prefix the function with the scope qualifier. The following example redefines Derived::identify() so it first calls Base::identify() and then does its own additional stuff.

1
2
3
4
void identify() {
Base::identify(); // call Base::identify() first
std::cout << "I am a Derived\n"; // then identify ourselves
}

Because a Derived is-a Base, we can static_cast our Derived object into a Base, so that the appropriate version of operator<< that uses a Base is called.

Hiding inherited functionality

C++ gives us the ability to change an inherited member’s access specifier in the derived class. This is done by using a using declaration to identify the (scoped) base class member that is having its access changed in the derived class, under the new access specifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Base
protected:
void printValue() { std::cout << m_value; }

// Derived
public: // changing it to public
using Base::printValue; // note: no parenthesis here

int main() {
Derived derived(7);
derived.printValue(); // prints 7
return 0;
}

Two notes:

  • First, you can only change the access specifiers of base members the derived class would normally be able to access. Therefore, you can never change the access specifier of a base member from private to protected or public, because derived classes do not have access to private members of the base class.

  • Second, as of C++11, using declarations are the preferred way of changing access levels. However, you can also change access levels by using an access declaration. This works identically to the using declaration method, but omits the “using” keyword. This access declaration way of redefining access is now considered deprecated, but you will likely see older code using this pattern, so it’s worth knowing about.

Hiding functionality

For example, we can also make a public member variable private.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
int m_value;
};

class Derived : public Base {
private:
using Base::m_value; // making it private

public:
Derived(int value) {
m_value = value;
}
};

int main() {
Derived derived(7);
// The following won't work because m_value has been redefined as private
std::cout << derived.m_value;

return 0;
}

Alternatively, instead of inheriting Base’s members publicly and making m_value private by overriding its access specifier, we could have inherited Base privately, however, which would have caused all of Base’s member to be inherited privately in the first place.

You can also mark member functions as deleted in the derived class, which ensures they can’t be called at all through a derived object:

1
int getValue() = delete;

Multiple inheritance

1
2
3
4
5
6
7
8
9
class Teacher : public Person, public Employee {
private:
int m_teachesGrade;
public:
Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade)
: Person(name, age), Employee(employer, wage), m_teachesGrade(teachsGrade) {
// foo
}
};

Problems with multiple inheritance

While multiple inheritance seems like a simple extension of single inheritance, multiple inheritance introduces a lot of issues that can markedly increase the complexity of programs and make them a maintenance nightmare. Let’s take a look at some of these situations.

  • First, ambiguity can result when multiple base classes contain a function with the same name, and the complier will complain.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // class USBDevice
    // member getID()
    // class NetworkDevice
    // member getID()

    class WirelessAdapter : public USBDevice, public NetworkDevice {
    public:
    WirelessAdapter(long usbId, long networkId)
    : USBDevice(usbId), NetworkDevice(networkId) {
    // goo
    }
    }

    int main() {
    WirelessAdapter c54G(5442, 181742);
    std::cout << c54G.getID() << "\n"; // error
    std::cout << c54G.USBDevice::getID() << "\n"; // okay
    return 0;
    }

    While this workaround is pretty simple, you can see how things can get complex when your class inherits from four or six base classes, which inherit from other classes themselves. The potential for naming conflicts increases exponentially as you inherit more classes, and each of these naming conflicts needs to be resolved explicitly.

  • Second, and more serious is the diamond problem, which your author likes to call the “diamond of doom”. This occurs when a class multiply inherits from two classes which each inherit from a single base class. This leads to a diamond shaped inheritance pattern.

There are many issues that arise in this context, including whether Copier should have one or two copies of PoweredDevice, and how to resolve certain types of ambiguous references. While most of these issues can be addressed through explicit scoping, the maintenance overhead added to your classes in order to deal with the added complexity can cause development time to skyrocket. We’ll talk more about ways to resolve the diamond problem in the next lesson.

Is multiple inheritance more trouble than it’s worth?

As it turns out, most of the problems that can be solved using multiple inheritance can be solved using single inheritance as well. Many object-oriented languages (eg. Smalltalk, PHP) do not even support multiple inheritance. Many relatively modern languages such as Java and C# restrict classes to single inheritance of normal classes, but allow multiple inheritance of interface classes (which we will talk about later). The driving idea behind disallowing multiple inheritance in these languages is that it simply makes the language too complex, and ultimately causes more problems than it fixes.

Many authors and experienced programmers believe multiple inheritance in C++ should be avoided at all costs due to the many potential problems it brings. Your author does not agree with this approach, because there are times and situations when multiple inheritance is the best way to proceed. However, multiple inheritance should be used extremely judiciously.

As an interesting aside, you have already been using classes written using multiple inheritance without knowing it. For example, the iostream library objects std::cin and std::cout are both implemented using multiple inheritance!

Rule: Avoid multiple inheritance unless alternatives lead to more complexity.


Comment