Study Notes on LearnCpp (Part II - OOP, Overload Operators)


Source: LearnCpp.com by Alex

Basic Object-Oriented Programming

In C++, classes and structs are essentially the same. In fact, the following struct and class are effectively identical:

1
2
3
4
5
6
7
8
9
10
11
12
struct DateStruct {
int year;
int month;
int day;
};

class DateClass {
public:
int m_year;
int m_month;
int m_day;
}; // don't forget the semicolon!

Just like a struct declaration, a class declaration does not declare any memory. It only defines what the class looks like.

Using the m_ prefix for member variables helps distinguish member variables from function parameters or local variables inside member functions. This is useful for several reasons:

  • First, when we see an assignment to a variable with the “m_” prefix, we know that we are changing the state of the class.
  • Second, unlike function parameters or local variables, which are declared within the function, member variables are declared in the class definition. Consequently, if we want to know how a variable with the “m_” prefix is declared, we know that we should look in the class definition instead of within the function.

A note about structs in C++

Intro

In C, structs can only hold data, and do not have associated member functions. In C++, after designing classes (using the class keyword), Bjarne Stroustrup spent some amount of time considering whether structs (which were inherited from C) should be granted the ability to have member functions. Upon consideration, he determined that they should, in part to have a unified ruleset for both. So although we wrote the above programs using the class keyword, we could have used the struct keyword instead.

Many developers (including myself) feel this was the incorrect decision to be made, as it can lead to dangerous assumptions: For example, it’s fair to assume a class will clean up after itself (e.g. a class that allocates memory will deallocate it before being destroyed), but it’s not safe to assume a struct will. Consequently, we recommend using the struct keyword for data-only structures, and the class keyword for defining objects that require both data and functions to be bundled together.

Rule: Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.

Public vs private access specifiers

1
2
3
4
5
6
7
8
9
10
struct DateStruct { // members are public by default
int month; // public by default, can be accessed by anyone
int day; // public by default, can be accessed by anyone
int year; // public by default, can be accessed by anyone
};
class DateClass { // members are private by default
int m_month; // private by default, can only be accessed by other members
int m_day; // private by default, can only be accessed by other members
pint m_year; // private by default, can only be accessed by other members
};

Classes can (and almost always do) use multiple access specifiers to set the access levels of each of its members. There is no limit to the number of access specifiers you can use in a class.

In general, member variables are usually made private, and member functions are usually made public.

Some programmers prefer to list private members first, because the public members typically use the private ones, so it makes sense to define the private ones first. However, a good counterargument is that users of the class don’t care about the private members, so the public ones should come first. Either way is fine.

Access functions and encapsulation

Encapsulation

In object-oriented programming, Encapsulation (also called information hiding) is the process of keeping the details about how an object is implemented hidden away from users of the object. Instead, users of the object access the object through a public interface. In this way, users are able to use the object without having to understand how it is implemented.

In C++, we implement encapsulation via access specifiers. Typically, all member variables of the class are made private (hiding the implementation details), and most member functions are made public (exposing an interface for the user). Although requiring users of the class to use the public interface may seem more burdensome than providing public access to the member variables directly, doing so actually provides a large number of useful benefits that help encourage class re-usability and maintainability.

Note: The word encapsulation is also sometimes used to refer to the packaging of data and functions that work on that data together. We prefer to just call that object-oriented programming.

Rule: Only provide access functions when it makes sense for the user to be able to get or set a value directly.

Rule: Getters should usually return by value or const reference, not non-const reference.

Constructors

Unlike normal member functions, constructors have specific rules for how they must be named:

  • Constructors must have the same name as the class (with the same capitalization)
  • Constructors have no return type (not even void)

Remember: Fundamental variables aren’t initialized by default. they should be initialized in constructors.

1
2
3
4
5
6
7
8
Fraction(int, numerator, int denominator=1) {
m_numerator = numerator;
m_denominator = denominator;
}
int x(5);
Fraction fiveThirds1(5, 3);
Fraction fiveThirds2 {5, 3};
Fraction fiveThirds3(5);

Copy initialization using equals with classes

Much like with fundamental variables, it’s also possible to initialize classes using copy initialization:

1
2
3
int x = 6; // Copy initialize an integer
Fraction six = Fraction(6); // Copy initialize a Fraction, will call Fraction(6, 1)
Fraction seven = 7; // Copy initialize a Fraction. The compiler will try to find a way to convert 7 to a Fraction, which will invoke the Fraction(7, 1) constructor.

However, we recommend you avoid this form of initialization with classes, as it may be less efficient. Although direct initialization, uniform initialization, and copy initialization all work identically with fundamental types, copy-initialization does not work the same with classes (though the end-result is often the same). We’ll explore the differences in more detail in a future chapter.

Rule: Do not copy initialize your classes

Reducing your constructors

In the above two-constructor declaration of the Fraction class, the default constructor is actually somewhat redundant. We could simplify this class as follows:

1
2
3
4
5
public:
// Default
Fraction(int numerator=0, int denominator=1) {
// foo
}

Although this constructor is still a default constructor, it has now been defined in a way that it can accept one or two user-provided values as well.

1
2
3
Fraction zero;   // will call Fraction(0, 1)
Fraction six(6); // will call Fraction(6, 1)
Fraction fiveThirds(5, 3); // will call Fraction(5, 3)

This may produce unexpected results for classes that have multiple default parameters of different types. Consider:

1
2
3
4
5
6
7
Something(int n = 0, double d = 1.2) { // allows us to construct a Something(int, double), Something(int), or Something()
// foo
}
// main
Something s1 { 1 };
Something s2;
Something s3 { 2.4 }; // will not compile, as there's no constructor to handle Something(double)

If we want to be able to construct a Something with only a double, we’ll need to add a second (non-default) constructor:

1
2
3
4
5
6
7
8
// Default constructor
Something(int n = 0, double d = 1.2) {
// allows us to construct a Something(int, double), Something(int), or Something()
}
// non-default constructor
Something(double d) {
// foo
}

An implicitly generated default constructor

If your class has no other constructors, C++ will automatically generate a public default constructor for you. This is sometimes called an implicit constructor (or implicitly generated constructor).

If your class has any other constructors, the implicitly generated constructor will not be provided. For example:

1
2
3
4
5
6
public:
Date(int year, int month, int day) {
// foo
}
// main
Date data; // error: Can't instantiate object because default constructor doesn't exist and the compiler won't generate one

Generally speaking, it’s a good idea to always provide at least one constructor in your class. This explicitly allows you to control how objects of your class are allowed to be created, and will prevent your class from potentially breaking later when you add other constructors.

Rule: Provide at least one constructor for your class, even if it’s an empty default constructor.

Many new programmers are confused about whether constructors create the objects or not. They do not (the code the compiler creates does that).

Constructors actually serve two purposes. The primary purpose is to initialize objects that have just been created. The secondary purpose is to determine whether creation of an object is allowed. That is, an object of a class can only be created if a matching constructor can be found. This means that a class without any public constructors can’t be created!

Constructors are only intended to be used for initialization when the object is created. You should not try to call a constructor to re-initialize an existing object. While it may compile, the results will not be what you intended (instead, the compiler will create a temporary object and then discard it).

Some types of data (e.g. const and reference variables) must be initialized on the line they are declared. Consider the following example:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_value;

public:
Something() {
m_value = 1; // error: const vars can not be assigned to
}
};

Member initializer lists

To solve this problem, C++ provides a method for initializing class member variables (rather than assigning values to them after they are created) via a member initializer list (often called a “member initialization list”). Do not confuse these with the similarly named initializer list that we can use to assign values to arrays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Something {
private:
int m_value1;
double m_value2;
char m_value3;

public:
Something() : m_value1(1), m_value2(2.2), m_value3('c') {
// directly initialize our member variables
// No need for assignment here
}
};
// main
Something something;

// or more useful
Something(int value1, double value2, char value3='c')
: m_value1(value1), m_value2(value2), m_value3(value3) {
// directly initialize our member variables
// No need for assignment here
}
// main
Something something(1, 2.2);
// value = 1, value2 = 2.2, value3 gets default value 'c'

Const problem solved:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_value;

public:
Something(): m_value(5) {
// directly initialize our const member variable
}
};

Rule: Use member initializer lists to initialize your class member variables instead of assignment.

Or even in C++11, we can use m_value {5}.

We strongly encourage you to begin using this new syntax (even if you aren’t using const or reference member variables) as initialization lists are required when doing composition and inheritance (subjects we will be covering shortly).

Rule: Favor uniform initialization over direct initialization if your compiler is C++11 compatible

Prior to C++11, you can only zero an array member via a member initialization list:

1
2
3
4
5
6
7
8
9
10
class Something {
private:
const int m_array[5];

public:
Something(): m_array {} {
// zero the member array
// If we want the array to have values, we'll have to use assignment here
}
};

However, in C++11, you can fully initialize a member array using uniform initialization:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_array[5];

public:
Something(): m_array { 1, 2, 3, 4, 5 } {
// use uniform initialization to initialize our member array
}
};

A member initialization list can also be used to initialize members that are classes.

1
2
3
4
5
class B {
B(int y) : m_a(y-1) {
// call A(int) constructor to initialize member m_a
}
}

Formatting your initializer lists

C++ gives you a lot of flexibility in how to format your initializer lists, and it’s really up to you how you’d like to proceed. But here are some recommendations:

If the initializer list fits on the same line as the function name, then it’s fine to put everything on one line:

1
2
3
Something() : m_value1(1), m_value2(2.2), m_value3('c') {
// everything on one line
}

If the initializer list doesn’t fit on the same line as the function name, then it should go indented on the next line.

1
2
3
4
5
6
7
8
9
Something(int value1, double value2, char value3='c') { // this line already has a lot of stuff on it
: m_value1(value1), m_value2(value2), m_value3(value3)
// so we can put everything indented on next line
// or
// :m_value1(value1),
// m_value2(value2),
// m_value3(value3),
// m_value4(value4)
}

Initializer list order

Perhaps surprisingly, variables in the initializer list are not initialized in the order that they are specified in the initializer list. Instead, they are initialized in the order in which they are declared in the class.

For best results, the following recommendations should be observed:

  • Don’t initialize member variables in such a way that they are dependent upon other member variables being initialized first (in other words, ensure your member variables will properly initialize even if the initialization ordering is different).
  • Initialize variables in the initializer list in the same order in which they are declared in your class. This isn’t strictly required so long as the prior recommendation has been followed, but your compiler may give you a warning if you don’t do so and you have all warnings turned on.

Non-static member initialization

Starting with C++11, it’s possible to give normal class member variables (those that don’t use the static keyword) a default initialization value directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rectangle {
private:
double m_length = 1.0; // m_length has a default value of 1.0
double m_width = 1.0; // m_width has a default value of 1.0

public:
Rectangle() {
// This constructor will use the default values above since they aren't overridden here
}
Rectangle(double length, double width)
: m_length(length), m_width(width) {
// m_length and m_width are initialized by the constructor (the default values aren't used)
}
};

Non-static member initialization (also called in-class member initializers) provides default values for your member variables that your constructors will use if the constructors do not provide initialization values for the members themselves (via the member initialization list).

Overlapping and delegating constructors

Overlapping functionality problem:

1
2
3
4
5
6
7
8
9
10
class Foo {
public:
Foo() {
// code to do A
}
Foo(int value) {
// code to do A
// code to do B
}
}

The obvious solution doesn’t work prior to C++11

The obvious solution would be to have the Foo(int) constructor call the Foo() constructor to do the A portion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
public:
Foo() {
// code to do A
}
Foo(int value) {
Foo(); // use the above constructor to do A (doesn't work)
// code to do B
}
// or
Foo(int value) : Foo() {
// code to do B
}
}

However, with a pre-C++11 compiler, if you try to have one constructor call another constructor, it will often compile, but it will not work as you expect, and you will likely spend a long time trying to figure out why, even with a debugger.

Prior to C++11, calling a constructor explicitly from another constructor creates a temporary object, initializes the temporary object using the constructor, and then discards it, leaving your original object unchanged.

Using a separate function

1
2
3
4
5
6
7
8
9
10
Foo() {
Init();
}
Foo(int value) {
Init();
// do something with value
}
void Init() {
// code to init Foo
}

One small caveat: be careful when using Init() functions and dynamically allocated memory. Because Init() functions can be called by anyone at any time, dynamically allocated memory may or may not have already been allocated when Init() is called. Be careful to handle this situation appropriately — it can be slightly confusing, since a non-null pointer could be either dynamically allocated memory or an uninitialized pointer!

Delegating constructors in C++11

Starting with C++11, constructors are now allowed to call other constructors. This process is called delegating constructors (or constructor chaining).

1
2
3
4
5
6
7
8
9
10
11
class Foo {
public:
Foo() {
// code to do A
}

Foo(int value): Foo() {
// use Foo() default constructor to do A
// code to do B
}
};

This works exactly as you’d expect. Make sure you’re calling the constructor from the member initializer list, not in the body of the constructor.

Here’s another example of using delegating constructor to reduce redundant code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <string>
#include <iostream>

class Employee
{
private:
int m_id;
std::string m_name;

public:
// delegated constructor
Employee(int id=0, const std::string &name="")
: m_id(id), m_name(name) {
std::cout << "Employee " << m_name << " created.\n";
}
// Use a delegating constructor to minimize redundant code
Employee(const std::string &name)]
: Employee(0, name) {
// foo
}
};

Destructors

If your class object is holding any resources (e.g. dynamic memory, or a file or database handle), or if you need to do any kind of maintenance before the object is destroyed, the destructor is the perfect place to do so, as it is typically the last thing to happen before the object is destroyed.

Like constructors, destructors have specific naming rules:

  1. The destructor must have the same name as the class, preceded by a tilde (~).
  2. The destructor can not take arguments.
  3. The destructor has no return type.

Generally you should not call a destructor explicitly (as it will be called automatically when the object is destroyed), since there are rarely cases where you’d want to clean up an object more than once. However, destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.

RAII (Resource Acquisition Is Initialization) is a programming technique whereby resource use is tied to the lifetime of objects with automatic duration (e.g. non-dynamically allocated objects). In C++, RAII is implemented via classes with constructors and destructors. A resource (such as memory, a file or database handle, etc…) is typically acquired in the object’s constructor (though it can be acquired after the object is created if that makes sense). That resource can then be used while the object is alive. The resource is released in the destructor, when the object is destroyed. The primary advantage of RAII is that it helps prevent resource leaks (e.g. memory not being deallocated) as all resource-holding objects are cleaned up automatically.

Under the RAII paradigm, objects holding resources should not be dynamically allocated. This is because destructors are only called when an object is destroyed. For objects allocated on the stack, this happens automatically when the object goes out of scope, so there’s no need to worry about a resource eventually getting cleaned up. However, for dynamically allocated objects, the user is responsible for deletion — if the user forgets to do that, then the destructor will not be called, and the memory for both the class object and the resource being managed will be leaked!

The IntArray class at the top of this lesson is an example of a class that implements RAII — allocation in the constructor, deallocation in the destructor. std::string and std::vector are examples of classes in the standard library that follow RAII — dynamic memory is acquired on initialization, and cleaned up automatically on destruction.

Rule: If your class dynamically allocates memory, use the RAII paradigm, and don’t allocate objects of your class dynamically.

A warning about the exit() function

Note that if you use the exit() function, your program will terminate and no destructors will be called. Be wary if you’re relying on your destructors to do necessary cleanup work (e.g. write something to a log file or database before exiting).

The hidden “this” pointer

1
2
3
4
5
6
7
8
9
void setID(int id) { m_id = id; }

// main
Simple simple(1);
simple.setID(2);
// compiler convert it to: setID(&simple, 2);
// void setID(Simple* const this, int id) {
// this->m_id = id;
// }

Putting it all together:

  1. When we call simple.setID(2), the compiler actually calls setID(&simple, 2).
  2. Inside setID(), the “this” pointer holds the address of object simple.
  3. Any member variables inside setID() are prefixed with “this->”. So when we say m_id = id, the compiler is actually executing this->m_id = id, which in this case updates simple.m_id to id.

Although using this->xxx is acceptable coding practice, we find using the “m_” prefix on all member variable names provides a better solution by preventing duplicate names altogether!

Some developers prefer to explicitly add this-> to all class members. We recommend that you avoid doing so, as it tends to make your code less readable for little benefit. Using the m_ prefix is a more readable way to differentiate member variables from non-member (local) variables.

Chaining member functions

1
std::cout << "Hello, " << userName;

In this case, std::cout is an object, and operator << is a member function that operates on that object. The compiler evaluates the above snippet like this:

1
2
3
(std::cout << "Hello, ") << userName;
// -> (std::cout) << userName;
// operator << returns *this, which in this context is just std::cout

Here is a version of Calc with “chainable” functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Calc {
private:
int m_value;
public:
Calc() { m_value = 0; }

Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }

int getValue() { return m_value; }
};
// main
Calc calc;
calc.add(5).sub(3).mult(4);

Class code and header files

Defining member functions outside the class definition

C++ provides a way to separate the “declaration” portion of the class from the “implementation” portion. This is done by defining the class member functions outside of the class definition. To do so, simply define the member functions of the class as if they were normal functions, but prefix the class name to the function using the scope resolution operator (::) (same as for a namespace).

Here is our Date class with the Date constructor and setDate() function defined outside of the class definition. Note that the prototypes for these functions still exist inside the class definition, but the actual implementation has been moved outside:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calc {
private:
int m_value = 0;
public:
Calc(int value=0);

Calc& add(int value);
Calc& sub(int value);
Calc& mult(int value);

int getValue() { return m_value; }
};

Calc::Calc(int value) : m_value(value) {
// foo
}

Calc& Calc::add(int value) {
m_value += value;
return *this;
}

// ...

Putting class definitions in a header file

Date.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef DATE_H
#define DATE_H

class Date {
private:
int m_year;
// ...
public:
Date(int year, int month, int day);
// ...
};

#endif

Date.cpp:

1
2
3
4
5
6
7
8
9
10
11
#include "Date.h"

Date::Date(int year, int month, int day) {
SetDate(year, month, day);
}

void Date::SetDate(int year, int month, int day) {
m_month = month;
m_day = day;
m_year = year;
}

Doesn’t defining member functions in the header violate the one-definition rule?

It depends. Member functions defined inside the class definition are considered implicitly inline. Inline functions are exempt from the one definition per program part of the one-definition rule. This means there is no problem defining trivial member functions (such as access functions) inside the class definition itself.

Member functions defined outside the class definition are treated like normal functions, and are subject to the one definition per program part of the one-definition rule. Therefore, those functions should be defined in a code file, not inside the header. The one exception for this is for template functions, which we’ll cover in a future chapter.

So what should I define in the header file vs the cpp file, and what inside the class definition vs outside?

You might be tempted to put all of your member function definitions into the header file, inside the class. While this will compile, there are a couple of downsides to doing so.

  • First, as mentioned above, this clutters up your class definition.
  • Second, functions defined inside the class are implicitly inline. For larger functions that are called from many places, this can bloat your code.
  • Third, if you change anything about the code in the header, then you’ll need to recompile every file that includes that header. This can have a ripple effect, where one minor change causes the entire program to need to recompile (which can be slow). If you change the code in a .cpp file, only that .cpp file needs to be recompiled!

Therefore, we recommend the following:

  • For classes used in only one file that aren’t generally reusable, define them directly in the single .cpp file they’re used in.
  • For classes used in multiple files, or intended for general reuse, define them in a .h file that has the same name as the class.
  • Trivial member functions (trivial constructors or destructors, access functions, etc…) can be defined inside the class.
  • Non-trivial member functions should be defined in a .cpp file that has the same name as the class.

Default parameters

Default parameters for member functions should be declared in the class definition (in the header file), where they can be seen by whomever #includes the header.

Libraries

Separating the class definition and class implementation is very common for libraries that you can use to extend your program. Throughout your programs, you’ve #included headers that belong to the standard library, such as iostream, string, vector, array, and other. Notice that you haven’t needed to add iostream.cpp, string.cpp, vector.cpp, or array.cpp into your projects. Your program needs the declarations from the header files in order for the compiler to validate you’re writing programs that are syntactically correct. However, the implementations for the classes that belong to the C++ standard library is contained in a precompiled file that is linked in at the link stage. You never see the code.

Outside of some open source software (where both .h and .cpp files are provided), most 3rd party libraries provide only header files, along with a precompiled library file. There are several reasons for this:

  1. It’s faster to link a precompiled library than to recompile it every time you need it。
  2. A single copy of a precompiled library can be shared by many applications, whereas compiled code gets compiled into every executable that uses it (inflating file sizes)。
  3. Intellectual property reasons (you don’t want people stealing your code).

Having your own files separated into declaration (header) and implementation (code file) is not only good form, it also makes creating your own custom libraries easier. Creating your own libraries is beyond the scope of these tutorials, but separating your declaration and implementation is a prerequisite to doing so.

Const class objects and member functions

1
2
3
const Date date1; // initialize using default constructor
const Date date2(2020, 10, 16); // initialize using parameterized constructor
const Date date3 { 2020, 10, 16 }; // initialize using parameterized constructor (C++11)

Once a const class object has been initialized via constructor, any attempt to modify the member variables of the object is disallowed, as it would violate the const-ness of the object. This includes both changing member variables directly (if they are public), or calling member functions that set the value of member variables. Consider the following class:

1
2
3
const Something something;
something.m_value = 5;
something.setValue(5);

Now, consider the following line of code:

1
std::cout << something.getValue();

Perhaps surprisingly, this will also cause a compile error, even though getValue() doesn’t do anything to change a member variable! It turns out that const class objects can only explicitly call const member functions, and getValue() has not been marked as a const member function.

A const member function is a member function that guarantees it will not modify the object or call any non-const member functions (as they may modify the object).

To make getValue() a const member function, we simply append the const keyword to the function prototype, after the parameter list, but before the function body:

1
2
3
public:
int getValue() const { return m_value; }
// note addition of const keyword after parameter list, but before function body

Futhermore, any const member function that attempts to change a member variable or call a non-const member function will cause a compiler error to occur.

Note that constructors cannot be marked as const. This is because constructors need to be able to initialize their member variables, and a const constructor would not be able to do so. Consequently, the language disallows const constructors.

Const references

Although instantiating const class objects is one way to create const objects, a more common way is by passing an object to a function by const reference.

We covered the merits of passing class arguments by const reference instead of by value. To recap, passing a class argument by value causes a copy of the class to be made (which is slow) — most of the time, we don’t need a copy, a reference to the original argument works just fine, and is more performant because it avoids the needless copy. We typically make the reference const in order to ensure the function does not inadvertently change the argument, and to allow the function to work with R-values (e.g. literals), which can be passed as const references, but not non-const references.

1
2
3
4
5
6
7
8
void printDate(const Date &date) {
std::cout << date.getYear() << "/" << date.getMonth() << "/" << date.getDay() << '\n';
}

// const is necessary; otherwise it will cause a compile error
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }

Overloading const and non-const function

Finally, although it is not done very often, it is possible to overload a function in such a way to have a const and non-const version of the same function:

1
2
3
4
5
6
7
8
9
10
11
12
class Something {
private:
std::string m_value;

public:
Something(const std::string &value="") { m_value= value; }

const std::string& getValue() const { return m_value; }
// getValue() for const objects
std::string& getValue() { return m_value; }
// getValue() for non-const objects
};

The non-const version of getValue() will only work with non-const objects, but is more flexible in that we can use it to both read and write m_value (which we do by assigning the string “Hi”).

The const version of getValue() will work with either const or non-const objects, but returns a const reference, to ensure we can’t modify the const object’s data.

Because passing objects by const reference is common, your classes should be const-friendly. That means making any member function that does not modify the state of the class object const!

Static member variables

C++ introduces two more uses for the static keyword when applied to classes: static member variables, and static member functions. Fortunately, these uses are fairly straightforward. We’ll talk about static member variables in this lesson, and static member functions in the next.

Member variables of a class can be made static by using the static keyword. Unlike normal member variables, static member variables are shared by all objects of the class. Consider the following program, similar to the above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Something {
public:
static int s_value;
// Note that the static member definition is not subject to access controls: you can define and initialize the value even if it's declared as private (or protected) in the class
};

int Something::s_value = 1;

int main() {
Something first;
Something second;
first.s_value = 2;

std::cout << first.s_value << '\n';
std::cout << second.s_value << '\n';
// but we'd better use Something::s_value!

return 0;
}

If the class is defined in a .h file, the static member definition is usually placed in the associated code file for the class (e.g. Something.cpp). If the class is defined in a .cpp file, the static member definition is usually placed directly underneath the class. Do not put the static member definition in a header file (much like a global variable, if that header file gets included more than once, you’ll end up with multiple definitions, which will cause a compile error).

Static member functions

What if static member variables are declared private:

1
2
3
4
5
6
7
8
9
10
11
class Something {
private:
static int s_value;
};

int Something::s_value = 1;
// initializer, this is okay even though s_value is private since it's a definition

int main() {
// Something::s_value can't be accessed
}

Normally we access private members through public member functions. While we could create a normal public member function to access s_value, we’d then need to instantiate an object of the class type to use the function! We can do better. It turns out that we can also make functions static.

1
2
public:
static int getValue() { return s_value; }

Static member functions have no *this pointer

C++ does not support static constructors

If initializing your static member variable requires executing code (e.g. a loop), there are many different, somewhat obtuse ways of doing this. The following code presents one of the better methods. However, it is a little tricky, and you’ll probably never need it, so feel free to skip the remainder of this section if you desire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
static std::vector<char> s_mychars;
public:
class _init {
// we're defining a nested class named _init
public:
_init() {
s_mychars.push_back('a');
s_mychars.push_back('e');
}
};
private:
static _init s_initializer;
// we'll use this static object to ensure the _init constructor is called
};

std::vector<char> MyClass::s_mychars;
// define our static member variable
MyClass::_init MyClass::s_initializer;
// define our static initializer, which will call the _init constructor, which will initialize s_mychars

Friend functions and classes

A friend function is a function that can access the private members of a class as though it were a member of that class. In all other regards, the friend function is just like a normal function. A friend function may be either a normal function, or a member function of another class. To declare a friend function, simply use the friend keyword in front of the prototype of the function you wish to be a friend of the class. It does not matter whether you declare the friend function in the private or public section of the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Accumulator {
private:
int m_value;
public:
Accumulator() { m_value = 0; }
void add(int value) { m_value += value; }
// Make the reset() function a friend of this class
friend void reset(Accumulator &accumulator);
};

// reset() is now a friend of the Accumulator class
void reset(Accumulator &accumulator) {
// And can access the private data of Accumulator objects
accumulator.m_value = 0;
}

int main() {
Accumulator acc;
acc.add(5); // add 5 to the accumulator
reset(acc); // reset the accumulator to 0

return 0;
}

Note that we have to pass an Accumulator object to reset(). This is because reset() is not a member function. It does not have a *this pointer, nor does it have an Accumulator object to work with, unless given one.

A function can be a friend of more than one class at the same time.

It is also possible to make an entire class a friend of another class. This gives all of the members of the friend class access to the private members of the other class. Here is an example:

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
class Storage {
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue) {
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display class a friend of Storage
friend class Display;
};

class Display {
private:
bool m_displayIntFirst;

public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }

void displayItem(Storage &storage) {
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};

Friend member functions

Instead of making an entire class a friend, you can make a single member function a friend. This is done similarly to making a normal function a friend, except using the name of the member function with the className:: prefix included (e.g. Display::displayItem).

However, in actuality, this can be a little trickier than expected. Let’s convert the previous example to make Display::displayItem a friend member function. You might try something like this:

Note: But it is not a member function! (member func -> a friend)

1
2
3
4
5
6
7
8
9
10
11
class Storage {
private:
// foo
public:
// foo
friend void Display::displayItem(Storage& storage);
// error: Storage hasn't seen the full definition of class Display
};
class Display {
// foo
};

However, it turns out this won’t work. In order to make a member function a friend, the compiler has to have seen the full definition for the class of the friend member function (not just a forward declaration). Since class Storage hasn’t seen the full definition for class Display yet, the compiler will error at the point where we try to make the member function a friend.

Fortunately, this is easily resolved simply by moving the definition of class Display before the definition of class Storage.

1
2
3
4
5
6
7
8
9
// forward declaration
// class Storage; // forward declaration for Storage
class Display {
// foo
// if displayItem() here uses Storage as a reference, we can add forward declaration before "class Display { ... };"
};
class Storage {
// goo
};

If this seems like a pain — it is. Fortunately, this dance is only necessary because we’re trying to do everything in a single file. A better solution is to put each class definition in a separate header file, with the member function definitions in corresponding .cpp files. That way, all of the class definitions would have been visible immediately in the .cpp files, and no rearranging of classes or functions is necessary!

Summary

A friend function or class is a function or class that can access the private members of another class as though it were a member of that class. This allows the friend or class to work intimately with the other class, without making the other class expose its private members (e.g. via access functions).

Friending is uncommonly used when two or more classes need to work together in an intimate way, or much more commonly, when defining overloading operators (which we’ll cover in chapter 9).

Note that making a specific member function a friend requires the full definition for the class of the member function to have been seen first.

Anonymous objects

Although our prior examples have been with built-in data types, it is possible to construct anonymous objects of our own class types as well. This is done by creating objects like normal, but omitting the variable name.

1
2
Cents cents(5);
Cents(7); // anonymous object

More specifically,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Cents {
private:
int m_cents;
public;
Cents(int cents) { m_cents = cents; }
int getCents() const { return m_cents; }
}; // end of the class

void print(const Cents &cents) {
std::cout << cents.getCents() << " cents" << "\n";
}

Cents add(const Cents &c1, const Cents &c2) {
return Cents(c1.getCents() + c2.getCents());
// return anonymous Cents value
}

int main() {
print(Cents(6));
std::cout << add(Cents(6), Cents(8)).getCents() << "\n";
return 0;
}

It is worth noting that anonymous objects are treated as rvalues (not lvalues, which have an address). This means anonymous objects can only be passed or returned by value or const reference. Otherwise, a named variable must be used instead.

Nested types in classes

Unlike functions, which can’t be nested inside each other, in C++, types can be defined (nested) inside of a class. To do this, you simply define the type inside the class, under the appropriate access specifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fruit {
public:
// Note: we've moved FruitType inside the class
enum FruitType {
APPLE,
BANANA,
CHERRY
};
};

int main() {
// Here, we can access the enum by Fruit::APPLE
}

Classes essentially act as a namespace for any nested types. In the prior example, we were able to access enumerator APPLE directly, because the APPLE enumerator was placed into the global scope (we could have prevented this by using an enum class instead of an enum, in which case we’d have accessed APPLE via FruitType::APPLE instead). Now, because FruitType is considered to be part of the class, we access the APPLE enumerator by prefixing it with the class name: Fruit::APPLE.

Note that because enum classes also act like namespaces, if we’d nested FruitType inside Fruit as an enum class instead of an enum, we’d access the APPLE enumerator via Fruit::FruitType::APPLE.

Although enumerations are probably the most common type that is nested inside a class, C++ will let you define other types within a class, such as typedefs, type aliases, and even other classes!

Like any normal member of a class, nested classes have the same access to members of the enclosing class that the enclosing class does. However, the nested class does not have any special access to the “this” pointer of the enclosing class.

Defining nested classes isn’t very common, but the C++ standard library does do so in some cases, such as with iterator classes.

Timing your code (Timing API)

One easy way is to time your code to see how long it takes to run. C++11 comes with some functionality in the chrono library to do just that. However, using the chrono library is a bit arcane. The good news is that we can easily encapsulate all the timing functionality we need into a class that we can then use in our own programs.

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
#include <chrono>

class Timer {
private:
// Type aliases to make accessing nested type easier
using clock_t = std::chrono::high_resolution_clock;
using second_t = std::chrono::duration<double, std::ratio<1> >;

std::chrono::time_point<clock_t> m_beg;

public:
Timer() : m_beg(clock_t::now()) {}
void reset() {
m_beg = clock_t::now();
}
double elapsed() const {
return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
}
};

const int g_arrayElements = 10000;

// usage
int main() {
std::array<int, g_arrayElements> array;
for (int i = 0; i < g_arrayElements; ++i)
array[i] = g_arrayElements - i;

Timer t;

std::sort(array.begin(), array.end());

std::cout << "Time taken: " << t.elapsed() << " seconds\n";
}

Timing is straightforward, but your results can be significantly impacted by a number of things, and it’s important to be aware of what those things are.

  1. Make sure you’re using a release build target, not a debug build target. Debug build targets typically turn optimization off, and that optimization can have a significant impact on the results.
  2. For best results, make sure your system isn’t doing anything CPU or memory intensive.
  3. When doing comparisons between two sets of code, be wary of what may change between runs that could impact timing. Your system may have kicked off an antivirus scan in the background, or maybe you’re streaming music now when you weren’t previously. Randomization can also impact timing.

Operator overloading

In C++, operators are implemented as functions. By using function overloading on the operator functions, you can define your own versions of the operators that work with different data types (including classes that you’ve written). Using function overloading to overload operators is called operator overloading.

1
2
x + y // translated as follow
operator+(x, y) // where operator+ is the name of the function

Resolving overloaded operators

When evaluating an expression containing an operator, the compiler uses the following rules:

  • If all of the operands are fundamental data types, the compiler will call a built-in routine if one exists. If one does not exist, the compiler will produce a compiler error.
  • If any of the operands are user data types (e.g. one of your classes, or an enum type), the compiler looks to see whether the type has a matching overloaded operator function that it can call. If it can’t find one, it will try to convert one or more of the user-defined type operands into fundamental data types so it can use a matching built-in operator (via an overloaded typecast, which we’ll cover later in this chapter). If that fails, then it will produce a compile error.

Note:

  • First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?:), sizeof, scope (::), member selector (.), and member pointer selector (.*).

  • Second, you can only overload the operators that exist. You can not create new operators or rename existing operators. For example, you could not create an operator ** to do exponents.

  • Third, at least one of the operands in an overloaded operator must be a user-defined type. This means you can not overload the plus operator (+) to work with one integer and one double. However, you could overload the plus operator to work with an integer and a Mystring.

  • Fourth, it is not possible to change the number of operands an operator supports.

  • Finally, all operators keep their default precedence and associativity (regardless of what they’re used for) and this can not be changed.

    • One example: Some new programmers attempt to overload the bitwise XOR operator (^) to do exponentiation. However, in C++, operator^ has a lower precedence level than the basic arithmetic operators, which causes expressions to evaluate incorrectly.

Rule: When overloading operators, it’s best to keep the function of the operators as close to the original intent of the operators as possible.

Rule: If the meaning of an operator when applied to a custom class is not clear and intuitive, use a named function instead.

Overload the arithmetic operators using friend functions

1
2
3
4
5
6
7
8
9
10
11
12
13
// addCents example
class Cents {
public:
friend Cents operator+(const Cents &c1, const Cents &c2);

// if we declare it as a member function, we need an Cents object to use this function!
};

Cents operator+(const Cents &c1, const Cents &c2) {
// note: this function is not a member function!
// we can access m_cents directly because this is a friend function
return Cents(c1.m_cents + c2.m_cents);
}

Friend functions can be defined inside the class

Even though friend functions are not members of the class, they can still be defined inside the class if desired:

1
2
3
4
5
public:
// but it is not a member function
friend Cents operator+(const Cents &c1, const Cents &c2) {
return Cents(c1.m_cents + c2.m_cents);
}

We generally don’t recommend this, as non-trivial function definitions are better kept in a separate .cpp file, outside of the class definition. However, we will use this pattern in future tutorials to keep the examples concise.

Overloading operators for operands of different types

When C++ evaluates the expression x + y, x becomes the first parameter, and y becomes the second parameter. When x and y have the same type, it does not matter if you add x + y or y + x — either way, the same version of operator+ gets called. However, when the operands have different types, x + y does not call the same function as y + x.

For example, Cents(4) + 6 would call operator+(Cents, int), and 6 + Cents(4) would call operator+(int, Cents).

Therefore, we need to write two functions separately:

1
2
3
4
5
// add Cents + int using a friend function
friend Cents operator+(const Cents &c1, int value);

// add int + Cents using a friend function
friend Cents operator+(int value, const Cents &c1);

It is often possible to define overloaded operators by calling other overloaded operators. You should do so if and when doing so produces simpler code.

Overload operators using normal functions

Using a friend function to overload an operator is convenient because it gives you direct access to the internal members of the classes you’re operating on. In the initial Cents example above, our friend function version of operator+ accessed member variable m_cents directly.

However, if you don’t need that access, you can write your overloaded operators as normal functions. Note that the Cents class above contains an access function (getCents()) that allows us to get at m_cents without having to have direct access to private members. Because of this, we can write our overloaded operator+ as a non-friend:

One-file version:

1
2
3
4
5
6
// note: this function is not a member function nor a friend function!
Cents operator+(const Cents &c1, const Cents &c2) {
// use the Cents constructor and operator+(int, int)
// we don't need direct access to private members here
return Cents(c1.getCents() + c2.getCents());
}

Two-file version:

  • Cents.h:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Cents {
    private:
    int m_cents;
    public:
    Cents(int cents) { m_cents = cents; }
    int getCents() const { return m_cents; }
    };

    // Need to explicitly provide prototype for operator+ so uses of operator+ in other files know this overload exists
    Cents operator+(const Cents &c1, const Cents &c2);
  • Cents.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "Cents.h"
    // note: this function is not a member function nor a friend function!
    Cents operator+(const Cents &c1, const Cents &c2) {
    // use the Cents constructor and operator+(int, int)
    // we don't need direct access to private members here
    return Cents(c1.getCents() + c2.getCents());
    }

In general, a normal function should be preferred over a friend function if it’s possible to do so with the existing member functions available (the less functions touching your classes’s internals, the better). However, don’t add additional access functions just to overload an operator as a normal function instead of a friend function!

Rule: Prefer overloading operators as normal functions instead of friends if it’s possible to do so without adding additional functions.

Overload the I/O operators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// std::ostream is the type for object std::cout
friend std::ostream& operator<< (std::ostream &out, const Point &point);

std::ostream& operator<< (std::ostream &out, const Point &point) {
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ")";
return out;
}

int main() {
Point point(1, 2);
std::cout << point << "\n";
return 0;
}

The trickiest part here is the return type. With the arithmetic operators, we calculated and returned a single answer by value (because we were creating and returning a new result). However, if you try to return std::ostream by value, you'll get a compiler error. This happens because std::ostream specifically disallows being copied.

In this case, we return the left hand parameter as a reference. This not only prevents a copy of std::ostream from being made, it also allows us to “chain” output commands together, such as std::cout << point << std::endl;.

Overloading operator>>

It is also possible to overload the input operator. This is done in a manner analogous to overloading the output operator. The key thing you need to know is that std::cin is an object of type std::istream. Here’s our Point class with an overloaded operator>>:

1
2
3
4
5
6
std::istream& operator>> (std::istream &in, Point &point) {
in >> point.m_x;
in >> point.m_y;
}
Point point;
std::cin >> point;

In conclusion, overloading operator<< and operator>> make it extremely easy to output your class to screen and accept user input from the console.

Overload operators using member functions

Overloading operators using a member function is very similar to overloading operators using a friend function. When overloading an operator using a member function:

  • The overloaded operator must be added as a member function of the left operand.
  • The left operand becomes the implicit *this object
  • All other operands become function parameters.

Converting a friend overloaded operator to a member overloaded operator is easy:

  1. The left parameter is removed, because that parameter now becomes the implicit *this object.
  2. Inside the function body, all references to the left parameter can be removed (e.g. cents.m_cents becomes m_cents, which implicitly references the *this object).
1
2
3
4
5
6
7
8
9
10
11
12
13
// friend version
friend Cents operator+(const Cents &cents, int value);

// member version
Cents operator+(int value) {
return Cents(m_cents + value);
}

// main
Cents cents2 = cents1 + 2;
// friend version: operator+(cents1, 2)
// member version: cents1.operator+(2)
// --implicitly--> operator+(&cents1, 2)

So if we can overload an operator as a friend or a member, which should we use? In order to answer that question, there’s a few more things you’ll need to know.

Not everything can be overloaded as a friend function

The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.

Not everything can be overloaded as a member function

In lesson 9.3 — Overloading the I/O operators, we overloaded operator<< for our Point class using the friend function method.

However, we are not able to overload operator<< as a member function. Why not? Because the overloaded operator must be added as a member of the left operand. In this case, the left operand is an object of type std::ostream. std::ostream is fixed as part of the standard library. We can’t modify the class declaration to add the overload as a member function of std::ostream. This necessitates that operator<< be overloaded as a friend.

Similarly, although we can overload operator+(Cents, int) as a member function (as we did above), we can’t overload operator+(int, Cents) as a member function, because int isn’t a class we can add members to.

Typically, we won’t be able to use a member overload if the left operand is either not a class (e.g. int), or it is a class that we can’t modify (e.g. std::ostream).

When to use a normal, friend, or member function overload

One of the two is usually a better choice than the other.

  • When dealing with binary operators that don't modify the left operand (e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry”, as all operands become explicit parameters (instead of the left operand becoming *this and the right operand becoming an explicit parameter).

  • When dealing with binary operators that do modify the left operand (e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.

  • Unary operators are usually overloaded as member functions as well, since the member version has no parameters.

The following rules of thumb can help you determine which form is best for a given situation:

  • If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.
  • If you’re overloading a unary operator, do so as a member function.
  • If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), do so as a member function if you can.
  • If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function or friend function. (if left operands of basic types change, they are new variables, not the original ones)

Also, friend or normal?

  • Depending on whether you’re going to access the private members of the class.
  • But in the normal version, you can still call public getter and setter to access private members.

Overload unary operators +, -, and !

1
2
3
4
public:
Cents operator- () const {
return Cents(-m_cents);
}

Note that there’s no confusion between the negative operator- and the
minus operator- since they have a different number of parameters.

1
2
3
4
5
public:
// const!
bool operator! () const {
return (m_x == 0.0 && m_y == 0.0);
}

Overload the comparison operators

1
2
3
4
// normal or friend version (don't need const)
bool operator == (const Car &c1, const Car &c2) {
return (c1.m_make == c2.m_make && c1.m_model == c2.m_model);
}

What about operator< and operator>? What would it mean for a Car to be greater or less than another Car? We typically don’t think about cars this way. Since the results of operator< and operator> would not be immediately intuitive, it may be better to leave these operators undefined.

Recommendation: Don’t define overloaded operators that don’t make sense for your class.

However, there is one common exception to the above recommendation. What if we wanted to sort a list of Cars? In such a case, we might want to overload the comparison operators to return the member (or members) you’re most likely to want to sort on. For example, an overloaded operator< for Cars might sort based on make and model alphabetically.

Overload the increment and decrement operators

Because the increment and decrement operators are both unary operators and they modify their operands, they’re best overloaded as member functions. We’ll tackle the prefix versions first because they’re the most straightforward.

1
2
3
4
5
6
7
8
9
public:
// prefix version
Digit& operator++ () {
if (m_digit == 9)
m_digit = 0;
else
++m_digit;
return *this;
}

Overloading postfix increment and decrement

Normally, functions can be overloaded when they have the same name but a different number and/or different type of parameters. However, consider the case of the prefix and postfix increment and decrement operators. Both have the same name (eg. operator++), are unary, and take one parameter of the same type. So how it is possible to differentiate the two when overloading?

dummy = fake, false

The answer is that C++ uses a dummy variable or dummy argument for the postfix operators. This argument is a fake integer parameter that only serves to distinguish the postfix version of increment/decrement from the prefix version.

  • ++i: i is the (this), so the 1st parameter is not needed.

  • i++: i corresponds the 2nd parameter “int”, but we don’t use it; instead we’ll use the implicit, hidden 1st parameter (this).

I don’t know if it is correct. Too confusing. Maybe it just belongs to syntax stuff.

Here is the above Digit class with both prefix and postfix overloads:

1
2
3
4
5
6
7
8
9
10
11
12
public:
// prefix (for comparison) | ++i
Digit& operator++ ();
// postfix | i++
Digit operator++ (int) {
// Create a temp
Digit temp(m_digit);
// apply the prefix operator above, cut down duplicate code!
++(*this);
// Return temp result
return temp;
}

There are a few interesting things going on here.

  • First, note that we’ve distinguished the prefix from the postfix operators by providing an integer dummy parameter on the postfix version.

  • Second, because the dummy parameter is not used in the function implementation, we have not even given it a name. This tells the compiler to treat this variable as a placeholder, which means it won’t warn us that we declared a variable but never used it.

  • Third, note that the prefix and postfix operators do the same job — they both increment or decrement the object. The difference between the two is in the value they return. The overloaded prefix operators return the object after it has been incremented or decremented. Consequently, overloading these is fairly straightforward. We simply increment or decrement our member variables, and then return *this.

  • The postfix operators, on the other hand, need to return the state of the object before it is incremented or decremented. This leads to a bit of a conundrum — if we increment or decrement the object, we won’t be able to return the state of the object before it was incremented or decremented. On the other hand, if we return the state of the object before we increment or decrement it, the increment or decrement will never be called.

The typical way this problem is solved is to use a temporary variable that holds the value of the object before it is incremented or decremented. Then the object itself can be incremented or decremented. And finally, the temporary variable is returned to the caller. In this way, the caller receives a copy of the object before it was incremented or decremented, but the object itself is incremented or decremented. Note that this means the return value of the overloaded operator must be a non-reference, because we can't return a reference to a local variable that will be destroyed when the function exits. Also note that this means the postfix operators are typically less efficient than the prefix operators because of the added overhead of instantiating a temporary variable and returning by value instead of reference.

Note that they are two different objects!

Overload the subscript operator

The subscript operator is one of the operators that must be overloaded as a member function. An overloaded operator[] function will always take one parameter (unary): the subscript that the user places between the hard braces. In our IntList case, we expect the user to pass in an integer index, and we’ll return an integer value back as a result.

1
2
3
4
5
6
7
8
9
10
11
class IntList {
private:
int m_list[10];
public:
int& operator[] (const int index);
};
// including setter and getter!
int& IntList::operator[] (const int index) {
assert(index >= 0 && index < 10); // error checking
return m_list[index];
}

This is both easy syntactically and from a comprehension standpoint. When list[2] evaluates, the compiler first checks to see if there’s an overloaded operator[] function. If so, it passes the value inside the hard braces (in this case, 2) as an argument to the function.

Note that although you can provide a default value for the function parameter, actually using operator[] without a subscript inside is not considered a valid syntax, so there’s no point.

Why operator[] returns a reference

Let’s take a closer look at how list[2] = 3 evaluates. Because the subscript operator has a higher precedence than the assignment operator, list[2] evaluates first. list[2] calls operator[], which we’ve defined to return a reference to list.m_list[2]. Because operator[] is returning a reference, it returns the actual list.m_list[2] array element. Our partially evaluated expression becomes list.m_list[2] = 3, which is a straightforward integer assignment.

Because the result of operator[] can be used on the left hand side of an assignment, the return value of operator[] must be l-value, because you can only take a reference of variables that have memory addresses.

Dealing with const objects

In the above IntList example, operator[] is non-const, and we can use it as an l-value to change the state of non-const objects. However, what if our IntList object was const? In this case, we wouldn’t be able to call the non-const version of operator[] because that would allow us to potentially change the state of a const object.

The good news is that we can define a non-const and a const version of operator[] separately. The non-const version will be used with non-const objects, and the const version with const-objects.

1
2
3
public:
int& operator[] (const int index);
const int& operator[] (const int index); // add const?

Pointers to objects and overloaded operator[] don’t mix

If you try to call operator[] on a pointer to an object, C++ will assume you’re trying to index an array of objects of that type.

1
2
3
4
5
IntList *list = new IntList;
list[2] = 3; // error: this will assume we're accessing index 2 of an array of IntLists
// proper syntax:
(*list)[2] = 3;
delete list;

Because we can’t assign an integer to an IntList, this won’t compile. However, if assigning an integer was valid, this would compile and run, with undefined results.

Rule: Make sure you’re not trying to call an overloaded operator[] on a pointer to an object.

The function parameter does not need to be an integer

As mentioned above, C++ passes what the user types between the hard braces as an argument to the overloaded function. In most cases, this will be an integer value. However, this is not required — and in fact, you can define that your overloaded operator[] take a value of any type you desire. You could define your overloaded operator[] to take a double, a std::string, or whatever else you like.

1
2
3
4
5
6
7
8
9
class Stupid {
public:
void operator[] (std::string index);
};

// It does not make sense to print sth., but it is the easiest way to show that the function parameter can be a non-integer
void Stupid::operator[] (std::string index) {
std::cout << index;
}

Overload the parenthesis operator

Why this one is special

All of the overloaded operators you have seen so far let you define the type of the operator’s parameters, but not the number of parameters (which is fixed based on the type of the operator). For example, operator== always takes two parameters, whereas operator! always takes one. The parenthesis operator (operator()) is a particularly interesting operator in that it allows you to vary both the type AND number of parameters it takes.

There are two things to keep in mind:

  • First, the parenthesis operator must be implemented as a member function.
  • Second, in non-object-oriented C++, the () operator is used to call functions. In the case of classes, operator() is just a normal operator that calls a function (named operator()) like any other overloaded operator.

The following content is about Matrix.

Operator[] is limited to a single parameter, it is not sufficient to let us index a two-dimensional array.

However, because the () operator can take as many parameters as we want it to have, we can declare a version of operator() that takes two integer index parameters, and use it to access our two-dimensional array. Here is an example of this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private:
double data[4][4];
public:
double& operator() (int row, int col) {
// assert
return data[row][col];
}
const double& operator() (int row, int col) const {
// assert
return data[row][col];
}
void operator() () {
// reset all elements of the matrix to 0.0
}

// main
Matrix matrix;
matrix(1, 2) = 4.5;
std::cout << matrix(1, 2);
matrix(); // erase matrix

Because the () operator is so flexible, it can be tempting to use it for many different purposes. However, this is strongly discouraged, since the () symbol does not really give any indication of what the operator is doing. In our example above, it would be better to have written the erase functionality as a function called clear() or erase(), as matrix.erase() is easier to understand than matrix() (which could do anything!).

Having fun with functors

Operator() is also commonly overloaded to implement functors (or function object), which are classes that operate like functions. The advantage of a functor over a normal function is that functors can store data in member variables (since they are classes).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Accumulator {
private:
int m_counter = 0;

public:
Accumulator() { }
int operator() (int i) {
return (m_counter += i);
}
};

int main() {
Accumulator acc;
std::cout << acc(10) << std::endl; // prints 10
std::cout << acc(20) << std::endl; // prints 30
return 0;
}

Note that using our Accumulator looks just like making a normal function call, but our Accumulator object is storing an accumulated value.

You may wonder why we couldn’t do the same thing with a normal function and a static local variable to preserve data between function calls. We could, but because functions only have one global instance, we’d be limited to using it for one thing at a time. With functors, we can instantiate as many separate functor objects as we need and use them all simultaneously.

Overload typecasts

C++ already knows how to convert between the built-in data types. However, it does not know how to convert any of our user-defined classes. That’s where overloading the typecast operators comes into play.

1
2
3
4
5
6
7
8
9
10
void printInt(int value) {
std::cout << value;
}

int main() {
Cents cents(7);
printInt(cents.getCents()); // print 7

return 0;
}

If we have already written a lot of functions that take integers as parameters, our code will be littered with calls to getCents(), which makes it more messy than it needs to be.

To make things easier, we can overload the int typecast, which will allow us to cast our Cents class directly into an int. The following example shows how this is done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cents {
private:
int m_cents;
public:
Cents(int cents=0) {
m_cents = cents;
}
// Overloaded int cast
operator int() { return m_cents; }

int getCents() { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};

int main() {
Cents cents(7);
printInt(cents);
return 0;
}

There are two things to note:

  • To overload the function that casts our class to an int, we write a new function in our class called operator int(). Note that there is a space between the word operator and the type we are casting to.
  • Casting operators do not have a return type. C++ assumes you will be returning the correct type.

We can now also explicitly cast our Cents variable to an int:

1
2
Cents cents(7);s
int c = static_cast<int>(cents);

You can overload cast operators for any data type you wish, including your own user-defined data types!

Here’s a new class called Dollars that provides an overloaded Cents cast operator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dollars {
private:
int m_dollars;
public:
Dollars(int dollars=0) {
m_dollars = dollars;
}

// Allow us to convert Dollars into Cents
operator Cents() {
return Cents(m_dollars * 10);
}
};

// main
Dollars dollars(9);
printCents(dollars); // dollars will be implicitly cast to a Cents here

The copy constructor

Recap the types of initialization that C++ supports:

  • direct initialization
  • uniform initialization
  • copy initialization

With direct and uniform initialization, the object being created is directly initialized. However, copy initialization is a little more complicated. We’ll explore copy initialization in more detail in the next lesson. But in order to do that effectively, we need to take a short detour.

1
2
3
4
int main() {
Fraction fiveThirds(5, 3); // Direct initialize a Fraction, calls Fraction(int, int) constructor
Fraction fCopy(fiveThirds); // with what constructor??
}

The answer is that this line is calling Fraction’s copy constructor. A copy constructor is a special type of constructor used to create a new object as a copy of an existing object. And much like a default constructor, if you do not provide a copy constructor for your classes, C++ will create a public copy constructor for you. Because the compiler does not know much about your class, by default, the created copy constructor utilizes a method of initialization called memberwise initialization. Memberwise initialization simply means that each member of the copy is initialized directly from the member of the class being copied. In the above example, fCopy.m_numerator would be initialized from fiveThirds.m_numerator, etc…

Just like we can explicitly define a default constructor, we can also explicitly define a copy constructor. The copy constructor looks just like you’d expect it to:

1
2
3
4
5
// Copy constructor
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Copy constructor called\n";
}

One interesting note: You’ve already seen a few examples of overloaded operator<<, where we’re able to access the private members of parameter f1 because the function is a friend of the Fraction class. Similarly, member functions of a class can access the private members of parameters of the same class type. Since our Fraction copy constructor takes a parameter of the class type (to make a copy of), we’re able to access the members of parameter fraction directly, even though it’s not the implicit object.

Preventing copies

We can prevent copies of our classes from being made by making the copy constructor private:

1
2
3
4
5
private:
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Testing" << "\n";
}

The copy constructor may be elided

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public:
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Testing" << "\n";
}

// main
// original version
Fraction fiveThirds(5, 3);
Fraction fCopy(fiveThirds);
// output: Testing

// Elided version
Fraction fiveThirds(Fraction(5, 3));
// output: nothing!

Why didn’t our copy constructor get called?

Note that initializing an anonymous object and then using that object to direct initialize our defined object takes two steps (one to create the anonymous object, one to call the copy constructor). However, the end result is essentially identical to just doing a direct initialization, which only takes one step.

For this reason, in such cases, the compiler is allowed to opt out of calling the copy constructor and just do a direct initialization instead. This process is called elision.

So although you wrote:

1
Fraction fiveThirds(Fraction(5, 3));

The compiler may change this to:

1
Fraction fiveThirds(5, 3);

which only requires one constructor call (to Fraction(int, int)). Note that in cases where elision is used, any statements in the body of the copy constructor are not executed, even if they would have produced side effects (like printing to the screen)!

Prior to C++17, copy elision is an optimization the compiler can make. As of C++17, some cases of copy elision (including the example above) have been made mandatory.

Finally, note that if you make the copy constructor private, any initialization that would use the copy constructor will cause a compile error, even if the copy constructor is elided! Jesus!

1
2
// copy constructor is private
Fraction fiveThirds(Fraction(5, 3)); // error

Copy initialization

Consider the following line of code:

1
int x = 5;

This statement uses copy initialization to initialize newly created integer variable x to the value of 5. However, classes are a little more complicated, since they use constructors for initialization.

1
2
3
Fraction six = Fraction(6);
// it is evaluated the same way as the following
Fraction six(Fraction(6));

And as you learned in the previous lesson, this can potentially make calls to both Fraction(int, int) and the Fraction copy constructor (which may be elided for performance reasons). However, because eliding isn’t guaranteed (prior to C++17, where elision in this particular case is now mandatory), it’s better to avoid copy initialization for classes, and use uniform initialization instead.

Other places copy initialization is used

There are a few other places copy initialization is used, but two of them are worth mentioning explicitly. When you pass or return a class by value, that process uses copy initialization.

1
2
3
4
5
Fraction makeNegative(Fraction f) {
// ideally we should do this by const reference
f.setNumerator(-f.getNumerator());
return f;
}

In the above case, both the argument passed by value and the return value can not be elided. However, in other cases, if the argument or return value meet specific criteria, the compiler may opt to elide the copy constructor. For example:

1
2
3
4
5
6
7
8
9
10
class Something {

};

Something foo() {
Something s;
return s;
}

Something s = foo();

In this case, the compiler will probably elide the copy constructor, even though variable s is returned by value.

Convert constructors, explicit, and delete

By default, C++ will treat any constructor as an implicit conversion operator. Consider the following case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public:
// Default constructor
Fraction(int numerator=0, int denominator=1) :
m_numerator(numerator), m_denominator(denominator) {
assert(denominator != 0);
}

Fraction makeNegative(Fraction f) {
f.setNumerator(-f.getNumerator());
return f;
}

int main() {
std::cout << makeNegative(6); // note the integer here
return 0;
}

Although function makeNegative() is expecting a Fraction, we’ve given it the integer literal 6 instead. Because Fraction has a constructor willing to take a single integer, the compiler will implicitly convert the literal 6 into a Fraction object. It does this by copy-initializing makeNegative() parameter f using the Fraction(int, int) constructor.

This implicit conversion works for all kinds of initialization (direct, uniform, and copy).

Constructors eligible to be used for implicit conversions are called converting constructors (or conversion constructors). Prior to C++11, only constructors taking one parameter could be converting constructors. However, with the new uniform initialization syntax in C++11, this restriction was lifted, and constructors taking multiple parameters can now be converting constructors.

The explicit keyword

While doing implicit conversions makes sense in the Fraction case, in other cases, this may be undesirable, or lead to unexpected behaviors:

1
2
3
4
5
6
7
8
9
public:
MyString(int x) { // allocate string of size x
m_string.resize(x);
}
MyString(const char *string) { // allocate string to hold string value
m_string = string;
}
// main
MyString mine = 'x'; // use copy initialization for MyString

In the above example, the user is trying to initialize a string with a char. Because chars are part of the integer family, the compiler will use the converting constructor MyString(int) constructor to implicitly convert the char to a MyString. The program will then print this MyString, to unexpected results.

One way to address this issue is to make constructors (and conversion functions) explicit via the explicit keyword, which is placed in front of the constructor’s name. Constructors and conversion functions made explicit will not be used for implicit conversions or copy initialization:

1
2
3
4
5
6
7
public:
explicit MyString(int x) {
m_string.resize(x);
}

// main
MyString mine = 'x'; // compile error, since MyString(int) is now explicit and nothing will match this

However, note that making a constructor explicit only prevents implicit conversions. Explicit conversions (via casting) are still allowed:

1
2
std::cout << static_cast<MyString>(5);
// Allowed: explicit cast of 5 to MyString(int)

Direct or uniform initialization will also still convert parameters to match (uniform initialization will not do narrowing conversions, but it will happily do other types of conversions).

I am losing myself! Complicated!

1
2
MyString str('x');
// Allowed: initialization parameters may still be implicitly converted to match

Rule: Consider making your constructors and user-defined conversion member functions explicit to prevent implicit conversion errors.

In C++11, the explicit keyword can also be used with conversion operators.

The delete keyword

In our MyString case, we really want to completely disallow ‘x’ from being converted to a MyString (whether implicit or explicit, since the results aren’t going to be intuitive). One way to partially do this is to add a MyString(char) constructor, and make it private:

1
2
3
4
private:
MyString(char) {
/* objects of type MyString(char) can't be constructed from outside the class */
}

However, this constructor can still be used from inside the class (private access only prevents non-members from calling this function).

A better way to resolve the issue is to use the “delete” keyword (introduced in C++11) to delete the function:

1
2
3
4
5
public:
myString(char) = delete;
/* any use of this constructor is an error */

MyString mine('x'); // compile error, since MyString(char) is deleted

When a function has been deleted, any use of that function is considered a compile error.

Note that the copy constructor and overloaded operators may also be deleted in order to prevent those functions from being used.

Overload the assignment operator

Assignment vs Copy constructor

The purpose of the copy constructor and the assignment operator are almost equivalent — both copy one object to another. However, the copy constructor initializes new objects, whereas the assignment operator replaces the contents of existing objects.

The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

Overloading the assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public:
// Copy constructor
Fraction(const Fraction &copy)
: m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) {
// foo
}
// Overloaded assignment
Fraction& operator= (const Fraction &fraction) {
// do the copy
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;
return *this; // support chained assignment
}
// main
Fraction f1(5, 3);
Fraction f2(7, 2);
Fraction f3(9, 5);
f1 = f2 = f3; // chained assignment

Issues due to self-assignment

1
2
3
4
5
6
7
int main() {
Fraction f1(5,3);
f1 = f1; // self assignment
/* this will call f1.operator=(f1) */

return 0;
}

Issue Example skipped: link

Solution: detecting and handling self-assignment

1
2
3
4
5
6
7
8
9
10
11
12
13
// A better implementation of operator=
Fraction& Fraction::operator= (const Fraction &fraction) {
// self-assignment guard
if (this == &fraction)
return *this;

// do the copy
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;

// return the existing object so we can chain this operator
return *this;
}

By checking if our implicit object is the same as the one being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.

Note that there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.

Default assignment operator

Unlike other operators, the compiler will provide a default public assignment operator for your class if you do not provide one. This assignment operator does memberwise assignment (which is essentially the same as the memberwise initialization that default copy constructors do).

Shallow vs. deep copying

Because C++ does not know much about your class, the default copy constructor and default assignment operators it provides use a copying method known as a memberwise copy (also known as a shallow copy). This means that C++ copies each member of the class individually (using the assignment operator for overloaded operator=, and direct initialization for the copy constructor). When classes are simple (e.g. do not contain any dynamically allocated memory), this works very well.

However, when designing classes that handle dynamically allocated memory, memberwise (shallow) copying can get us in a lot of trouble! This is because shallow copies of a pointer just copy the address of the pointer — it does not allocate any memory or copy the contents being pointed to!

1
2
3
4
5
class MyString {
private:
char *m_data;
int m_length;
}

If we do not provide specific constructors, C++ will provide a default copy constructor and default assignment operator that do a shallow copy. The copy constructor will look something like this:

1
2
3
4
5
6
7
MyString::MyString(const MyString &source)
: m_length(source.m_length), m_data(source.m_data) {
// foo
}
~MyString() {
delete[] m_data;
}

Note that m_data is just a shallow pointer copy of source.m_data, meaning they now both point to the same thing.

1
2
3
4
5
6
7
8
9
10
int main() {
MyString hello("Hello, world!");
{
MyString copy = hello; // use default copy constructor
} // copy is a local variable, so it gets destroyed here. The destructor deletes copy's string, which leaves hello with a dangling pointer

std::cout << hello.getString() << "\n"; // this will have undefined behavior

return 0;
}

Deep copying

One answer to this problem is to do a deep copy on any non-null pointers being copied. A deep copy allocates memory for the copy and then copies the actual value, so that the copy lives in distinct memory from the source. This way, the copy and source are distinct and will not affect each other in any way. Doing deep copies requires that we write our own copy constructors and overloaded assignment operators.

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
// Copy constructor
MyString::MyString(const MyString& source) {
// because m_length is not a pointer, we can shallow copy it
m_length = source.m_length;

// m_data is a pointer, so we need to deep copy it if it is non-null
if (source.m_data) {
// allocate memory for our copy
m_data = new char[m_length];
// do the copy
for (int i=0; i < m_length; ++i)
m_data[i] = source.m_data[i];
} else
m_data = 0;
}

// Assignment operator
MyString& MyString::operator= (const MyString & source) {
// check for self-assignment - difference
if (this == &source)
return *this;

// explicitly delete the original value - difference
// first we need to deallocate any value that this string is holding!
delete[] m_data;

// because m_length is not a pointer, we can shallow copy it
m_length = source.m_length;

// m_data is a pointer, so we need to deep copy it if it is non-null
if (source.m_data) {
// allocate memory for our copy
m_data = new char[m_length];

// do the copy
for (int i=0; i < m_length; ++i)
m_data[i] = source.m_data[i];
}
else
m_data = 0;

return *this; // - difference
}

A better solution

Classes in the standard library that deal with dynamic memory, such as std::string and std::vector, handle all of their memory management, and have overloaded copy constructors and assignment operators that do proper deep copying. So instead of doing your own memory management, you can just initialize or assign them like normal fundamental variables! That makes these classes simpler to use, less error-prone, and you don’t have to spend time writing your own overloaded functions!

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 define 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 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
  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
  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) {
}

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;
}
};

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 that 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 the class the member belongs to, 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.

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.

Private inheritance

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

Note that 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.

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 name of the base class and two colons). 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
// Base
protected:
void printValue() { std::cout << m_value; }
// Derived
public: // changing it to public
using Base::printValue; // note: no parenthesis here
// okay:
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 make a public member 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, 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: 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.

0%