Introduction
Classes are the fundamental building blocks of any OOP program.
C++, a comparatively low-level language, provides a lot of features for OOP.
On the other hand, some higer-level languages such as Java and Python go for a more simplistic approach, leaving you out in the dark about some advanced aspects.
This is usually advertised as a good thing: the programmer won't have to break their head over copy constructors, move semantics, member initialiser lists, and all other C++ OOP pecularities.
However, these features all have their purpose, as all features in C++ do, which often boils down to speed and perfomance gains.
Studying the powerful tools C++ offers makes you excel in the language and makes you a better programmer in general.
Because of the low-level nature you will learn a lot from looking at what's under the hood and how things work exactly.
In this blog you will learn how classes work in C++ and how to deal with them.
I will go through examples of some important advanced features and techniques, which are often blissfully ignored by C++ newbies and even some intermediate programmers, yet are the foundation of good C++ OOP.
Dusting off that class
keyword
Classes are used in OOP languages to group data into a single package.
We will create a simple class that represents a cat in C++.
This should look somewhat familiar if you have already worked with classes in other programming languages.
1
class Cat
2
{
3
private:
4
5
// Fields that hold information about this cat
6
7
std::string name;
8
int age;
9
10
11
public:
12
13
// Constructor (creates a Cat instance)
14
15
Cat(const std::string& name, int age)
16
{
17
this->name = name;
18
this->age = age;
19
}
20
21
// Print information about this cat
22
23
void print()
24
{
25
std::cout << "Cat " << name << " is " << age << " years old.\n";
26
}
27
};
Just like in other OOP languages, classes consist of fields and methods to hold information and define functionality, respectively.
In our example, we have two fields: name
and age
.
These fields characterise the instance we are dealing with.
We also define two methods: Cat
and print
.
The first method is a special one: it is a constructor.
Note that the name of this method matches the name of our class.
Also note that a class constructor in C++ does not name a return type, which we will come back to later.
The second method we defined is a normal method, which displays information about a cat.
In C++, we use labels inside classes to indicate from where we are allowed to access the class fields and methods.
In this example, we put both fields in a private
context, which means that we can only use the fields within the class declaration.
This is useful, because we don't want anyone from outside to be able to modify the class fields.
To the contrary, our methods are public
.
We want to be able to use these methods, of course.
Interacting with classes
We now created a class declaration: a template for a class instance. So, what are we waiting for? Let's get ourselves a cat!
1
Cat first_cat = Cat("Nala", 3);
On the right-hand side of our variable declaration, we call the constructor of Cat
.
There is actually a shorthand way of writing this:
1
Cat first_cat("Nala", 3);
Congrats, we created a cat. Now let's interact with it.
1
first_cat.print();
This should output the following:
1
Cat Nala is 3 years old.
C++ magic: implicit constructors
Besides the constructor we created, our cat class also has two implicit constructors: the copy constructor and the move constructor.
We get them for free by the C++ compiler. We don't have to specify them.
The copy constructor is used, as you might have guessed, to construct a copy of an instance.
1
Cat cloned_cat(first_cat);
In C++, we are allowed to overload functions and methods.
In simple terms this means that we can create multiple functions with the same name, but different parameters.
Our cat constructor takes two parameters: a string for the name and an integer for the age.
The copy constructor takes one parameter: the instance we want to make a clone of.
We can also call the copy constructor as follows:
1
Cat cloned_cat = first_cat;
Both code snippets are equivalent, but the latter might feel more intuitive.
I will discuss move constructors later on.
Passing classes through functions
We often have to connect classes together in Object Oriented Programming. Sometimes we must pass a class instance through a function or a method. Let's create a method in our cat class to update the name.
1
void change_name(std::string new_name)
2
{
3
name = new_name;
4
}
I often see beginners writing code like this.
The above code will work as intended, but is unoptimised.
Note that std::string
is also a class.
However, it is not written by us, as it comes out of the box in the C++ standard library.
If you pass a class through a method in this way, the C++ compiler will allocate space for it.
Then the copy constructor is implicitly called, cloning the string we pass in into the allocated space for new_name
.
Next, the method body is executed, and the copy assignment operator for name
is called.
This copies new_name
into name
.
After we are all set, we have copied our input string twice.
Wouldn't it be more optimal if we only copied it once?
The answer is obviously yes, and it is quite simple to do this in C++.
1
void change_name(const std::string& new_name)
2
{
3
name = new_name;
4
}
The above code block functions the same as the previous code, but the string we pass in is copied only once: directly from where we got it from into the name
.
The only thing we had to change was to change std::string
to const std::string&
.
Note the &
at the end.
It indicates a reference.
Together with the const
, this is a constant reference.
References in C++ differ from normal variables, as they refer to another variable, instead of being an actual variable.
We make it constant, because we don't want or need the value it refers to to be mutated.
Getting our hands dirty: class constructors
We have briefly touched constructors already, but there is more to it.
Default constructors
Sometimes we want to create an instance, but leave it unititialised and initialise it later on. We can only do this if our class has a default constructor. A default constructor is just a constructor, but without any parameters. Let's make a default constructor for our cat class.
1
Cat()
2
{
3
name = "Unnamed cat";
4
age = 0;
5
}
We can now create an uninitialised cat. Note that there are no parameters and we can skip the parentheses.
1
Cat uninitialised_cat;
We declare a cat instance, but don't assign it a value.
Note that this is only possible because our cat class has a default constructor.
If a class does not have a default constructor, we cannot create an uninitialised instance.
We could use our change_name
method to change the cat's name later on.
1
uninitialised_cat.change_name("Simba");
It might be a good idea to create another method that we can use to change the age.
The implementation of this method is left as an exercise for the reader.
The default constructor we wrote actually does initialise the name and the age of the cat, which we might not even need to do.
We could instead create a default constructor that does not initialise anything:
1
Cat() {}
This is often completely reasonable, but we will run into UB if we tried to consume the class fields before they are initialised. For example, the following code, using the above default constructor, will behave oddly:
1
Cat uninitialised_cat;
2
uninitialised_cat.print();
Let's look at the output I got.
1
Cat is 1609080832 years old.
Well, that's odd. We never told the cat to be 1609080832 years old. This cat must have more than nine lives! Let's run this again, there must be a glitch in the matrix somewhere, right?
1
Cat is -1855227680 years old.
Oh well, that doesn't look right either.
Before you declare C++ to be some evil black magic programming language, let me explain what is going on.
First, let's look at the cat's name in the output.
It is empty in both cases, which we can explain by the fact that std::string
has a default constructor that initialises the string to be empty.
So, the cat's name is actually initialised.
The cat's age, however, is not initialised.
int
is not a class and therefore doesn't have a default constructor.
This is UB and when we get in such a situation in C++, anything could happen.
Yes, our computer might even spawn a bunch of demons that will haunt us.
Glad that didn't happen when I to ran the code.
Empty default constructors are completely safe to use, as long as you remember not to consume the data of the instance before initialising it.
When we are dealing with millions of class instances, it might be a very good idea from a performance perspective not to initialise each instance until we actually need to.
Member initialiser lists
Within a class constructor, we can use a handy shorthand notation to initialise members:
1
Cat(const std::string& name, int age)
2
: name(name), age(age) {}
Compared with the previous constructor we made, this is shorter.
We can omit the this
keyword completely, and instead put our initisations in-order in a comma seperated list.
When we have a class with a field that is a class without a default constructor, we must use a member initialiser list, because C++ does not allow uninitialised class instances:
1
class Point
2
{
3
private:
4
5
int x;
6
int y;
7
8
public:
9
10
// The constructor takes a coordinate
11
// and stores them into x and y
12
13
Point(int x, int y) : x(x), y(y) {}
14
};
15
16
17
class Line
18
{
19
private:
20
21
Point from;
22
Point to;
23
24
public:
25
26
Line(int x_from, int y_from, int x_to, int y_to)
27
: from(x_from, x_to), to(x_to, y_to) {}
28
};
Here, we use a member initialiser list in the constructor of the Point
class to initialise the x and y value.
In the Line
class we use a member initialiser list to call the Point
constructor on the from
and to
fields.
References must also be initialised at the construction of a class. C++ does not allow uninitialised references.
Destructors
When a class instance goes out of scope or a pointer to a class instance, its destructor is called. The destructor is supposed to clean up all the mess the class has created in its lifetime.
1
class Population
2
{
3
private:
4
5
std::thread threads[16];
6
std::vector<Human *> humans;
7
8
9
public:
10
11
Population() { ... }
12
void add_human(Human *human) { ... }
13
void run_simulation() { ... }
14
15
// Stops the threads and deletes the humans
16
17
~Population()
18
{
19
for (std::thread& thr : threads)
20
{
21
thr.join();
22
}
23
24
for (Human *human : humans)
25
{
26
// Calls the destructor on the human instance
27
28
delete human;
29
}
30
}
31
};
32
33
34
int main()
35
{
36
Population population;
37
38
for ( ... )
39
{
40
population.add_human( ... );
41
}
42
43
population.run_simulation();
44
45
// The destructor of Population is called here at the end of scope
46
}
In C++, destructors are denoted with a tilde and the class name, which is ~Population()
in the above example.
When the simulation finishes, the destructor of the population class is called, which stops the threads and calls the destructor on each human, which cleans up the humans.
Keeping it simple: operator overloading
In C++ we are allowed to overload functions and methods as well as operators. This allows us to write very clear-cut code. We can almost overload every normal C++ operator. Let's write a class for a complex number to demonstrate operator overloading in its full glory.
1
class ComplexNumber
2
{
3
private:
4
5
double re;
6
double im;
7
8
9
public:
10
11
ComplexNumber(double re, double im)
12
: re(re), im(im) {}
13
14
void print()
15
{
16
std::cout << re << " + " << im << "i" << std::endl;
17
}
18
19
// ( a + bi ) + ( c + di ) = ( a + c ) + i * ( b + d )
20
21
ComplexNumber operator+(const ComplexNumber& other)
22
{
23
return ComplexNumber(re + other.re, im + other.im);
24
}
25
26
// ( a + bi ) - ( c + di ) = ( a - c ) + i * ( b - d )
27
28
ComplexNumber operator-(const ComplexNumber& other)
29
{
30
return ComplexNumber(re - other.re, im - other.im);
31
}
32
33
// ( a + bi ) * ( c + di ) = ( ac - db ) + i * ( ad - bc )
34
35
ComplexNumber operator*(const ComplexNumber& other)
36
{
37
double new_re = re * other.re - im * other.im;
38
double new_im = re * other.im + im * other.re;
39
40
return ComplexNumber(new_re, new_im);
41
}
42
43
};
44
45
46
int main()
47
{
48
ComplexNumber complex_1(1, 2);
49
ComplexNumber complex_2(-3, 1.5);
50
ComplexNumber complex_3(0, -5);
51
52
ComplexNumber result = complex_1 * complex_2 - complex_3;
53
54
result.print();
55
}
In this example we overloaded the plus, minus and multiplication operator.
Overloaded operator are just class methods or functions, with operator
and the operator symbol as their name.
We can now use the corresponding +
, -
and *
operators on instances of our complex number class.
When we use these operators, the methods for the operators we wrote will be executed.
Standard operator precedence applies.
Next to the mathematical operators, we can also overload other operators.
Some example interesting operators to overload are =
, []
.
Next to the assignment operator, we can also overload compound assignment mathematical operators (+=
, -=
, *=
, etc.).
Let's look at an example where we overload some special operators in weird ways.
1
class Printer
2
{
3
public:
4
5
void operator[](const std::string& str)
6
{
7
std::cout << str << std::endl;
8
}
9
};
10
11
12
int main()
13
{
14
Printer printer;
15
printer["Hello, World!"];
16
}
Yup, this is a hello world program.
We overloaded the array index operator, which is normally use to retrieve something from a linear data structure, to print something.
Some examples of standard library classes that have an overloaded array index operator include std::vector
and std::string
.
We can use it on vectors to get a reference to the n-th element of the vector and we can use it on strings to get the n-th character of the string.
std::unordered_map
implements the array index operator in an interesting way:
1
std::unordered_map<std::string, std::string> phonebook;
2
3
phonebook["John"] = "+44 7700 900077;
4
phonebook["Chantal"] = "+1 202 555 0126";
5
phonebook["Abby"] = "+61 1900 654 321";
6
phonebook["Clayton"] = "+65 4776 5921";
7
8
std::cout << phonebook["John"];
The assignment operator is also very useful. It can be used to copy values from an instance into another existing instance. It is often used in a similar way as the copy constructor, but the assignment operator is called when the value of an existing instance changes.
1
class Point
2
{
3
private:
4
5
int x;
6
int y;
7
8
9
public:
10
11
Point(int x, int y) : x(x), y(y) {}
12
13
Point& operator=(const Point& other)
14
{
15
x = other.x;
16
y = other.y;
17
18
return *this;
19
}
20
};
21
22
23
int main()
24
{
25
Point point_1(1, 2);
26
27
// Copy point_1 into point_2
28
// The copy constructor will be called because this is a declaration
29
30
Point point_2 = point_1;
31
32
// Copy point_1 into point_2
33
// The assignment operator will be called because this is an assignment
34
35
point_2 = point_1;
36
}
Note the return type and value of the assignment operator. We do this because in C++, as well as other languages, the assignment operator assigns a value to something, but it also resolves into the new value. This makes the following also valid:
1
Point point_3 = (point_2 = point_1);
This code first copies the value of point_1
into point_2
.
Then, point_3
will be constructed by copying the new value stored in point_2
.
After this line of code, all three points hold the same data.
This brings us to the next operators that are useful to overload: the equality operators.
Let's add them to the point class.
1
bool operator==(const Point& other)
2
{
3
return x == other.x && y == other.y;
4
}
5
6
bool operator!=(const Point& other)
7
{
8
return x != other.x && y != other.y;
9
}
We can now confirm that all three points hold the same value:
1
if (point_1 == point_2 && point_2 == point_3) { ... }
For an extensive article on operator overloading, see the C++ reference.
One to rule them all: templates
Welcome to the C++ feature that is both the best and worst thing ever. Yes, we're talking templates. Templates themselves are amazing, it's just the error messages you might get that are horrible and a pain to read. The most important thing is not to panic when you receive a long error like this (scroll it all the way down):
1
main.cpp: In function ‘int main()’:
2
main.cpp:14:28: error: use of deleted function ‘SomeClass::SomeClass()’
3
14 | SomeClass instance;
4
| ^~~~~~~~
5
main.cpp:4:7: note: ‘SomeClass::SomeClass()’ is implicitly deleted because the default definition would be ill-formed:
6
4 | class SomeClass
7
| ^~~~~~~~~
8
main.cpp:4:7: error: use of deleted function ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map() [with _Key = SomeOtherClass; _Tp = SomeOtherClass; _Hash = std::hash; _Pred = std::equal_to; _Alloc = std::allocator >]’
9
In file included from /usr/include/c++/9/unordered_map:47,
10
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
11
from main.cpp:1:
12
/usr/include/c++/9/bits/unordered_map.h:141:7: note: ‘std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::unordered_map() [with _Key = SomeOtherClass; _Tp = SomeOtherClass; _Hash = std::hash; _Pred = std::equal_to; _Alloc = std::allocator >]’ is implicitly deleted because the default definition would be ill-formed:
13
141 | unordered_map() = default;
14
| ^~~~~~~~~~~~~
15
/usr/include/c++/9/bits/unordered_map.h:141:7: error: use of deleted function ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>::_Hashtable() [with _Key = SomeOtherClass; _Value = std::pair; _Alloc = std::allocator >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing; _Hash = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits]’
16
In file included from /usr/include/c++/9/unordered_map:46,
17
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
18
from main.cpp:1:
19
/usr/include/c++/9/bits/hashtable.h:414:7: note: ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>::_Hashtable() [with _Key = SomeOtherClass; _Value = std::pair; _Alloc = std::allocator >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing; _Hash = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits]’ is implicitly deleted because the default definition would be ill-formed:
20
414 | _Hashtable() = default;
21
| ^~~~~~~~~~
22
/usr/include/c++/9/bits/hashtable.h:414:7: error: use of deleted function ‘std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, _Hash, _Traits>::_Hashtable_base() [with _Key = SomeOtherClass; _Value = std::pair; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing; _Hash = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits]’
23
In file included from /usr/include/c++/9/bits/hashtable.h:35,
24
from /usr/include/c++/9/unordered_map:46,
25
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
26
from main.cpp:1:
27
/usr/include/c++/9/bits/hashtable_policy.h:1822:5: note: ‘std::__detail::_Hashtable_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, _Hash, _Traits>::_Hashtable_base() [with _Key = SomeOtherClass; _Value = std::pair; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing; _Hash = std::__detail::_Default_ranged_hash; _Traits = std::__detail::_Hashtable_traits]’ is implicitly deleted because the default definition would be ill-formed:
28
1822 | _Hashtable_base() = default;
29
| ^~~~~~~~~~~~~~~
30
/usr/include/c++/9/bits/hashtable_policy.h:1822:5: error: use of deleted function ‘std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, std::__detail::_Default_ranged_hash, true>::_Hash_code_base() [with _Key = SomeOtherClass; _Value = std::pair; _ExtractKey = std::__detail::_Select1st; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing]’
31
/usr/include/c++/9/bits/hashtable_policy.h:1373:7: note: ‘std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2, std::__detail::_Default_ranged_hash, true>::_Hash_code_base() [with _Key = SomeOtherClass; _Value = std::pair; _ExtractKey = std::__detail::_Select1st; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing]’ is implicitly deleted because the default definition would be ill-formed:
32
1373 | _Hash_code_base() = default;
33
| ^~~~~~~~~~~~~~~
34
/usr/include/c++/9/bits/hashtable_policy.h:1373:7: error: use of deleted function ‘std::__detail::_Hashtable_ebo_helper<_Nm, _Tp, true>::_Hashtable_ebo_helper() [with int _Nm = 1; _Tp = std::hash]’
35
/usr/include/c++/9/bits/hashtable_policy.h:1096:7: note: ‘std::__detail::_Hashtable_ebo_helper<_Nm, _Tp, true>::_Hashtable_ebo_helper() [with int _Nm = 1; _Tp = std::hash]’ is implicitly deleted because the default definition would be ill-formed:
36
1096 | _Hashtable_ebo_helper() = default;
37
| ^~~~~~~~~~~~~~~~~~~~~
38
/usr/include/c++/9/bits/hashtable_policy.h:1096:7: error: use of deleted function ‘std::hash::hash()’
39
In file included from /usr/include/c++/9/bits/basic_string.h:6719,
40
from /usr/include/c++/9/string:55,
41
from /usr/include/c++/9/bits/locale_classes.h:40,
42
from /usr/include/c++/9/bits/ios_base.h:41,
43
from /usr/include/c++/9/ios:42,
44
from /usr/include/c++/9/istream:38,
45
from /usr/include/c++/9/sstream:38,
46
from /usr/include/c++/9/complex:45,
47
from /usr/include/c++/9/ccomplex:39,
48
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:54,
49
from main.cpp:1:
50
/usr/include/c++/9/bits/functional_hash.h:101:12: note: ‘std::hash::hash()’ is implicitly deleted because the default definition would be ill-formed:
51
101 | struct hash : __hash_enum<_Tp>
52
| ^~~~
53
/usr/include/c++/9/bits/functional_hash.h:101:12: error: no matching function for call to ‘std::__hash_enum::__hash_enum()’
54
/usr/include/c++/9/bits/functional_hash.h:82:7: note: candidate: ‘std::__hash_enum<_Tp, >::__hash_enum(std::__hash_enum<_Tp, >&&) [with _Tp = SomeOtherClass; bool = false]’
55
82 | __hash_enum(__hash_enum&&);
56
| ^~~~~~~~~~~
57
/usr/include/c++/9/bits/functional_hash.h:82:7: note: candidate expects 1 argument, 0 provided
58
/usr/include/c++/9/bits/functional_hash.h:101:12: error: ‘std::__hash_enum<_Tp, >::~__hash_enum() [with _Tp = SomeOtherClass; bool = false]’ is private within this context
59
101 | struct hash : __hash_enum<_Tp>
60
| ^~~~
61
/usr/include/c++/9/bits/functional_hash.h:83:7: note: declared private here
62
83 | ~__hash_enum();
63
| ^
64
In file included from /usr/include/c++/9/bits/hashtable.h:35,
65
from /usr/include/c++/9/unordered_map:46,
66
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
67
from main.cpp:1:
68
/usr/include/c++/9/bits/hashtable_policy.h:1096:7: error: use of deleted function ‘std::hash::~hash()’
69
1096 | _Hashtable_ebo_helper() = default;
70
| ^~~~~~~~~~~~~~~~~~~~~
71
In file included from /usr/include/c++/9/bits/basic_string.h:6719,
72
from /usr/include/c++/9/string:55,
73
from /usr/include/c++/9/bits/locale_classes.h:40,
74
from /usr/include/c++/9/bits/ios_base.h:41,
75
from /usr/include/c++/9/ios:42,
76
from /usr/include/c++/9/istream:38,
77
from /usr/include/c++/9/sstream:38,
78
from /usr/include/c++/9/complex:45,
79
from /usr/include/c++/9/ccomplex:39,
80
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:54,
81
from main.cpp:1:
82
/usr/include/c++/9/bits/functional_hash.h:101:12: note: ‘std::hash::~hash()’ is implicitly deleted because the default definition would be ill-formed:
83
101 | struct hash : __hash_enum<_Tp>
84
| ^~~~
85
/usr/include/c++/9/bits/functional_hash.h:101:12: error: ‘std::__hash_enum<_Tp, >::~__hash_enum() [with _Tp = SomeOtherClass; bool = false]’ is private within this context
86
/usr/include/c++/9/bits/functional_hash.h:83:7: note: declared private here
87
83 | ~__hash_enum();
88
| ^
89
In file included from /usr/include/c++/9/bits/hashtable.h:35,
90
from /usr/include/c++/9/unordered_map:46,
91
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
92
from main.cpp:1:
93
/usr/include/c++/9/bits/hashtable_policy.h:1373:7: error: use of deleted function ‘std::__detail::_Hashtable_ebo_helper<1, std::hash, true>::~_Hashtable_ebo_helper()’
94
1373 | _Hash_code_base() = default;
95
| ^~~~~~~~~~~~~~~
96
/usr/include/c++/9/bits/hashtable_policy.h:1093:12: note: ‘std::__detail::_Hashtable_ebo_helper<1, std::hash, true>::~_Hashtable_ebo_helper()’ is implicitly deleted because the default definition would be ill-formed:
97
1093 | struct _Hashtable_ebo_helper<_Nm, _Tp, true>
98
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
99
/usr/include/c++/9/bits/hashtable_policy.h:1093:12: error: use of deleted function ‘std::hash::~hash()’
100
/usr/include/c++/9/bits/hashtable_policy.h:1822:5: error: use of deleted function ‘std::__detail::_Hash_code_base, std::__detail::_Select1st, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’
101
1822 | _Hashtable_base() = default;
102
| ^~~~~~~~~~~~~~~
103
/usr/include/c++/9/bits/hashtable_policy.h:1346:12: note: ‘std::__detail::_Hash_code_base, std::__detail::_Select1st, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’ is implicitly deleted because the default definition would be ill-formed:
104
1346 | struct _Hash_code_base<_Key, _Value, _ExtractKey, _H1, _H2,
105
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
106
1347 | _Default_ranged_hash, true>
107
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~
108
/usr/include/c++/9/bits/hashtable_policy.h:1346:12: error: use of deleted function ‘std::__detail::_Hashtable_ebo_helper<1, std::hash, true>::~_Hashtable_ebo_helper()’
109
In file included from /usr/include/c++/9/unordered_map:46,
110
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
111
from main.cpp:1:
112
/usr/include/c++/9/bits/hashtable.h:414:7: error: use of deleted function ‘std::__detail::_Hashtable_base, std::__detail::_Select1st, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits >::~_Hashtable_base()’
113
414 | _Hashtable() = default;
114
| ^~~~~~~~~~
115
In file included from /usr/include/c++/9/bits/hashtable.h:35,
116
from /usr/include/c++/9/unordered_map:46,
117
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
118
from main.cpp:1:
119
/usr/include/c++/9/bits/hashtable_policy.h:1770:10: note: ‘std::__detail::_Hashtable_base, std::__detail::_Select1st, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits >::~_Hashtable_base()’ is implicitly deleted because the default definition would be ill-formed:
120
1770 | struct _Hashtable_base
121
| ^~~~~~~~~~~~~~~
122
/usr/include/c++/9/bits/hashtable_policy.h:1770:10: error: use of deleted function ‘std::__detail::_Hash_code_base, std::__detail::_Select1st, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::~_Hash_code_base()’
123
In file included from /usr/include/c++/9/unordered_map:46,
124
from /usr/include/x86_64-linux-gnu/c++/9/bits/stdc++.h:117,
125
from main.cpp:1:
126
/usr/include/c++/9/bits/hashtable.h: In instantiation of ‘std::_Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, _Traits>::~_Hashtable() [with _Key = SomeOtherClass; _Value = std::pair; _Alloc = std::allocator >; _ExtractKey = std::__detail::_Select1st; _Equal = std::equal_to; _H1 = std::hash; _H2 = std::__detail::_Mod_range_hashing; _Hash = std::__detail::_Default_ranged_hash; _RehashPolicy = std::__detail::_Prime_rehash_policy; _Traits = std::__detail::_Hashtable_traits]’:
127
/usr/include/c++/9/bits/unordered_map.h:102:11: required from here
128
/usr/include/c++/9/bits/hashtable.h:1354:5: error: use of deleted function ‘std::__detail::_Hashtable_base, std::__detail::_Select1st, std::equal_to, std::hash, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits >::~_Hashtable_base()’
129
1354 | }
130
| ^
I'm glad you didn't click away out of utter despair. Let's now look at the beauty of templates!
1
template <typename Type>
2
class LinkedListNode
3
{
4
private:
5
6
LinkedListNode *next;
7
Type value;
8
9
10
public:
11
12
LinkedListNode(const Type& value) : value(value), next(NULL) {}
13
14
~LinkedListNode()
15
{
16
if (next != NULL)
17
{
18
delete next;
19
}
20
}
21
22
void append(const Type& value)
23
{
24
if (next == NULL)
25
{
26
next = new LinkedListNode<Type>(value);
27
}
28
else
29
{
30
next->append(value);
31
}
32
}
33
};
We created a simple class LinkedListNode
that has a template parameter Type
.
This allows us to create instances of our linked list node of any type we want!
Let's make a linked list of integers and a linked list of strings.
1
LinkedListNode<int> head_1;
2
LinkedListNode<std::string> head_2;
3
4
head_1.append(1);
5
head_1.append(2);
6
head_1.append(3);
7
8
head_2.append("Hello");
9
head_2.append("World");
Lots of classes in the C++ standard library are templated classes, which allows us to create a bunch of specialised classes to fit all our needs.
Some examples are std::vector
, std::list
, std::map
, std::set
, std::pair
, std::queue
, and the list goes on...
Templates are not just limited to classes.
You can even template functions!
Read the C++ reference for more.
From atoms to molecules and beyond: class hierarchy
A house is made out of walls and a roof.
Walls are made out of bricks.
The roof is made out of roof tiles.
Bricks are made out of sand and clay.
Roof tiles can be made out of multiple materials.
In the end, everything is made out of atoms.
OOP programs also work like this, but they are made out of machine code that is represented by a class hierarchy.
These classes interrelate with each other in an abstract way.
This means classes on top of classes, which means inheritance, and conceptual classes, which means abstract classes.
In C++, we can create abstract classes with functionality that can be later implemented by inherited classes.
Let's look at a simple example:
1
class Animal
2
{
3
private:
4
5
std::string name;
6
int speed;
7
8
9
public:
10
11
Animal(const std::string& name, int speed)
12
: name(name), speed(speed) {}
13
14
virtual void make_sound() = 0;
15
16
void move(int distance)
17
{
18
int time = distance / speed;
19
std::cout << "Took " << time << " seconds" << std::endl;
20
}
21
};
22
23
class Cat : public Animal
24
{
25
public:
26
27
Cat(const std::string& name) : Animal(name, 7) {}
28
29
void meow()
30
{
31
std::cout << "meow" << std::endl;
32
}
33
34
35
void make_sound()
36
{
37
meow();
38
}
39
};
40
41
class Dog : public Animal
42
{
43
public:
44
45
Dog(const std::string& name) : Animal(name, 9) {}
46
47
void bark()
48
{
49
std::cout << "woof" << std::endl;
50
}
51
52
void make_sound()
53
{
54
bark();
55
}
56
};
In the above example, we created a base class Animal
, which we extended into a cat and a dog class.
Our animal class is an abstract class, because it has a virtual
method.
Note the = 0
in the virtual make_sound
method.
This is C++ syntax for specifying a virtual method that does not have an implementation yet.
This means that we cannot create instances of the animal base class, and we must define the make_sound
method in classes we derive from the base animal class.
To denote inheritance in C++, we add a colon after the class name in the declaration and the class it extends.
We also have to express the visibility mode (public
, private
or protected
).
Public and protected members in the base class will become available in the derived class with the selected visibility mode.
Private members in the base class will never be available to the derived class.
In the derived classes, we must call the Animal
constructor, because there is no default Animal
constructor.
We can also use the member initialiser list for this.
Instead of initialising a member by name, we will initialise the base class instance by name.
Classes and pointers to them
In C++, being a low-level language, it is important to understand where your variables and class instances are actually stored. There are two storage places in a typical program: the stack and the heap. All global and local variables and class instances are placed on the stack.
1
int global_1;
2
Cat global_2("Cleo");
3
4
void func()
5
{
6
int local_1;
7
}
8
9
int main()
10
{
11
int local_2;
12
13
if ( ... )
14
{
15
uint64_t local_3;
16
}
17
}
All variables above are stored on the stack.
The machine code of the functions and methods we wrote also lives on the stack.
However, there is a technical limitation to the use of the stack: it is fixed in size.
On a typical modern Linux machine, the stack size is 8MB.
You can check your stack size on your Linux machine with the command $ ulimit -s
.
This command displays the stack size in kB.
If our program has to store a lot of data, we should use the heap.
The heap is basically all memory of our machine that is not in use and not reserved.
Using the heap, we can use all memory available to our device.
When we want to allocate something on the heap in C++, we use the new
operator.
This operator will ask the system for a block of memory of the size we need and returns a pointer to this memory block.
Let's try it out.
1
Cat *cat_1 = new Cat("Cleo");
Declaring a heap-allocated cat is simple.
Note the asterisk symbol (*
) that specifies that cat_1
is a pointer to a cat.
We write the new
operator and then use the cat constructor as we would usually do.
There is no shorthand way of writing this expression.
To make our cat meow, we have to dereference the pointer and then call the meow method:
1
(*cat_1).meow();
Dereferencing is done with another asterisk symbol.
This basically means "get the value cat_1
points to, then call the meow method on it".
As this is a very common operation in C++, there exists a shorthand way of writing the above code by using the arrow operator:
1
cat_1->meow();
The new
operator will allocate a block of memory for us.
When we are done using this block, we must delete
it.
If we don't and we simply stop caring about the block, the memory will still be in use by our program and we have a memory leak.
Eventually, our machine might run out of memory, because the memory we have allocated isn't returned to the operating system.
This is especially concerning when we develop an application that must run for an extended period of time, such as background programs or servers.
Let's look at an example program that leaks memory.
1
// Called when a button is pressed in the lift
2
3
LiftQuery *create_lift_query(int floor_number)
4
{
5
LiftQuery *query = new LiftQuery(floor_number);
6
return query;
7
}
8
9
10
// Called after the doors of the lift have closed
11
12
void move_to_desired_floor(LiftQuery *query)
13
{
14
if (current_floor < query->floor_number)
15
{
16
move_up();
17
}
18
else
19
{
20
move_down();
21
}
22
}
23
24
25
// Called when the lift arrives at the desired floor
26
27
void on_arrival(LiftQuery *query)
28
{
29
// Here, we forget to delete the query
30
// Congrats, we leaked memory!
31
}
In this program, we created some code for a lift, but we leak memory every time we arrive at the desired floor.
Let's say that the embedded system in the lift has a 4MB memory chip, and an instance of the LiftQuery
class is 128 bytes in size.
Then, after 4 * 1024 * 1024 / 128 = 32768
arrivals, we have consumed the entire memory chip and the lift program will crash because there is no more memory available to allocate another LiftQuery
.
Next time you are stuck in a lift, you know you can blame some programmer.
Now, how do we return unneeded memory back to the operating system correctly?
1
Cat *cat_1 = new Cat("Cleo");
2
3
// Do something with the cat...
4
5
delete cat_1;
new
instance, always make sure you delete
it when it's not needed anymore.
Missing deletions cause memory leaks.
Pointers to classes can also be very useful when we're dealing with abstract classes. We cannot construct abstract classes, but we can have pointers to them. Let's demonstrate that in an example of racing animals.
1
class RacingAnimal
2
{
3
private:
4
5
// Racing animals will get a set advantage
6
// over other racing animals
7
8
int head_start;
9
10
11
public:
12
13
Animal *animal;
14
15
RacingAnimal(Animal *animal, int head_start)
16
: animal(animal), head_start(head_start) {}
17
18
int race(int distance) const
19
{
20
int remaining_distance = distance - head_start;
21
int time_taken = remaining_distance / animal->speed;
22
23
return time_taken;
24
}
25
};
26
27
28
int main()
29
{
30
std::vector<RacingAnimal> racing_animals;
31
int race_track_distance = 250;
32
33
// Add some animals to the list
34
35
racing_animals.push_back( RacingAnimal(new Cat("George"), 100) );
36
racing_animals.push_back( RacingAnimal(new Dog("Belle"), 50) );
37
racing_animals.push_back( RacingAnimal(new Bear("Bruno"), 0) );
38
39
// Let all animals race
40
41
for (const RacingAnimal& racing_animal : racing_animals)
42
{
43
const std::string& name = racing_animal.animal->name;
44
int seconds_taken = racing_animal.race(race_track_distance);
45
46
std::cout << name << " took " <<
47
seconds_taken << " seconds" << std::endl;
48
49
// And delete them when we don't need them anymore
50
51
delete racing_animal.animal;
52
}
53
}
In the above example, we created a class to represent a racing animal.
This class has a field animal
that holds a pointer to an actual animal.
We are allowed to pass in any type of animal into this pointer.
On line 35-37 we create a racing cat, dog and bear.
We allocate the animals on the heap, using the new
operator.
The animals will race and we delete
the animal instances when we don't need them anymore.
The output of this program is:
1
George took 21 seconds
2
Belle took 22 seconds
3
Bruno took 20 seconds
Seems like Bruno the bear won the race!
Final thoughts
With reading this tour of C++ classes, I hope you have learnt about some of the powerful tools C++ offers. Now it is your turn: build something cool to apply your new knowledge!