Introduction to modern C++
C++ is a programming language often used when working with embedded systems, such as computers integrated into vehicles and robots. The reason why C++ is popular for these applications is that it allows the programmer to both get close to the hardware of the computer, such as the CPU and memory, and at the same time provide good possibilities for high-level language features such as objects and lambda functions.
Modern C++ means version 11 or later, and here version 17 is usually used. The language C++ is not released by a company, instead it is defined as an international standard. Then, from the standard, different code compilers are created, where many of them are free and open source. When looking for help online, just make sure to search for answers using modern revisions of C++, such as versions 11, 14, 17, or 20.
Getting started with C++ in Linux
Now it is time to open a new terminal on your computer to be able to start working with C++ in Linux. When you have opened your new terminal begin by running the following commands to install the C++ compiler and other crucial tools:
Note! If you are using OpenDLV Desktop, this is already included.
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential
The first command, apt-get update
, checks online and downloads the latest
Ubuntu software catalog, so that the latest software and updates can be
installed. The next command, apt-get upgrade
, is to update all existing
software if new versions are found in the newly downloaded catalog. Finally,
the commend apt-get install build-essential
installs the software package
build-essentials
which is a collection of software development tools for
Ubuntu. For example, it contains compilers that we need to compile our code and
develop our software (including C++).
As perhaps noticed, all of the commands were prepended with sudo
that is a
command, actually referring to super-user do, that runs the following command
with super-user (administrator) privileges. This is needed whenever we do things
that needs to affect the underlying system, and not only for our normal
day-to-day user. Installing software is a good example of things that happens on
the system level, and where super-user privileges are required. However, one
should note that the sudo
command should be avoided in general, and only be
used when necessary. If used in the wrong context it can mess up your system,
typically creating files for the admin user (in Linux referred to as root
)
that can later not be accessed from the normal user, typically resulting in a
permission denied error.
The next step is to create the mandatory hello world program and see how this
works with the compiler. Start with creating a new folder called mytest
with
the following command:
mkdir -p data/mytest
The command mkdir
is used to create a new folder, and the -p
option means
that it will create all parts of the path if needed. Here we create the folder
data
, if it was not already present, and then we create the folder mytest
inside.
Note! In the OpenDLV Desktop system the folder data
has a special meaning, being a folder that is shared with the underlying host. Anything that is put outside this folder will be forgotten if the OpenDLV Desktop container is closed.
In order to see the new folders you can use the ls
(list) command that
allows you to see all the folders and files in your current path. Here you
should now see data
. Then continue by entering the folders using the cd
(change directory) command:
cd data
cd mytest
This will take you to the newly created mytest
folder. You could as well done
the same movement with the single command cd data/mytest
.
Tip! There are many convenient functions available when working in the terminal. Perhaps the most useful one is to use the TAB key to make the terminal fill in what you are likely to type next. Hit TAB once to solve clear cases, and hit it twice to get a list of possible options (based on your input so far).
The next step is to make and enter a new file named helloworld.cpp
in the
mytest
folder. For that a simple text editor can be used as:
gedit helloworld.cpp
The text editor gedit
is a graphical file editor that is available in most
Linux distributions and used for creating and editing files. The file extension
.cpp
is the most common file extension for C++ files source code files.
Write a hello world program
Using gedit
type the following content into helloworld.cpp
:
#include <iostream>
int32_t main(int32_t argc, char **argv) {
std::cout << "Hello world!" << std::endl;
return 0;
}
Tip! It is highly recommended that you type everything manually rather than copying directly from the webpage. By doing so, your brain will automatically get the time to digest the material and notice small details that you would otherwise miss out on.
The line int32_t main(int32_t argc, char **argv)
defines a function in C++.
First, the line says what return type the function has, here int32_t
which is
an integer of 32 bits (4 bytes) where the t
means that it is a standard C++
type. Then, main
is the name of the function, and the last part
(int32_t argc, char **argv)
is the arguments, or parameters, to the function.
Importantly, the main
function, with exactly the properties explained here
is the default starting point of any C++ program. In other words, it is always
here the execution of the program starts, even if it is distribute over many
functions and code files.
In the top of the file the row #include <iostream>
was added. This includes
another resource (source code library), where the tags <>
indicate that this
particular resource is part of the standard C++ code libraries. In this case the
iostream
library was included, which contains functions to handle input and
output streams. An output stream can be used to print text messages to the
terminal, and an input stream can be used to input data into the program. In
this case we use iostream
to allow our program to output text in the terminal
window.
Inside the main
function, the line std::cout << "Hello world!" << std::endl;
is included, and this is the part that forms an output stream to print text to
the terminal. Specifically, std
means that we use a standard function from
C++, which is defined in iostream
. Then, the function used is called cout
and the syntax << "Hello world!"
means that an output stream is formed to
print the words Hello world!
. In the end, the << std::endl;
is added to
indicate a new line, which is also a standard function in iostream
.
The program ends with return 0;
and this is to indicate to the caller of the
program (for example the terminal) if the program experienced any errors. A
return value of 0
means that there were no errors.
When you are done writing the program, press the save button and close the text
editor window. If you now type ls
in the terminal, you will now see your
helloworld.cpp
file. Now it is time to compile it with the following command:
g++ -std=c++17 -o helloworld helloworld.cpp
In this command, g++
is a common open source compiler for C++. The argument
-std=c++17
tells the compiler what version of the C++ standard we want to use
during the compilation, which in this case is version 17. To set an output name
for the compiled machine code, or executable, the argument -o
is used and the
resulting name is set to helloworld
. Finally, the source code file
helloworld.cpp
is selected for compilation. Note that the name of the
resulting executable does not need to reflect the name of the source code file,
but it makes sense to use similar names.
When the compilation is done, the resulting compiled machine code is found in
the mytest
folder, and this can be verified by using the ls
command. You may
also notice that the color of the filename is green in the terminal window, this
is to indicate that the file is executable.
Tip! To get more information about the files, typels -l
. Thex
on the row of the executable indicates that the file is executable, and can be run as a program.
You can now try to run the program with the command:
./helloworld
Tip! One dot.
means the current directory and two dots..
means the directory above.
On the terminal you should now see the output message Hello world!
.
Tip! To clean in your terminal you can run the command clear
.
You should now remove the executable helloworld
file with:
rm helloworld
If you now try the command ls
, you will see that the green helloworld
file
is gone.
So far we have created a simple C++ 17 program, by first typing in and saving
the code in a text file, then compiling it using a compiler, and then running
it. For larger projects you are likely to include more functions, probably
divided into several files, and with linked external code libraries. Therefore,
in the general case it is not very convenient to directly use the g++
command,
but to instead use build tools to aid in the compilation and linking. Next, one
such tool, namely cmake
will be shown.
Info! There is a big conceptual difference between a program/app and a script. A program is compiled into machine code saved inside an executable file, while a script is formed from human-readable text that is inputted into a program that reacts to each line of text. Python is an example of a scripting language, and C++ is an example of a programming language.
The build environment CMake
The next step is to set up a system to help us with building more advanced
programs. A good option is to use a build environment called CMake. CMake is a
wrapper around the compiler and other tools to help us automate and simplify
the compilation process. Note that CMake is not a compiler by itself, instead
it is a tool made to understand the entire build process which uses compilers
and similar software to automate and simplify the build process. CMake works
with different compilers, for example compilers in Mac OS and Windows, not just
for the g++
compiler found in Linux.
In Ubuntu, install CMake from the terminal using the following command:
Note! If you are using OpenDLV Desktop, this is already included.
sudo apt-get install cmake
Next, create a new file inside the mytest
folder called CMakeLists.txt
using
the gedit
editor:
gedit CMakeLists.txt
It is important to type the name exactly like this, including for the capital
and lower-case letters. In the file, continue by defining your project:
cmake_minimum_required(VERSION 3.2)
project(helloworld)
set(CMAKE_CXX_STANDARD 17)
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp)
The first row, cmake_minimum_required(VERSION 3.2)
, states what version of
CMake is required when building the program. With the next line,
project(helloworld)
, the project is given a name, and the line after,
CMAKE_CXX_STANDARD 14
, indicates the C++ standard version 17. The last row,
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp)
,
is where the compiler will be called to create the actual executable. Here, the
syntax ${PROJECT_NAME}
is a variable created from the second line here used
to set the name of the resulting executable (to the name of the project).
Finally, the built in CMake variable ${CMAKE_CURRENT_SOURCE_DIR}
is used to
point to the directory where the CMake command was engaged, and where the source
code is stored, to point to the file, ./helloworld.cpp
, we want to use in the
compilation.
When you are done writing your CMake instructions, press the save button and
close the CMakeLists.txt
file. Now, the build process can be started by using
CMake. Run the following commands in the terminal:
mkdir build
cd build
cmake ..
make
The first command, mkdir build
, creates a new directory inside the mytest
folder called build
. Then, cd
is used to move inside this folder, and then
CMake is engaged by running the command cmake ..
where the two dots
corresponds to the folder above (mytest
) where cmake
expects to find a
CMakeLists.txt
file. The file is scanned and CMake prepares all the compile
build process and all related commands. To see all the files created by cmake
you can simply run the ls
command, but these files are not important to us and
can be recreated at any time. Finally, when running make
the build process is
started and the resulting executable is created.
Now run the program:
./helloworld
Once again, the output message Hello world! is shown, demonstrating that CMake
carried out the compilation by using g++
in the background. Even though, in
principle, the exact same thing was done as before, but in a more complicated
way, it is now easier to continue expanding the program with the help from
CMake.
Making a more useful application
Now it is time to create a more useful program, one that can determine if a
given integer is a prime number or not. From the folder mytest/build
, open the
helloworld.cpp
file for editing:
cd ..
gedit helloworld.cpp
Remember, the two dots corresponds to the folder above, so cd ..
goes to the
folder above the current one (mytest
), where the source code file is located.
Edit the program so it looks like:
Tip! Indentation is very important when writing code. Here, two spaces are used for indentation. After each opening bracket one indentation level is added, and after each closing bracket one indentation level is removed.
#include <cstdint>
#include <iostream>
bool isPrime(uint16_t n) {
bool retVal{true};
if (n < 2 || n%2 == 0) {
retVal = false;
} else {
for (uint16_t i{3}; (i*i) <= n; i += 2) {
if (n%i == 0) {
retVal = false;
break;
}
}
}
return retVal;
}
int32_t main(int32_t argc, char **argv) {
std::cout << "Hello world = " << isPrime(43) << std::endl;
return 0;
}
A new standard library called cstdint
is included with #include <cstdint>
.
However, the largest change is a new function that checks if an integer is a
prime number or not. The function is declared as bool isPrime(uint16_t n)
,
where bool
refers to the return type. In this case, the function should return
true
if the given integer is a prime, and false
if it is not. The name of
the function is isPrime
, and the input is uint16_t n
which is a 16 bit
(2 byte) unsigned integer. As for the main
function, the body, or logic, of
the function is all kept within mandatory curly brackets.
On the next row bool retVal{true};
, a new boolean variable named retVal
is
created and is initialized with the value true
. In modern C++, there is slight
difference in initializing a new variable with a value using curly brackets,
compared to assigning a value using an equal symbol (like
bool retVal = true
). The initialization strictly means that the variable holds
the value when created, and the assignment could mean that it is given
afterwards. This is just one example of how specific C++ syntax can be, and how
much control (and responsibility) the language offers to the programmer.
Next, the if statement if (n<2 || n%2 == 0)
checks if the input variable n
either is less than 2
, or (||
) if it is an evenly divided by 2
. If so, the
given number is not a prime and retVal
is set to false
. If the conditions
were not met, then the statement continues into the else
clause. Inside, the
for loop for (uint16_t i{3}; (i*i) <= n; i += 2)
is used to further analyze
the give numer, to check if it is a prime number. In the loop, an integer
uint16_t i{3}
called i
and give it the initial value of 3
is created and
as long i*i
is less or equal to n
the loop is continued, adding the value of
2
to i
for every iteration.
Inside the for loop, the if statement if (n%i == 0)
checks if n
is evenly
divided by i
(an odd number starting at 3
). If so, then it is clear that the
given number is not a prime and retVal
is set to false
and the for loop is
aborted by break
.
If the first condition is not fulfilled, and if the for loop is concluded
without aborting, then this means that the input value n
is a prime number. In
this case, the retVal
variable will be unchanged and true
will be given as
the final function return value.
Finally, in main
the isPrime
function is tested inside the Hello world
print out, where it tests if 43
is a prime or not.
Tip! When working with streams in C++, a good thing is that one can just continue to add new parts to the stream, with the <<
(stream out) operator between each added part, as exemplified in the above code.
Now, save and close the file. Go to the build folder, then rebuild the program and test it with the following:
cd build
make
./helloworld
Note that you do not need to run cmake ..
now, as the CMakeLists.txt
file is
already processed by CMake. Even if there would be changes to the
CMakeLists.txt
file, the make
command is set up in a way so that it would
detect and rerun cmake
whenever needed.
If there were no errors from make
, and when running the program it should show
the message Hello world = 1
in your terminal. Here, 1
represents true
,
meaning that 43
is a prime.
Classes
In same cases it might be a good idea to bundle code in classes, a construct that includes both data and related logic that can be instantiated into objects. Classes are the central concept within object-oriented programming, and here they are mainly used as a way to formalize code testing as discussed below.
In C++, classes are separated into two parts: the header, or declaration, and the implementation, or definition. In other words, the header explains what the class is, and the implementation explains how it works. The reason why this language design exists, and why it is a good design, is that C++ very often are used to form libraries that can be shared between different programs. A programmer that uses an external library, even a closed-source one, only needs to know the what, and not the how.
First start by writing our class header by creating a new file in the
mytest
folder called prime-checker.hpp
(gedit prime-checker.hpp
). If you
are in the build
folder, use cd ..
to go back into mytest
. Fill it
with the following code:
#ifndef PRIMECHECKER
#define PRIMECHECKER
#include <cstdint>
class PrimeChecker {
public:
bool isPrime(uint16_t);
};
#endif
Remember, this is only one out of two parts that form our class, and this first
part only declares the class, meaning that it explains what the class contains
(interfaces) without specifying how it works. In the code, one may once again
notice the #
operator in-front of the first two lines. In general this
character indicates that the code runs even before the compiler, in the
preprocessing step, where code is prepared before it is sent to the compiler.
The first part of the code #ifndef PRIMECHECKER
is like a gate that checks if
the word PRIMECHECKER
is defined or not. If the preprocessor passes here for
the first time, then this word is not defined as it is defined directly on the
line after. This means that the preprocessor will only consider the content
between #ifndef
and #endif
only once if it was part of an #include
call
from many other files. The preprocessor is tasked to collect all code into a
single long code listing as input to the compilation step, and the gate prevents
the same code to be included many times, or even worse, end up in infinite
inclusion loops.
In the actual class declaration class PrimeChecker
the class is given a name.
Then, the class has one public
method (functions inside classes are called
methods), called isPrime
, which is recognized from the previous version of
the code. The public
keywords means that the method can be accessed from
outside the class scope, in contrast to members (methods or variables) which
would be specified under a private
keyword.
The next step is to create the next part of the class, namely the definition.
Save and close the prime-checker.hpp
file. Then create a new file called
prime-checker.cpp
(gedit prime-checker.cpp
) and add the following code:
#include "prime-checker.hpp"
bool PrimeChecker::isPrime(uint16_t n) {
bool retVal{true};
if (n < 2 || n%2 == 0) {
retVal = false;
} else {
for(uint16_t i{3}; (i*i) <= n; i += 2) {
if (n%i == 0) {
retVal = false;
break;
}
}
}
return retVal;
}
On the first row #include "prime-checker.hpp"
the previous file is included
by the preprocessor (copy–paste before handing over to the compiler). The
difference from when we used the #include
directive before is that there are
now quotes rather than less-than and greater-than signs. The quotes tell the
preprocessor to look inside the current folder for this file, rather than for
system libraries.
The function is almost exactly the same as it is in the helloworld.cpp
file.
The difference is that the method name is prepended with PrimeChecker::
which
tells the compiler that this is the definition of the method declared in the
PrimeChecker
class. If this would have been missing, the compiler would
complain that the isPrime
method was declared in the class declaration
(header file), but it was never defined.
Save the file and close it. Now, the class is both declared and defined and is
ready to use. From the class, any number of PrimeChecker
objects can be
formed, each with its own logical scope. In this case, the class does not
contain any variables, just a method, so it would not make much sense to create
several different objects even though this is possible. One object is needed
though, and that should be added to helloworld.cpp
. Open the file and change
it to this:
#include <iostream>
#include "prime-checker.hpp"
int32_t main(int32_t argc, char **argv) {
PrimeChecker pc;
std::cout << "Hello world = " << pc.isPrime(43) << std::endl;
return 0;
}
The first thing that might be noticed is that the #include <cstdint>
and
isPrime
function was removed, since they are now instead part of the new
PrimeChecker
class. Next, it can be seen that an object named pc
is created
from the PrimeChecker
class. This object includes all members of the class,
in this case the method PrimeChecker::isPrime
. The method is called by using
the dot operator together with the object, similarly to how the function was
used. Save and close the helloworld.cpp
file.
Next, the new source code file prime-checker.cpp
needs to be added to CMake,
to be included in the compilation. It is only needed to add definition files, as
the declarations are included using the preprocessor. Later, the different
compiled definitions are combined to a complete program in a step after
compilation, referred to as the linking step. To include the relevant files
for compilation, and then linking handled automatically by CMake, change the
CMakeLists.txt
file into:
cmake_minimum_required(VERSION 3.2)
project(helloworld)
set(CMAKE_CXX_STANDARD 17)
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp
${CMAKE_CURRENT_SOURCE_DIR}/prime-checker.cpp)
On the last row, the new file was added. Then save and close CMakeLists.txt
.
Now, build and run the program to see that the changes still give the same
results:
cd build
make
./helloworld
Bringing in automated testing
When writing code that contains logic it is important to test that the code gives the right output given a specific input. It is often much easier for the developer to know what the function should produce, compared to making it work for all situations. There is a method to formally test functionality often referred to unit tests. A unit test is simply a function call with specific input values, where the developer tests towards an expected output value. The prime checker created here fits perfectly for such tests, and now when it was wrapped into a class it is easy to use it with a library for unit tests. When configured properly with CMake the tests will then be re-run every time the code is compiled, and if someone in the future would change the code and introduce a new bug, the test would immediately fail.
Some library to handle the tests is needed, and a simple such library is called
Catch2, which is a library for unit tests that is very easy to include in our
project since it consists of only a single header file (a header-only
library). Download the library by running the following command inside the
folder mytest
(use cd ..
if needed, and ls
to check that you are in the
mytest
folder with the CMakeLists.txt
and helloworld.cpp
and other files):
Tip! In a header-only library, the full source code is encapsulated in a header file (.hpp
). This is convenient for libraries that should be easy to distribute and include in projects. The down-side is that the library needs to be compiled every time the project is compiled. In contrast, pre-compiled libraries would only need to be linked to the compiled project.
wget https://github.com/catchorg/Catch2/releases/download/v2.13.10/catch.hpp
Now run the command ls
to verify that the file catch.hpp
is located in the
source folder. To make the first unit tests, used for the automated testing,
a new source code file needs to be created. For this purpose, create a file
called test-prime-checker.cpp
(gedit test-prime-checker.cpp
) and add the
following:
#define CATCH_CONFIG_MAIN // This tells Catch to provide a main(), only do once
#include "catch.hpp"
#include "prime-checker.hpp"
TEST_CASE("Test PrimeChecker 1.") {
PrimeChecker pc;
REQUIRE(pc.isPrime(5));
}
The first part #define CATCH_CONFIG_MAIN
tells Catch2 to provide a main
function. Normally, as mentioned earlier, all source files or programs need to
have a main function, and here that is simplified for us through the Catch2
library as this specific program should only consider the unit tests themselves.
In the next two rows the Catch2 header-only library and the code under test is included. Again, only the header file (declaration) of the prime checker should be included, as a template for how the class can be used. The actual implementation (definition) is compiled separately byt the compiled, and after both files are compiled they are the linked together into a single program.
Finally, a single unit test is defined, starting with
TEST_CASE("Test PrimeChecker 1.")
, where TEST_CASE
tells Catch2 to form a
specific unit test function with the name "Test PrimeChecker 1.". Inside this
test the first thing that happens is that a PrimeChecker
object is created
(an instance of the class PrimeChecker
) named pc
. Then, on the next row,
a REQUIRE
keyword from Catch2 is used, that must be given true
as input
to not fail the unit test. In this case that means that if pc.isPrime(5)
would
give anything else than true
the unit test would fail (the program
test-prime-checker
would return an error).
Save and close the file, and then go ahead and try to compile the new Catch2
program. As a test before moving to CMake, use g++
manually as:
g++ -std=c++17 -o test-prime-checker test-prime-checker.cpp prime-checker.cpp
The name of the program is test-prime-checker
and it uses two source code
files, test-prime-checker.cpp
and prime-checker.cpp
. Both of them will try
to include the prime-checker.hpp
file but it will only happen once (remember
the pre-processor logic discussed before). Note that the helloworld.cpp
file
is not considered here, as the Catch2 program is its own complete program with
its own main function. Later it will be more clear how the two programs relate
to each other. Next, run the compiled program as:
./test-prime-checker
The following output is expected:
All tests passed (1 assertion in 1 test case)
Now, add some more unit tests into test-prime-checker.cpp
, according to:
#define CATCH_CONFIG_MAIN // This tells Catch to provide a main(), only do once
#include "catch.hpp"
#include "prime-checker.hpp"
TEST_CASE("Test PrimeChecker 1.") {
PrimeChecker pc;
REQUIRE(pc.isPrime(5));
}
TEST_CASE("Test PrimeChecker 2.") {
PrimeChecker pc;
REQUIRE(!pc.isPrime(4));
}
TEST_CASE("Test PrimeChecker 3.") {
PrimeChecker pc;
REQUIRE(pc.isPrime(3));
}
TEST_CASE("Test PrimeChecker 4.") {
PrimeChecker pc;
REQUIRE(pc.isPrime(2));
}
The above test cases require that 5
, 3
, and 2
are all considered prime
numbers from the prime checker, and that 4
is not a prime number as the
exclamation mark is the negation symbol in C++. Save and close the new program.
Then compile and run the program manually again with g++
.
Tip! Instead of typing the long command again you can use the up-arrow to get to previous commands you've written.
When running the tests again, you will now notice that there was in fact a bug
in our program. It considered 2
to not be a prime (but it is a prime). This
demonstrates the strength of unit tests, that there might be specific border
cases that might be hard to get right in the code. Now open prime-checker.cpp
to fix the bug. Change the code into:
#include "prime-checker.hpp"
bool PrimeChecker::isPrime(uint16_t n) {
bool retVal{true};
if (n < 2 || (n != 2 && n%2 == 0)) {
retVal = false;
} else {
for(uint16_t i{3}; (i*i) <= n; i += 2) {
if (n%i == 0) {
retVal = false;
break;
}
}
}
return retVal;
}
There is a small change to the first if statement where the code now checks if
the input value is dividable by 2
, but at the same time it should not equal
to 2
. Save and close the editor, then recompile the unit tests and run them
again. Now everything should pass.
Bring the test cases into CMake
Finally, it is time to make the tests automated by including them into the
CMake build process. In order to do so, open CMakeLists.txt
in the editor and
change it to:
cmake_minimum_required(VERSION 3.2)
project(helloworld)
set(CMAKE_CXX_STANDARD 17)
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp
${CMAKE_CURRENT_SOURCE_DIR}/prime-checker.cpp)
enable_testing()
add_executable(test-prime-checker ${CMAKE_CURRENT_SOURCE_DIR}/test-prime-checker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/prime-checker.cpp)
add_test(NAME test-prime-checker COMMAND test-prime-checker)
The enable_testing()
command tells CMake to add an extra test step into the
build process, where the build will fail if any of the added test programs
would exit with any error. Then, on the next row, CMake is instructed to
compile an additional program, the Catch2 program with all the tests. Then
finally, CMake is instructed to run the Catch2 program in the test step of the
build process. Save and close the file. Now, try to once again build the program
and then engage the tests using CMake:
cd build
make
make test
The newcomer here is the make test
command. When it runs, all tests registered
with CMake will be engaged one by one. In this case there was only one such
test, the Catch2 program that we created. Now, automated testing is introduced
to the build process, and that is a great way of increasing the quality of code
by finding unexpected bugs.
No pain no gain: Making the compiler unfriendly
There is one more thing that can be done in order to increase the quality of our
code, and that is related to the compiler. The compiler is a very advanced
software that can be configured in many different ways. So far in this tutorial
only the specific C++ version was specified, but there are also many other
things that can be tightened up in regards to what the compiler should accept
in terms of code quality and style. Since the goal should always be to increase
the quality of the code, and by that reduce the risk for bugs, it makes sense to
maximize the pickiness of the compiler so that it can abort the compilation if
it detects questionable code. To do this, open the CMakeLists.txt
file
and change it according to:
cmake_minimum_required(VERSION 3.2)
project(helloworld)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
add_executable(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.cpp
${CMAKE_CURRENT_SOURCE_DIR}/prime-checker.cpp)
enable_testing()
add_executable(test-prime-checker test-prime-checker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/prime-checker.cpp)
add_test(NAME test-prime-checker COMMAND test-prime-checker)
The added row includes to compiler directives, -Wall
and -Wextra
. The first
will include warnings for all thinkable problems, and the second will include
warnings for even more cases. Some will be related to direct violations of the
version 17 standard, and some will be related to good programming practice. Even
more warnings could be added by other flags, but these two give a good default
coverage. Next, lets see if our code would generate any warnings in its current
state. Save and close the file, and then compile the program using CMake as
before.
When compiling, there will in fact be an error saying that argc
and argv
from the main function (int32_t main(int32_t argc, char **argv)
) are unused
parameters. This is true, the variables are declared in the function header,
but they are never used. It is not a very serious problem of course, but the
principle of minimalism is one of the most important when reducing the risk
of bugs. Therefore, the code should be slightly adjusted to remove the warning.
Change the code in helloworld.cpp
into:
#include <iostream>
#include "prime-checker.hpp"
int32_t main(int32_t, char **) {
PrimeChecker pc;
std::cout << "Hello world = " << pc.isPrime(43) << std::endl;
return 0;
}
The code still has the exact same meaning as before, but by removing the names
of the unused variables it is indicated to the compiler that these variables
are never used. In that way, the developer and compiler now has a formal
agreement concerning the two variables. Save and close the file and compiler
again. Now, no warnings should be reported and the risk of bugs should be
minimal.