By the end of this chapter, you will be able to:
In this chapter, we will learn to establish the code-build-test model that will be used throughout the book, write beautiful code, and perform unit tests.
C++ is one of the oldest and most popular languages that you can use to write efficient code. It is both "close to the metal," like C, and has advanced object-oriented features, like Java. Being an efficient low-level language makes C++ the language of choice for domains in which efficiency is paramount, such as games, simulations, and embedded systems. At the same time, being an object-oriented language with advanced features such as generics, references, and countless others makes it suitable for large projects that are developed and maintained by multiple people.
Almost any programming experience involves organizing your code base and using libraries written by others. C++ is no different. Unless your program is simple, you will distribute your code into multiple files that you need to organize, and you will use various libraries that fulfill tasks, usually in a much more efficient and robust way than your code would. C++ projects that do not use any third-party libraries are edge cases that do not represent the majority of projects, which use many libraries. These projects and their libraries are expected to work in different hardware architectures and operating systems. Therefore, it is important to spend time on project setup and understand the tools used to manage dependencies if you are going to develop anything meaningful with C++.
Most modern and popular high-level languages have standard tools to maintain projects, build them, and handle their library dependencies. Many of these have repositories that host libraries and tools that automatically download and use libraries from those repositories. For example, Python has pip, which takes care of downloading and using appropriate versions of libraries that the programmer wants to use. Similarly, JavaScript has npm, Java has maven, Dart has pub, and C# has NuGet. In most of these languages, you list the name of the library and the version that you would like to use, and the tool automatically downloads and uses the compatible version of the library. These languages benefit from the fact that the programs are built and run in a controlled environment in which a certain level of hardware and software requirements are satisfied. C++, on the other hand, is expected to work in a variety of contexts with different architectures, including very primitive hardware. Hence, C++ programmers are less pampered when it comes to building programs and performing dependency management.
In the world of C++, we have several tools that help in managing project sources and their dependencies. For example, pkg-config, Autotools, make, and CMake are the most notable ones in the community. Compared to the tools of the other high-level languages, these are much more complicated to use. CMake has arisen among these as the de facto standard for managing C++ projects and their dependencies. It is more opinionated compared to make, and it is accepted as the direct project format for most IDEs (Integrated Development Environments).
While CMake helps with managing projects and their dependencies, the experience is still far from higher-level languages in which you list the libraries and their versions that you want to use and everything else is taken care of for you. With CMake, you still are responsible for installing libraries properly in your development environment, and you are expected to use compatible versions for each library. In popular Linux distributions with extensive package managers, you can easily install binary versions of most popular libraries. However, sometimes, you may have to compile and install the libraries yourself. This is a part of the whole C++ developer experience, which you will gather by learning more about the development platform of your choice. Here, we will focus more on how to properly set up our CMake projects, including understanding and resolving issues related to libraries.
In order to base our discussion on a solid foundation, we will immediately start with a practical example. We will start with a C++ code base template that you can use as a starting point for your own projects. We will see how we can build and compile it using CMake on the command line. We will also set up the Eclipse IDE for C/C++ developers and import our CMake project. The use of an IDE will provide us with facilities that ease the creation of source code and enable us to debug our programs line by line to view what exactly happens during the execution of our program and correct our mistakes in an informed fashion rather than trial and error and superstition.
The de facto standard for C++ projects is to use CMake to organize and build the project. Here, we will use a basic template project as a starting point. The following is the folder structure of a sample template:
In the preceding figure, the .gitignore file lists the file patterns that should not be added to the git version control system. Such ignored files include the outputs of the build process, which are created locally and should not be shared among computers.
The files in the include and src folders are the actual C++ source files, and the CMakeLists.txt file is the CMake script file that glues the project together by handling the source compilation rules, library dependencies, and other project settings. CMake rules are high-level platform-independent rules. CMake uses them to create various types of make files for different platforms.
Building a project with CMake is a two-step process. First, we get CMake to generate platform-dependent configuration files for a native build system that will compile and build the project. Then, we will use the generated files to build the project. The platform-dependent build systems that CMake can generate configuration files for include UNIX Makefiles, Ninja build files, NMake Makefiles, and MinGW Makefiles. The choice here depends on the platform in use, the availability of these tools, and personal preference. UNIX Makefiles are a de facto standard for Unix and Linux, whereas NMake is its Windows and Visual Studio counterpart. MinGW, on the other hand, is a Unix-like environment in Windows in which Makefiles are also in use. Ninja is a modern build system that provides exceptional speed compared to other build systems coupled with multi-platform support, which we choose to use here. Furthermore, in addition to these command-line build systems, we can also generate IDE projects for Visual Studio, XCode, Eclipse CDT, and many others, and build our projects inside the IDE. Therefore, CMake is a meta tool that will create the configuration files for another system that will actually build the project. In the next section, we will solve an exercise, wherein we will generate Ninja build files using CMake.
In this exercise, we will use CMake to generate Ninja build files, which are used to build C++ projects. We will first download our source code from a git repository and will use CMake and Ninja to build it. The aim of this exercise is to use CMake to generate Ninja build files, build the project, and then run them.
The link to the GitHub repository can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project.
Perform the following steps to complete the exercise:
git clone https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project
The output of the previous command is similar to the following:
Now you have the source code in the CxxTemplate folder.
cd CxxTemplate
find .
cmake -Bbuild -H. -GNinja
The output of the preceding command is as follows:
Let's explain parts of the preceding command. With -Bbuild, we are telling CMake to use the build folder to generate build artifacts. Since this folder does not exist, CMake will create it. With –H., we are telling CMake to use the current folder as the source. By using a separate build folder, we will keep our source files clean and all the build artifacts will live in the build folder, which is ignored by Git thanks to our .gitignore file. With –GNinja, we are telling CMake to use the Ninja build system.
ls
ls build
The preceding command will show the following output in the terminal:
It's clear that the preceding files will be present inside the build folder. build.ninja and rules.ninja in the preceding output are the Ninja build files that can actually build our project in this platform.
By using CMake, we did not have to write the Ninja build files and avoided committing to the Unix platform. Instead, we have a meta-build system that can generate low-level build files for other platforms such as UNIX/Linux, MinGW, and Nmake.
cd build
ninja
You should see a final output like the following:
ls
The previous command yields the following output in the terminal:
In the preceding figure, you can see that the CxxTemplate executable is generated.
./CxxTemplate
The previous command in the terminal will provide the following output:
The following line from the src/CxxTemplate.cpp file is responsible for writing the previous output:
std::cout << "Hello CMake." << std::endl;
Now you have successfully built a CMake project in Linux. Ninja and CMake work quite well together. You have to run CMake only once and Ninja will detect whether CMake should be called again and will call it for you. For example, even if you add new source files to your CMakeLists.txt file, you only need to type the ninja command in the terminal, and it will run CMake automatically for you to update the Ninja build files. Now that you have learned about building a CMake project in Linux, in the next section, we will look at how to import a CMake project into Eclipse CDT.
A Ninja build file is useful for building our project in Linux. However, a CMake project is portable and can be used with other build systems and IDEs as well. Many IDEs accept CMake as their configuration file and provide a seamless experience as you modify and build your project. In this section, we will discuss how to import a CMake project into Eclipse CDT, which is a popular cross-platform C/C++ IDE.
There are multiple ways of using Eclipse CDT with CMake. The default one that CMake provides is the one-way generation of the IDE project. Here, you create the IDE project once, and any modifications you make to your IDE project will not change back the original CMake project. This is useful if you manage your project as a CMake project and do one-time builds with Eclipse CDT. However, it's not ideal if you want to do your development in Eclipse CDT.
Another way of using CMake with Eclipse CDT is to use the custom cmake4eclipse plugin. When using this plugin, you do not abandon your CMakeLists.txt file and make a one-way switch to Eclipse CDT's own project manager. Instead, you keep managing your project through the CMakeLists.txt file, which continues to be the main configuration file of your project. Eclipse CDT actively works with your CMakeLists.txt file to build your project. You can add or remove source files and make other changes in your CMakeLists.txt, and the cmake4eclipse plugin applies those changes to the Eclipse CDT project at every build. You will have a nice IDE experience while keeping your CMake project current. The benefit of this approach is that you can always quit using Eclipse CDT and use your CMakeLists.txt file to switch to another build system (such as Ninja) later. We will use this second approach in the following exercise.
In the last exercise, you developed a CMake project and you would like to start using Eclipse CDT IDE to edit and build that project. In this exercise, we will import our CMake project into the Eclipse CDT IDE using the cmake4eclipse plugin. Perform the following steps to complete the exercise:
You have successfully built and run a CMake project using Eclipse CDT. In the next exercise, we will introduce a frequent change to our projects by adding new source files with new classes.
As you develop significantly bigger C++ projects, you will tend to add new source files to it as the project grows to meet the set expectations. In this exercise, we will add a new .cpp and .h file pair to our project and see how CMake and Eclipse CDT work together with these changes. We will add these files inside the project using the new class wizard, but you can also create them with any other text editor. Perform the following steps to add new source files to CMake and Eclipse CDT:
#include "ANewClass.h"
#include <iostream>
void ANewClass::run() {
std::cout << "Hello from ANewClass." << std::endl;
}
You will see that Eclipse warns us with a Member declaration not found message:
This error is generated since we need to add this to our ANewClass.h file as well. Such warnings are made possible by analyzers in IDEs and are quite useful as they help you fix your code as you are typing, without running the compiler.
public:
void run(); // we added this line
ANewClass();
You should see that the error in the .cpp file went away. If it did not go away, it may be because you may have forgotten to save one of the files. You should make it a habit to press Ctrl + S to save the current file, or Shift + Ctrl + S to save all the files that you edited.
#include "CxxTemplate.h"
#include "ANewClass.h"
#include <string>
...
CxxApplication::CxxApplication( int argc, char *argv[] ) {
std::cout << "Hello CMake." << std::endl;
::ANewClass anew;
anew.run();
}
The complete code of this file can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Exercise03/src/CxxTemplate.cpp.
add_executable(CxxTemplate
src/CxxTemplate.cpp
src/ANewClass.cpp
)
Try to build the project again. This time you should not see any errors.
You modified a CMake project, added new files to it, and ran it fine. Note that we created the files in the src folder and let the CMakeLists.txt file know about the CPP file. If you do not use Eclipse, you can simply continue with the usual CMake build commands and your program will run successfully. So far, we have checked out the sample code from GitHub and built it both with plain CMake and with the Eclipse IDE. We also added a new class to the CMake project and rebuilt it in Eclipse IDE. Now you know how to build and modify CMake projects. In the next section, we will perform an activity of adding a new source-header file pair to the project.
As you develop C++ projects, you add new source files to it as the project grows. You may want to add new source files for various reasons. For example, let's say you are developing an accounting application in which you calculate interest rates in many places of your project, and you want to create a function in a separate file so that you can reuse it throughout your project. To keep things simple, here we will create a simple summation function instead. In this activity, we will add a new source-header file pair to the project. Perform the following steps to complete the activity:
The expected output should be similar to the following:
The solution for this activity can be found on page 620.
In the following section, we will talk about how to write unit tests for our projects. It is common to divide projects into many classes and functions that work together to achieve the desired goal. You must manage the behavior of these classes and functions with unit tests to ensure that they behave in expected ways.
Unit tests are an important part of programming in general. Basically, unit tests are little programs that use our classes in various scenarios with expected results, live in a parallel file hierarchy in our project, and do not end up in the actual executable but are executed separately by us during development to ensure that our code is behaving in expected ways. We should write unit tests for our C++ programs to ensure that they behave as they are supposed to after each change.
There are several C++ test frameworks that we can use with CMake. We will use Google Test, which has several benefits over other options. In the next exercise, we will prepare our project for unit testing with Google Test.
We have installed Google Test but our project is not set up to use Google Test for unit testing. In addition to the installation, there are settings that need to be carried out in our CMake project to have Google Test unit tests. Follow these steps to implement this exercise:
find_package(GTest)
if(GTEST_FOUND)
set(Gtest_FOUND TRUE)
endif()
if(GTest_FOUND)
include(GoogleTest)
endif()
# add these two lines below
enable_testing()
add_subdirectory(tests)
This is all we need to add to our main CMakeLists.txt file.
include(GoogleTest)
add_executable(tests CanTest.cpp)
target_link_libraries(tests GTest::GTest)
gtest_discover_tests(tests)
Let's dissect this code line by line. The first line brings in the Google Test capability. The second line creates the tests executable, which will include all our test source files. In this case, we only have one CanTest.cpp file, which will just verify that the testing works. After that, we link the GTest library to the tests executable. The last line identifies all individual tests in the tests executable and adds them to CMake as a test. This way, various test tools will be able to tell us which individual tests failed and which ones passed.
#include "gtest/gtest.h"
namespace {
class CanTest: public ::testing::Test {};
TEST_F(CanTest, CanReallyTest) {
EXPECT_EQ(0, 0);
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
The TEST_F line is an individual test. Now, EXPECT_EQ(0, 0) is testing whether zero is equal to zero, which will always succeed if we can actually run the test. We will later add the results of our own classes here to be tested against various values. Now we have the necessary setup for Google Test in our project. Next, we will build and run these tests.
Now, we will discuss how to build, run, and write unit tests. The example that we have so far is a simple dummy test that is ready to be built and run. Later, we will add tests that make more sense and view the output of passing and failing tests. In the following exercise, we will build, run, and write unit tests for the project that we created in the previous exercise.
So far, you have created a project with GoogleTest set up, but you did not build or run the tests we created. In this exercise, we will build and run the tests that we created. Since we added our tests folder using add_subdirectory, building the project will automatically build the tests. Running the tests will require some more effort. Perform the following steps to complete the exercise:
As you can see, our project now has two executable targets. They both live in the build folder, as with any other build artifact. Their locations are build/Debug/CxxTemplate and build/Debug/tests/tests. Since they are executables, we can simply run them.
./build/Debug/tests/tests
The preceding code generates the following output in the terminal:
This is the simple output of our tests executable. If you want to see whether the tests have passed, you can simply run this. However, testing is so much more than that.
cd build/Debug/tests
ctest
cd ../../..
And here is the output that you will see:
The ctest command can run your tests executable with a number of options, including the ability to submit test results automatically to online dashboards. Here, we will simply run the ctest command; its further features are left as an exercise for the interested reader. You can type ctest --help or visit the online documentation to discover ctest further at https://cmake.org/cmake/help/latest/manual/ctest.1.html#.
The result will be similar to the following screenshot:
This is a nice report that contains entries for all tests—only one for now. You may prefer this if you do not want to leave the IDE. Furthermore, when you have many tests, this interface can help you filter them effectively. Now you have built and run tests that were written using Google Test. You ran them in a couple of different ways, including directly executing the test, using ctest, and using Eclipse CDT. In the next section, we will solve an exercise wherein we will actually test the functionality of our code.
You have run simple tests but now you want to write meaningful tests that are testing functionality. In the initial activity, we created SumFunc.cpp, which had the sum function. Now, in this exercise, we will write a test for that file. In this test, we will use the sum function to add two numbers and verify that the result is correct. Let's recall the contents of the following files with the sum function from before:
#ifndef SRC_SUMFUNC_H_
#define SRC_SUMFUNC_H_
int sum(int a, int b);
#endif /* SRC_SUMFUNC_H_ */
#include "SumFunc.h"
#include <iostream>
int sum(int a, int b) {
return a + b;
}
add_executable(CxxTemplate
src/CxxTemplate.cpp
src/ANewClass.cpp
src/SumFunc.cpp
)
Also, let's recall our CantTest.cpp file, which has the main() function of our unit tests:
#include "gtest/gtest.h"
namespace {
class CanTest: public ::testing::Test {};
TEST_F(CanTest, CanReallyTest) {
EXPECT_EQ(0, 0);
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Perform the following steps to complete the exercise:
#include "gtest/gtest.h"
#include "../src/SumFunc.h"
namespace {
class SumFuncTest: public ::testing::Test {};
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
}
Note that this does not have a main() function since CanTest.cpp has one and these will be linked together. Secondly, note that this includes SumFunc.h, which is in the src folder of the project and uses it as sum(3, 4) inside the test. This is how we use our project code in tests.
include(GoogleTest)
add_executable(tests CanTest.cpp SumFuncTest.cpp ../src/SumFunc.cpp) # added files here
target_link_libraries(tests GTest::GTest)
gtest_discover_tests(tests)
Note that we added both the test (SumFuncTest.cpp) and the code that it tests (../src/SumFunc.cpp) to the executable, as our test code is using the code from the actual project.
We can add such tests to our project and all of them will appear on the screen as shown in the preceeding screenshot.
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
// add this test
TEST_F(SumFuncTest, CanSumAbsoluteValues) {
EXPECT_EQ(6, sum(3, -3));
}
Note that this test assumes that the absolute values of the inputs are summed up, which is incorrect. The result of this call is 0 but is expected to be 6 in this example. This is the only change that we have to make in our project to add this test.
As you can see in the preceding figure, the first two tests passed and the last test failed. When we see this output, there are two options: either our project code is wrong, or the test is wrong. In this case, our test is wrong. This is because our CanSumAbsoluteValues test case expects that 6 is equal to sum(3, -3). This is because we assumed that our function sums up the absolute values of the integers provided. However, this is not the case. Our function simply adds the given numbers, whether they are positive or negative. Therefore, this test had a faulty assumption and failed.
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
// change this part
TEST_F(SumFuncTest, CanUseNegativeValues) {
EXPECT_EQ(0, sum(3, -3));
}
Finally, we have set up Google Test with CMake both in our system and project. We also wrote, built, and ran unit tests with Google Test, both in the terminal and in Eclipse. Ideally, you should write unit tests for every class and cover every possible usage. You should also run the tests after each major change and make sure you do not break existing code. In the next section, we will perform an activity of adding a new class and its test.
As you develop a C++ project, you add new source files to it as the project grows. You also write tests for them to ensure that they are working properly. In this activity, we will add a new class that simulates 1D linear motion. The class will have double fields for position and velocity. It will also have a advanceTimeBy() method, which receives a double dt parameter, which modifies position based on the value of velocity. Use EXPECT_DOUBLE_EQ instead of EXPECT_EQ for double values. In this activity, we will add a new class and its test to the project. Follow these steps to perform this activity:
The final test results should look similar to the following:
The solution for this activity can be found on page 622.
Adding new classes and their tests is a very common task in C++ development. We create classes for various reasons. Sometimes, we have a nice software design plan and we create the classes that it calls for. Other times, when a class becomes too large and monolithic, we separate some of its responsibility to another class in a meaningful way. Having this task be practical is important to prevent dragging your feet and ending up with huge monolithic classes. In the following section, we discuss what happens during the compilation and linking stages. This will give us a better perspective of what is happening under the hood of C++ programs.
One of the main reasons for using C++ is efficiency. C++ gives us control over memory management, which is why understanding how objects are laid out in memory is important. Furthermore, C++ source files and libraries are compiled to object files for the target hardware and linked together. Often, C++ programmers have to deal with linker problems, which is why understanding the steps of the compilation and being able to investigate object files is important. On the other hand, large projects are developed and maintained by teams over a long period of time, which is why creating clean and understandable code is important. As with any other software, bugs arise in C++ projects and need to be identified, analyzed, and resolved carefully by observing the program behavior. Therefore, learning how to debug C++ code is also important. In the next section, we will learn how to create code that is efficient, plays well with other code, and is maintainable.
A C++ project is created as a set of source code files and project configuration files that organize the sources and library dependencies. In the compilation step, these sources are first converted to object files. In the linking step, these object files are linked together to form the executable that is the ultimate output of the project. The libraries that the project uses are also linked at this step.
In the upcoming exercises, we will use our existing project to observe the compilation and linking stages. Then, we will manually recreate them to view the process in more detail.
You have been building your projects without investigating the details of the build actions. In this exercise, we will investigate the details of our project's build steps. Perform the following to complete the exercise:
cd build/Debug
make clean
make VERBOSE=1 all
You will get a detailed output of the build process in the terminal, which may look a bit crowded:
Here are some of the lines from this output. The following lines are the important ones related to the compilation and linkage of the main executable:
/usr/bin/c++ -g -pthread -std=gnu++1z -o CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/CxxTemplate.cpp
/usr/bin/c++ -g -pthread -std=gnu++1z -o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/ANewClass.cpp
/usr/bin/c++ -g -pthread -std=gnu++1z -o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/SumFunc.cpp
/usr/bin/c++ -g -pthread -std=gnu++1z -o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/LinearMotion1D.cpp
/usr/bin/c++ -g CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o -o CxxTemplate -pthread
namei /usr/bin/c++
You will see the following output:
Therefore, we will use c++ and g++ interchangeably throughout our discussion. In the build output that we quoted earlier, the first four lines are compiling each .cpp source file and creating the corresponding .o object file. The last line is linking together these object files to create the CxxTemplate executable. The following figure visually presents this process:
As the previous figure shows, the CPP files that are added to CMake as a part of a target, along with the header files that they included, are compiled to object files, which are later linked together to create the target executable.
cd ~/CxxTemplate
mkdir mybuild
/usr/bin/c++ src/CxxTemplate.cpp -o mybuild/CxxTemplate.o -c
/usr/bin/c++ src/ANewClass.cpp -o mybuild/ANewClass.o -c
/usr/bin/c++ src/SumFunc.cpp -o mybuild/SumFunc.o -c
/usr/bin/c++ src/LinearMotion1D.cpp -o mybuild/LinearMotion1D.o -c
cd mybuild
ls
We see the following output as expected. These are our object files:
/usr/bin/c++ CxxTemplate.o ANewClass.o SumFunc.o LinearMotion1D.o -o CxxTemplate
ls
This shows the new CxxTemplate file in the following figure:
./CxxTemplate
And see the output that we had before:
Now that you have examined the details of the build process and have recreated them yourself, in the next section, let's explore the linking process.
In this section, let's look at a connection between two source files and how they end up in the same executable. Look at the sum function in the following figure:
The sum function's body is defined in SumFunc.cpp. It has a forward declaration in SumFunc.h. This way, the source files that want to use the sum function can know about its signature. Once they know its signature, they can call it and trust that the actual function definition will be there during runtime, without actually having any interaction with SumFunc.cpp where the function is defined.
After compilation, CxxTemplate.cpp, which calls the sum function, carries that call into its object file. Still, it does not know where the function definition is. The object file of SumFunc.cpp has that definition, but it has nothing to do with CxxTemplate.o, yet.
During the linking step, the linker matches the call in CxxTemplate.o with the definition in SumFunc.o. The call works fine in the executable as a result. Had the linker not found the definition of the sum function, it would have given a linker error.
The linker finds the sum function by its name and parameters. This is called resolving a symbol. The classes, functions, and variables defined in object files are placed in a symbol table and each reference to a symbol is resolved via a lookup at this table. When the symbol is not there, you receive a symbol could not be resolved error.
This took us through the two stages of the build process: compilation and linking. Notice that we used rather simpler commands compared to that of CMake, when we compiled our sources manually. Feel free to type man g++ to see all the options there. Later, we discussed linking and how symbols are resolved. We also talked about possible issues with the linking step. In the next section, we will learn about object files.
For the linking step to work without errors, we need to have all our symbol references match our symbol definitions. Most of the time, we can analyze how things will be resolved just by looking at the source files. Sometimes, in complex situations, we may have a difficult time understanding why a symbol is not being resolved. In such situations, looking at the contents of object files to investigate references and definitions can be useful to resolve the problem. Besides linker errors, understanding object file contents and how linking works in general is useful for a C++ programmer. Knowing what is happening under the hood may help programmers understand the whole process in a better way.
When our source code is compiled to object files, our statements and expressions are converted to assembly code, which is the low-level language that the CPU understands. Each instruction in assembly contains an operation, followed by operators, which are registers of the CPU. There are instructions to load data to and from registers and operate on values in registers. The objdump command in Linux helps us view the contents of these object files.
We will utilize Compiler Explorer, a nice online tool that is easier to use, where you can write code on the window to the left, and on the right, you can see the compiled assembly code. Here is the link to the Compiler Explorer: https://godbolt.org.
In this exercise, we will use Compiler Explorer to compile some simple C++ code in which we define and call a function. We will investigate the compiled assembly code to understand how exactly names are resolved and calls are made. This will give us a better understanding of what happens under the hood and how our code works in the executable format. Perform the following steps to complete the exercise:
int sum(int a, int b) {
return a + b;
}
int callSum() {
return sum(4, 5);
}
We have two functions; one is calling the other. Here is the compiled output:
While it's not very clear, you can more or less make out what it is doing. We are not going to dive into the details of assembly code, but we will focus on how symbols are resolved during the linker stage. Let's focus on the following lines for now:
sum(int, int):
...
callSum():
...
call sum(int, int)
...
The call sum(int, int) line does what you expect: it calls the preceding sum function and places the arguments in some registers. The important point here is that the functions are identified by their names and the types of their parameters in order. The linker looks for the appropriate function with this signature. Note that the return value is not a part of the signature.
Here, our lines became this:
_Z3sumii:
...
_Z7callSumv:
...
call _Z3sumii
...
The preceding are the mangled names of these functions. After _Z, the number tells us how long the function name is, so that the following letters are correctly interpreted. After the function name, we have v for no parameters and i for an int parameter. You can change these function signatures to view other possible types.
class MyClass {
private:
int a = 5;
int myPrivateFunc(int i) {
a = 4;
return i + a;
}
public:
int b = 6;
int myFunc(){
return sum(1, myPrivateFunc(b));
}
};
MyClass myObject;
int main() {
myObject.myFunc();
}
Here is the compiled version of these added lines:
You may be surprised that there is no class definition in the compiled code. The methods are similar to global functions, but with a twist: their mangled names contain the class name and they receive the object instance as a parameter. Creating an instance simply allocates space for the fields of the class.
During the linker phase, these mangled function names are used to match callers with callees. For callers that cannot find a callee, we get linker errors. Most linker errors can be resolved by carefully checking sources. However, in some cases, viewing the object file contents with objdump can help get to the bottom of the problem.
There are different levels of problems that you can come across while developing a C++ project:
To find and fix runtime errors, you have to interact with and monitor the running application in some way. An often-used technique is to add print statements to the code and monitor the logs that it generates, hoping to correlate the application behavior with the logs to pinpoint the region in code that has the problem. While this works for some cases, sometimes you need to take a closer look at the execution.
A debugger is a better tool to fight runtime errors. A debugger can let you run code line by line, continue running and pause on the lines that you want, investigate the values of memory, and pause on errors, among other things. This lets you watch what exactly is going on with memory as your program is running and identify the line of code that results in the unwanted behavior.
gdb is the canonical command-line debugger that can debug C++ programs. However, it may be difficult to use as debugging is inherently a visual task—you want to be able to look at lines of code, values of variables, and the output of the program at the same time. Luckily, Eclipse CDT includes a visual debugger that is easy to use.
You have been simply running your projects and viewing the output. Now you want to learn how to debug your code in detail. In this exercise, we will explore Eclipse CDT's debugging capabilities. Perform the following steps to complete the exercise:
Since we ran our project before, it should be there. If not, please go back and create it again.
At the moment, our code froze at the very first line of our main() function, which is shown with the green highlight and the arrow in the center code view. On the left, we see the running threads, of which there is only one. On the right, we see the variables that are accessible in this context. On the bottom, we see the gdb output that Eclipse uses behind the scenes to actually debug the executable. Now, there is not much to be debugged with our main function.
int i = 1, t = 0;
do {
t += i++;
} while (i <= 3);
std::cout << t << std::endl;
The post-increment operator mixed with the occasional do-while loop can be a head-scratcher for some. This is because we try to execute the algorithm in our heads. However, our debugger is perfectly able to run it step by step and show us what exactly happens during execution.
The preceding output clearly explains how the values are changing and why 6 is printed at the end.
Furthermore, the Expression view helps you calculate things that are otherwise not clear from the values that you browse.
You can press the red square in the toolbar or select Run | Terminate to stop debugging at any time. Another feature is breakpoints, which tell the debugger to pause whenever it goes to a line marked with a breakpoint. So far, we have been stepping through our code line by line, which may be very time-consuming in a large project. Instead, you usually want to continue the execution until it arrives at the code that you are interested in.
This is a handy feature for diving deeper into the function instead of simply stepping over it. Also, notice the call stack in the left debug view. You can always click on the lower entries to go and view the callers' contexts again.
This was a brief introduction to the Eclipse CDT debugger, which uses GDB under the hood to give you a visual debugging experience. You may find debugging useful when trying to understand runtime errors better and correcting the mistakes that caused them.
While visual debuggers are quite useful to identify and eliminate runtime errors or unintended program behavior, it is a better idea to write code that is less likely to have problems to begin with. One way to do that is to strive to write code that is easier to read and to understand. Then, finding problems in code becomes more like identifying contradictions between English sentences and less like solving cryptic puzzles. When you are writing code in a way that is understandable, your mistakes will often be apparent as you are making them and will be easier to spot when you come back to solve problems that slipped through.
After some unenjoyable maintenance experiences, you realize that the primary purpose of the programs that you write is not to make the computer do what you want to, but to tell the reader what the computer will do when the program runs. This usually means that you need to do more typing, which IDEs can help with. This may also mean that you sometimes write code that is not the most optimal in terms of execution time or memory used. If this goes against what you have learned, consider that you may be trading a minuscule amount of efficiency for the risk of being incorrect. With the vast processing power and memory at our disposal, you may be making your code unnecessarily cryptic and possibly buggy in the vain quest for efficiency. In the next sections, we will list some rules of thumb that may help you write code that is more readable.
C++ code, as in many other programming languages, is composed of program blocks. A function has a set of statements that form its body as a block. A loop's block statements will execute in iterations. An if statement's block executes if the given condition is true and the corresponding else statement's block executes otherwise.
Curly braces, or lack thereof for single-statement blocks, inform the computer, whereas indentation in the form of white space informs the human reader about the block structure. The lack of indentation, or misleading indentation, can make it very difficult for the reader to understand the structure of the code. Therefore, we should strive to keep our code well-indented. Consider the following two code blocks:
// Block 1
if (result == 2)
firstFunction();
secondFunction();
// Block 2
if (result == 2)
firstFunction();
secondFunction();
While they are identical in terms of execution, it is much clearer in the second one that firstFunction() is executed only if result is 2. Now consider the following code:
if (result == 2)
firstFunction();
secondFunction();
This is simply misleading. If the reader is not careful, they might easily assume that secondFunction() is executed only if result is 2. However, this code is identical to the two previous examples in terms of execution.
If you feel like correcting indentation is slowing you down, you can use your editor's formatting facilities to help you. In Eclipse, you can select a block of code and use Source | Correct Indentation to fix the indentation of that selection, or use Source | Format to also fix other formatting issues with code.
Beyond indentation, other formatting rules such as placing the curly brace at the correct line, inserting spaces around binary operators, and inserting a space after each comma are also very important formatting rules that you should abide by to keep your code well-formatted and easy to read.
In Eclipse, you can set formatting rules per-workspace in Window | Preferences | C/C++ | Code Style | Formatter or per-project in Project | Properties | C/C++ General | Formatter. You can either select one of the industry-standard styles such as K&R or GNU, or you can modify them and create your own. This becomes especially important when you use Source | Format to format your code. For example, if you choose to use spaces for indentation but Eclipse's formatting rules are set to tabs, your code would become a mixture of tabs and spaces.
In our code, we use identifiers to name many items—variables, functions, class names, types, and so on. For the computer, these identifiers are merely a sequence of characters to distinguish them from one another. However, for the reader, they're much more. The identifier should completely and unambiguously describe the item that it represents. At the same time, it should not be overly long. Furthermore, it should abide by the style standards that are in use.
Consider the following code:
studentsFile File = runFileCheck("students.dat");
bool flag = File.check();
if (flag) {
int Count_Names = 0;
while (File.CheckNextElement() == true) {
Count_Names += 1;
}
std::cout << Count_Names << std::endl;
}
While this is a perfectly valid piece of C++ code, it is quite difficult to read. Let's list the problems with it. First of all, let's look at the style problems of the identifiers. The studentsFile class name starts with a lowercase letter, which should have been uppercase instead. The File variable should have started with a lowercase letter. The Count_Names variable should have started with a lowercase letter and should not have had an underscore in it. The CheckNextElement method should have started with a lowercase letter. While these may seem arbitrary rules, being consistent in naming carries extra information about the name—when you see a word that starts with an uppercase letter, you immediately understand that it must be a class name. Furthermore, it is simply a distraction to have names that do not obey the standard in use.
Now, let's look beyond the style and inspect the names themselves. The first problematic name is the runFileCheck function. A method is an action that returns a value: its name should both clearly explain what it does as well as what it returns. "Check" is an overused word that is too vague for most situations. Yes, we checked it, it's there—what should we do with it then? In this case, it seems we actually read the file and create a File object. In that case, runFileCheck should have been readFile instead. This clearly explains the action being taken, and the return value is what you would expect. If you wanted to be more specific about the return value, readAsFile could be another alternative. Similarly, the check method is vague and should be exists instead. The CheckNextElement method is also vague and should be nextElementExists instead.
Another overused vague word is flag, which is often used for Boolean variables. The name suggests an on/off situation but gives no clue as to what its value would mean. In this case, its true value means that the file exists, and the false value means that the file does not exist. The trick for naming Boolean variables is to devise a question or statement that is correct when the value of the variable is true. In this example, fileExists and doesFileExist are two good choices.
Our next misnamed variable is Count_Names, or countNames with its correct capitalization. This is a bad name for an integer because the name does not suggest a number—it suggests an action that results in a number. Instead, an identifier such as numNames or nameCount would clearly communicate what the number inside means.
When we read code, the steps that are taken and the flow should make sense. Things that are done indirectly—byproducts of functions, multiple actions being done together in the name of efficiency, and so on—are things that make it difficult to understand your code for the reader. For example, let's look at the following code:
int *input = getInputArray();
int length = getInputArrayLength();
int sum = 0;
int minVal = 0;
for (int i = 0; i < length; ++i) {
sum += input[i];
if (i == 0 || minVal > input[i]) {
minVal = input[i];
}
if (input[i] < 0) {
input[i] *= -1;
}
}
Here, we have an array that we are processing in a loop. At first glance, it is not very clear what exactly the loop is doing. The variable names are helping us understand what is going on, but we must run the algorithm in our heads to be sure that what's being advertised by those names is really happening here. There are three different operations that are taking place in this loop. Firstly, we are finding the sum of all the elements. Secondly, we are finding the minimum element in the array. Thirdly, we are taking the absolute value of each element after these operations.
Now consider this alternative version:
int *input = getInputArray();
int length = getInputArrayLength();
int sum = 0;
for (int i = 0; i < length; ++i) {
sum += input[i];
}
int minVal = 0;
for (int i = 0; i < length; ++i) {
if (i == 0 || minVal > input[i]) {
minVal = input[i];
}
}
for (int i = 0; i < length; ++i) {
if (input[i] < 0) {
input[i] *= -1;
}
}
Now everything is much clearer. The first loop finds the sum of the inputs, the second loop finds the minimum element, and the third loop finds the absolute value of each element. Although it's much clearer and more understandable, you may feel like you are doing three loops, and therefore wasting CPU resources. The drive to create more efficient code may compel you to merge these loops. Note that the efficiency gains you have here would be minuscule; your program's time complexity would still be O(n).
While creating code, readability and efficiency are two constraints that can often be in competition. If you want to develop readable and maintainable code, you should always prioritize readability. Then, you should strive to develop code that is also efficient. Otherwise, code that has low readability risks being difficult to maintain, or worse, risks having bugs that are difficult to identify and fix. Your program's high efficiency would be irrelevant when it is producing incorrect results or when the cost of adding new features to it becomes too high.
There are style and indentation problems in the following code. Spaces are used inconsistently, and the indentation is incorrect. Also, the decision on single-statement if blocks having curly braces or not is inconsistent. The following piece of code has problems in terms of indentation, formatting, naming, and clarity:
//a is the input array and Len is its length
void arrayPlay(int *a, int Len) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < Len; ++i) {
S += a[i];
if (i == 0 || M > a[i]) {
M = a[i];
}
if (a[i] >= Lim_value) { flag = true;
}
if (a[i] < 0) {
a[i] *= 2;
}
}
}
Let's fix these problems and make it compatible with a common C++ code style. Perform the following steps to complete this exercise:
//a is the input array and Len is its length
void arrayPlay(int *a, int Len) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < Len; ++i) {
S += a[i];
if (i == 0 || M > a[i]) {
M = a[i];
}
if (a[i] >= Lim_value) {
flag = true;
}
if (a[i] < 0) {
a[i] *= 2;
}
}
}
Now that the code is a bit easier to follow, let's try to understand what it does. Thanks to the comments, we understand that we have an input array, a, whose length is Len. Better names for these would be input and inputLength.
void arrayPlay(int *input, int inputLength) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < inputLength; ++i) {
S += input[i];
if (i == 0 || M > input[i]) {
M = input[i];
}
if (input[i] >= Lim_value) {
flag = true;
}
if (input[i] < 0) {
input[i] *= 2;
}
}
}
void arrayPlay(int *input, int inputLength) {
int sum = 0;
int smallest = 0;
int topThreshold = 100;
bool isTopThresholdCrossed = true;
for (int i = 0; i < inputLength; ++i) {
sum += input[i];
if (i == 0 || smallest > input[i]) {
smallest = input[i];
}
if (input[i] >= topThreshold) {
isTopThresholdCrossed = true;
}
if (input[i] < 0) {
input[i] *= 2;
}
}
}
Now, let's see how we can make this code simpler and easier to understand. The preceding code is doing these things: calculating the sum of the input elements, finding the smallest one, determining whether the top threshold was crossed, and multiplying each element by two.
void arrayPlay(int *input, int inputLength) {
// find the sum of the input
int sum = 0;
for (int i = 0; i < inputLength; ++i) {
sum += input[i];
}
// find the smallest element
int smallest = 0;
for (int i = 0; i < inputLength; ++i) {
if (i == 0 || smallest > input[i]) {
smallest = input[i];
}
}
// determine whether top threshold is crossed
int topThreshold = 100;
bool isTopThresholdCrossed = true;
for (int i = 0; i < inputLength; ++i) {
if (input[i] >= topThreshold) {
isTopThresholdCrossed = true;
}
}
// multiply each element by 2
for (int i = 0; i < inputLength; ++i) {
if (input[i] < 0) {
input[i] *= 2;
}
}
}
Now the code is much clearer. While it's very easy to understand what each block is doing, we also added comments to make it even more clear. In this section, we gained a better understanding of how our code is converted to executables. Then, we discussed ways of identifying and resolving possible errors with our code. We finalized this with a discussion about how to write readable code that is less likely to have problems. In the next section, we will solve an activity wherein we will be making code more readable.
You may have code that is unreadable and contains bugs, either because you wrote it in a hurry, or you received it from someone else. You want to change the code to eliminate its bugs and to make it more readable. We have a piece of code that needs to be improved. Improve it step by step and resolve the issues using a debugger. Perform the following steps to implement this activity:
Here's the code for SpeedCalculator.cpp and SpeedCalculator.h that you will add to your project. You will modify them as a part of this activity:
// SpeedCalculator.h
#ifndef SRC_SPEEDCALCULATOR_H_
#define SRC_SPEEDCALCULATOR_H_
class SpeedCalculator {
private:
int numEntries;
double *positions;
double *timesInSeconds;
double *speeds;
public:
void initializeData(int numEntries);
void calculateAndPrintSpeedData();
};
#endif /* SRC_SPEEDCALCULATOR_H_ */
//SpeedCalculator.cpp
#include "SpeedCalculator.h"
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <cassert>
void SpeedCalculator::initializeData(int numEntries) {
this->numEntries = numEntries;
positions = new double[numEntries];
timesInSeconds = new double[numEntries];
srand(time(NULL));
timesInSeconds[0] = 0.0;
positions[0] = 0.0;
for (int i = 0; i < numEntries; ++i) {
positions[i] = positions[i-1] + (rand()%500);
timesInSeconds[i] = timesInSeconds[i-1] + ((rand()%10) + 1);
}
}
void SpeedCalculator::calculateAndPrintSpeedData() {
double maxSpeed = 0;
double minSpeed = 0;
double speedLimit = 100;
double limitCrossDuration = 0;
for (int i = 0; i < numEntries; ++i) {
double dt = timesInSeconds[i+1] - timesInSeconds[i];
assert (dt > 0);
double speed = (positions[i+1] - positions[i]) / dt;
if (maxSpeed < speed) {
maxSpeed = speed;
}
if (minSpeed > speed) {
minSpeed = speed;
}
if (speed > speedLimit) {
limitCrossDuration += dt;
}
speeds[i] = speed;
}
std::cout << "Max speed: " << maxSpeed << std::endl;
std::cout << "Min speed: " << minSpeed << std::endl;
std::cout << "Total duration: " <<
timesInSeconds[numEntries - 1] - timesInSeconds[0] << " seconds" << std::endl;
std::cout << "Crossed the speed limit for " << limitCrossDuration << " seconds"<< std::endl;
delete[] speeds;
}
The solution for this activity can be found on page 626.
In this chapter, we learned how to create C++ projects that are portable and maintainable. We first learned how to create CMake projects and how to import them to Eclipse CDT, giving us the choice to use the command line or an IDE. The rest of the chapter focused on eliminating various problems in our projects. First, we learned how to add unit tests to a project and how to use them to ensure that our code works as intended. We continued this with a discussion about the compilation and linking steps that our code goes through and observed the contents of object files to gain a better understanding of executable files. Then, we learned how to debug our code visually in the IDE to eliminate runtime errors. We finished this discussion with a number of rules of thumb that help create readable, understandable, and maintainable code. These methods will come in handy on your C++ journey. In the next chapter, we will learn more about C++'s type system and templates.
Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.
If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.
Please Note: Packt eBooks are non-returnable and non-refundable.
Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:
If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:
Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.
You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.
Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.
When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.
For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.