A Crash Course in C++: Classes
Putting the ++ in C++
Audience
This article is aimed at engineers looking for a quick overview of classes in C++. It will require a good understanding of the very basics of the language, which you can acquire by reading my other articles here and here.
To get the most from the article you will need a solid understanding of at least one other language. It will help if it’s compiled, but it’s not completely necessary as long as you understand the concepts.
Within the article we will cover core concepts including class declaration, use, inheritance, access modifiers, constructors, destructors, polymorphism and virtual
.
Argument
Header and implementation files
To use a class we first need to declare it. Typically in C++ we separate declaration and implementation into two different files. Declarations are in header files (commonly a .h
extension), and implementations are in C++ files (.cpp
).
A simplified justification is that compilation in C++ goes through two stages:
- C++ files are compiled independently of each other into object files.
- All of the object files are linked together to create an executable.
Let’s say we have two C++
files, x.cpp
and y.cpp
. x
uses a method defined in y
. However, in stage 1 of the compilation, x
has no idea y
exists! We need to let it know which methods y
has for compilation to run.
This is the role of the header file. We can include the declarations, but not the definitions. This provides the necessary information in a lightweight way.
Simple class definitions
Now onto defining our first class. We start with the header file, which outlines what the class will contain.
We’re going to assume you’ve worked with classes before, so will focus on (what I consider to be) the most interesting points.
Initially, we have the #pragma once
preprocessor directive. This is comparable to header guards and is designed for the case where a definition in a header file is included more than once, causing a compilation error. It ensures we only get one definition, and there’s no conflicts.
The other thing to note is the destructor. This can contain things like releasing memory which will be unused after object destruction.
We move onto the definition of the class.
Again, let’s focus on the most notable components.
The first is the constructor, which takes in a list of variables to be initialised within the object. This uses a member initializer list, which replaces the slightly clumsy syntax you might see in languages like Java, where we individually set each field in the constructor body.
We also implement the print method we declared in the header. Notice the Class::method
syntax.
Let’s put this together and create an instance of the class.
Easy! Let’s spice things up with some object-oriented concepts, starting with inheritance.
In the above we introduce the API for our base class, A
. Let’s give it a simple implementation.
Multiple Inheritance
C++ is interesting in that it supports multiple inheritance, i.e. we can extend more than one class. Let’s introduce another base class.
We need to tie all of this together in our derived class.
For anyone familiar with object-orientation this should be pretty straightforward (syntax aside).
We won’t talk about the situation where base classes come into conflict with each other (e.g. if base class A
and base class B
both defined the same method). You can read about how some ambiguities are resolved elsewhere.
Inheritance access
Another interesting facet of inheritance in C++ is access.
- Public inheritance (default for structs): Public and protected members of a base class remain public and protected in the derived class.
- Protected inheritance: Public and protected members of a base class become protected in the derived class.
- Private inheritance (default for classes): Public and protected members of a base class become private in the derived class. Even though this is the default, generally it’s better to use public!
As a quick reminder, the derived class will never have access to the private member variables. How we specify access is below.
The virtual
keyword
Next, let’s bring in the virtual
keyword. First, let’s explain how it allows for runtime polymorphism.
Let’s say I have a class Shape
, which I would like Square
to inherit from. Shape
defines a printName
function, which I would like Square
to override. However, I would still like to assign Square
to Shape
variables and receive the Square
implementation when I call printName
.
By using the virtual
keyword we can achieve this behaviour.
The other use of the virtual keyword is to address the ‘diamond pattern’ we can encounter with multi-inheritance.
Imagine we have a class X
. X
is inherited by two classes, B
and C
, and A
inherits both. There are two ways this can function, demonstrated visually below.
In the first instance we have two copies of the X
class, each with their own data. This can become confusing when you want to access X
‘s data from A
. Which version do you get?
Additionally, you can convert a pointer to A
to a pointer to B
or C
, but not X
. This is as we don’t know which of the two X
instances to point to!
In the second case you share the X
instance between B
and C
meaning you only have one copy of it. No need for disambiguation!
Let’s concrete this idea with the below code.
When using virtual functions in reality there are two good rules to follow:
- ‘A base class destructor should be either public and virtual, or protected and non-virtual’
- ‘A class with a virtual function should have a virtual or protected destructor’
Both of these centre on the fact that if we have a derived class, we usually access it via a pointer to the base class. Without a virtual destructor, our derived class might allocate memory, only the base class destructor would be called when the object goes out of scope, and we would have a memory leak.
By having a virtual destructor we force our derived class to define how it wants to destroy itself, (hopefully) preventing the memory leak. The alternative is stopping the object being destructed through a base class pointer.
Friend functions and forward declarations
We have two types of ‘friend’ data: friend classes and friend functions.
- Friend classes are allowed to access and alter the protected and private data of another class without inheriting from it.
- Friend functions are functions with the same capabilities.
In order to demonstrate these concepts, we also needed to introduce ‘forward declarations’.
In this case, they’re used to let other classes know that SimpleClass
exists. To understand this let’s think about SimpleClass
and FriendFunctionClass
.
The SimpleClass
header needs to know FriendFunctionClass
exists. This is to let the compiler know one of its methods is allowed access to its data.
The FriendFunctionClass
header needs to know SimpleClass
exists in order to declare the print
function, which takes SimpleClass
as an argument.
We have a cyclic dependency, which we need to break.
In order to do this we can provide a forward declaration to the FriendFunctionClass
header to say ‘SimpleClass
does exist, but you don’t need to know anything about its contents’. This is enough to satisfy the compiler.
Then, when we do need to know about the contents (i.e. in the implementation of FriendFunctionClass
), we can include the ‘true’ header file.
Templates
Note: templates seems a very rich area. As this is only a crash course, we will scratch the surface, but won’t dive into details.
Templates are used to generalise code over data types. For example, we may have a List
class that can be used on lots of different types (a list of integers, a list of string etc.). We don’t want to write a different List
class each time we introduce a new one, so we would employ templates.
There are function templates, and class templates, both best demonstrated by example.
We’ve introduced a new keyword: typename
. This specifies the following identifier is a type. Here we use it to say A
, B
and T
are generic types, however, there are more involved usages beyond the scope of this piece.
Move and copy constructors
I have found these to be the most confusing part of this article. Let’s kick off with copy constructors, which can be called with the below syntax.
CopyAndMoveClass class_to_copy(“Initial value”);
CopyAndMoveClass copied_class = class_to_copy; // < Copy constructor called!
Here we are constructing copied_class
by copying the contents of class_to_copy
. In a lot of languages we do this without thinking, and C++ generally lets us do the same by generating default copy constructors. Only in certain circumstances (covered soon) do we define our own.
There are other cases when the copy constructor is used. When you pass an object by value to a function:
void func(A a) {
…
func(a); // < Copy constructor called to create a copy to pass to function
Then when an object is returned by value from a function.
A func() {
A a;
return a;
}
We don’t do anything particularly special in order to use a copy constructor, so it’s important to understand when they are applied (otherwise you’ll have angry C++ experts shouting in your code reviews, says the voice of experience).
Move constructors can be used with the below syntax:
CopyAndMoveClass class_to_move("Initial value");
CopyAndMoveClass moved_class = std::move(class_to_move);
Here we are constructing moved_class
by moving the contents of class_to_move
into it. Move constructors are optional and, unless you specify one, the copy constructor will be called instead.
With a move constructor we are usually concerned with transferring ownership of some data. For example, let’s say we have a pointer to an object as part of a class. When we move-construct class A
from class B
, we want class A
to have a pointer to the existing data class B
used to point to (emphasis on the used).
Contrast this to a copy constructor, which would make a full duplicate of the underlying data, and create a new pointer.
Let’s use a diagram to explain.
Some complimentary concepts are copy and move assignment operators. We call them as below.
CopyAndMoveClass class_to_copy("Initial value");
CopyAndMoveClass copied_class;
copied_class = class_to_copy; // < Copy assignment called
CopyAndMoveClass class_to_move("Initial value");
CopyAndMoveClass moved_class;
moved_class = std::move(class_to_move); // < Move assignment called
They are for cases when we don’t necessarily want to construct the object straight away.
We’ll outline when and why we need any of these concepts in the 0/3/5 rule section. For now, let’s code up an example.
The rule of 0/3/5
- The rule of 0: Try and avoid defining destructors, copy constructors, copy assignment, move constructors, move assignment. This is in the C++ core guidelines.
- The rule of 5: If you need one of them, you probably need them all of them.
- The rule of 3: From days before move constructor and move assignment (so you only needed destructor, copy constructor and copy assignment).
This mainly applies when you have a user-defined type responsible for managing a resource where the handle doesn’t destroy the object, or make deep copies.
You need the user-defined constructor and destructor in order to allocate and free memory for the resource your class owns.
You need the copy constructor and assignment as under-the-hood, C++ often copies or copy assigns objects (as we saw previously), and if you don’t define the correct way to do this, then it may make unexpected shallow copies.
If you define custom copy constructor and assignment you also need custom move constructors and assignment. An interesting discussion of why is here, and is expanded on in the ‘special member functions’ section.
We can apply this to our example of using copy and move functions. We allocated space for our pointer using new
, which means we need to make sure we delete
at the end (i.e. in the destructor).
We should write copy constructors and assignment to make sure the pointer and data is being duplicated correctly (and we’re not just making a copy of the pointer), which in turn necessitates us writing the move functionality.
Special member functions
Let’s cap off the article with some useful nomenclature. Special member functions are automatically generated by the compiler if used, without being declared explicitly by the programmer. They cover a lot of the things we’ve discussed so far:
- Default constructor
- Copy constructor
- Move constructor
- Copy assignment
- Move assignment
- Destructor
There are cases when declaring one prevents the default generation of others, which are helpfully listed here. This is part of the reason in the rule of 5, we need to define the move functionality as well as the copy one.
These are useful things to know, as they can be the cause of lots of compiler complaints!
Conclusion
In conclusion, we’ve whizzed through a lot of the C++ class fundamentals from the basics, to the more complex:
- Splitting code between header and implementation files
- Multiple inheritance
- Inheritance access
- Virtual
- Friend functions and classes
- Templates
- Move and copy constructors
I hope it’s been useful!