OpenDLV
Introduction

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, type ls -l. The x 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.