Advanced C++

1 (1 reviews total)
By Gazihan Alankus , Olena Lizina , Rakesh Mane and 2 more
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. 1. Anatomy of Portable C++ Software

About this book

C++ is one of the most widely used programming languages and is applied in a variety of domains, right from gaming to graphical user interface (GUI) programming and even operating systems. If you're looking to expand your career opportunities, mastering the advanced features of C++ is key.

The book begins with advanced C++ concepts by helping you decipher the sophisticated C++ type system and understand how various stages of compilation convert source code to object code. You'll then learn how to recognize the tools that need to be used in order to control the flow of execution, capture data, and pass data around. By creating small models, you'll even discover how to use advanced lambdas and captures and express common API design patterns in C++. As you cover later chapters, you'll explore ways to optimize your code by learning about memory alignment, cache access, and the time a program takes to run. The concluding chapter will help you to maximize performance by understanding modern CPU branch prediction and how to make your code cache-friendly.

By the end of this book, you'll have developed programming skills that will set you apart from other C++ programmers.

Publication date:
October 2019
Publisher
Packt
Pages
762
ISBN
9781838821135

 

1. Anatomy of Portable C++ Software

Learning Objectives

By the end of this chapter, you will be able to:

  • Establish the code-build-test process
  • Describe the various stages of compilation
  • Decipher complicated C++ type systems
  • Configure projects with unit tests
  • Convert source code to object code
  • Write readable code and debug it

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.

 

Introduction

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.

 

Managing C++ Projects

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.

The Code-Build-Test-Run Loop

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.

Building a CMake Project

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:

Figure 1.1: Folder structure of a sample template
Figure 1.1: 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.

Exercise 1: Using CMake to Generate Ninja Build Files

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.

Note

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:

  1. In a terminal window, type the following command to download the CxxTemplate repository from GitHub onto your local system:

    git clone https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project

    The output of the previous command is similar to the following:

    Figure 1.2: Checking out the sample project from GitHub
    Figure 1.2: Checking out the sample project from GitHub

    Now you have the source code in the CxxTemplate folder.

  2. Navigate into the CxxTemplate folder by typing the following command in the terminal:

    cd CxxTemplate

  3. Now you can list all the files in the project by typing the following command:

    find .

  4. Generate our Ninja build file using the cmake command in the CxxTemplate folder. To do that, write the following command:

    cmake -Bbuild -H. -GNinja

    The output of the preceding command is as follows:

    Figure 1.3: Generating the Ninja build file
    Figure 1.3: Generating the Ninja build file

    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.

  5. Run the following commands to list the project files and to check the files that were created inside the build folder:

    ls

    ls build

    The preceding command will show the following output in the terminal:

    Figure 1.4: Files in the build folder
    Figure 1.4: Files in the build folder

    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.

    Note

    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.

  6. Now, go into the build folder and build our project by typing the following commands in the terminal:

    cd build

    ninja

    You should see a final output like the following:

    Figure 1.5: Building with ninja
    Figure 1.5: Building with ninja
  7. Type ls in the build folder and check whether we have generated the CxxTemplate executable or not:

    ls

    The previous command yields the following output in the terminal:

    Figure 1.6: Files in the build folder after running ninja
    Figure 1.6: Files in the build folder after running ninja

    In the preceding figure, you can see that the CxxTemplate executable is generated.

  8. In the terminal, type the following command to run the CxxTemplate executable:

    ./CxxTemplate

    The previous command in the terminal will provide the following output:

Figure 1.7: Running the executable

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.

 

Importing 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.

Exercise 2: Importing the CMake File into Eclipse CDT

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:

  1. Open Eclipse CDT.
  2. Create a new C++ project in the location of our current project (the folder that contains the CMakeLists.txt file and the src folder). Go to File | New | Project. A New Project dialog box appears like the one in the following screenshot:
    Figure 1.8: New Project dialog box
    Figure 1.8: New Project dialog box
  3. Select the C++ Project option and click on the Next button. A C++ Project dialog box appears like the one in the following screenshot:
    Figure 1.9: C++ Project dialog box
    Figure 1.9: C++ Project dialog box
  4. Accept everything, including switching to the C/C++ perspective, and click Finish.
  5. Click on the Restore button at the top-left corner to view the newly created project:
    Figure 1.10: The Restore button
    Figure 1.10: The Restore button
  6. Click on the CxxTemplate project. Go to Project | Properties, then select Tool Chain Editor under C/C++ Build from the left pane and set Current builder to CMake Builder (portable). Then, click on the Apply and Close button:
    Figure 1.11: Project properties
    Figure 1.11: Project properties
  7. Then, choose the Project | Build All menu item to build the project:
    Figure 1.12: Building the project
    Figure 1.12: Building the project
  8. In the following Console pane, you will see the output of CMake as if you called it from the command line, followed by a call to make all that actually builds our project:
    Figure 1.13: The build output
    Figure 1.13: The build output
  9. If you did not get any errors in the previous steps, you can run the project using the menu item Run | Run. If you are given some options, choose Local C/C++ Application and CxxTemplate as the executable:
    Figure 1.14: Running a project
    Figure 1.14: Running a project
  10. When it runs, you will see the output of the program in the Console pane as follows:
Figure 1.15: Output of the project
Figure 1.15: Output of the project

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.

Exercise 3: Adding New Source Files to CMake and Eclipse CDT

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:

  1. First, open the project that we have been using until now. In the Project Explorer pane on the left, expand the root entry, CxxTemplate, and you will see the files and folders of our project. Right-click the src folder and select New | Class from the pop-up menu:
    Figure 1.16: Creating a new class
    Figure 1.16: Creating a new class
  2. In the dialog box that opened, type ANewClass for the class name. When you click on the Finish button, you will see the ANewClass.cpp and ANewClass.h files generated under the src folder.
  3. Now, let's write some code into the ANewClass class and access it from the CxxTemplate class that we already had. Open ANewClass.cpp and change the beginning of the file to match the following, and then save the file:

    #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:

    Figure 1.17: Analyzer warning
    Figure 1.17: Analyzer warning

    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.

  4. Open the ANewClass.h file, add the following code, and save the file:

    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.

  5. Now, let's use this class from our other class, CxxTemplate.cpp. Open that file, perform the following modifications, and save the file. Here, we are first importing header files and in the constructor of CxxApplication, we are printing text to the console. Then, we are creating a new instance of ANewClass and calling its run method:

    #include "CxxTemplate.h"

    #include "ANewClass.h"

    #include <string>

    ...

    CxxApplication::CxxApplication( int argc, char *argv[] ) {

      std::cout << "Hello CMake." << std::endl;

      ::ANewClass anew;

      anew.run();

    }

    Note

    The complete code of this file can be found here: https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Exercise03/src/CxxTemplate.cpp.

  6. Try to build the project by clicking on the Project | Build All menu options. You will get some undefined reference errors in two lines. This is because our project is built with CMake's rules and we did not let CMake know about this new file. Open the CMakeLists.txt file, make the following modification, and save the file:

    add_executable(CxxTemplate

      src/CxxTemplate.cpp  

      src/ANewClass.cpp

    )

    Try to build the project again. This time you should not see any errors.

  7. Run the project using the Run | Run menu option. You should see the following output in the terminal:
Figure 1.18: Program output
Figure 1.18: Program output

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.

Activity 1: 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:

  1. Open the project that we created in the earlier exercise in the Eclipse IDE.
  2. Add the SumFunc.cpp and SumFunc.h file pair to the project.
  3. Create a simple function named sum that returns the sum of two integers.
  4. Call the function from the CxxTemplate class constructor.
  5. Build and run the project in Eclipse.

The expected output should be similar to the following:

Figure 1.19: Final output
Figure 1.19: Final output

Note

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 Testing

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.

Preparing for the Unit Tests

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.

Exercise 4: Preparing Our Project for Unit Testing

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:

  1. Open Eclipse CDT and select the CxxTemplate project that we have been using.
  2. Create a new folder named tests as we will perform all our tests there.
  3. Edit our base CMakeLists.txt file to allow tests in the tests folder. Note that we already had code to find the GTest package that brings GoogleTest capability to CMake. We will add our new lines just after that:

    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.

  4. Create another CMakeLists.txt file inside our tests folder. This will be used because of the add_subdirectory(tests) line that we had in our main CMakeLists.txt file. This tests/CMakeLists.txt file will manage the test sources.
  5. Add the following code in the tests/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.

  6. Create a tests/CanTest.cpp file. Add this code to simply verify that tests are running, without actually testing anything in our actual project:

    #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.

Building, Running, and Writing Unit 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.

Exercise 5: Building and Running Tests

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:

  1. Open our CMake project in Eclipse CDT.
  2. To build the tests, simply build the project just like you did before. Here is the output of building the project one more time from Eclipse after a full build using Project | Build All:
    Figure 1.20: Build operation and its output
    Figure 1.20: Build operation and its output
  3. If you do not see this output, your console may be in the wrong view. You can correct it as shown in the following figures:
    Figure 1.21: Viewing the correct console output
    Figure 1.21: Viewing the correct console output
    Figure 1.22: Viewing the correct console output
    Figure 1.22: Viewing the correct console output

    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.

  4. We ran CxxTemplate before and will not see any extra output now. Run the other executable by typing the following command in the terminal while we are in our project folder:

    ./build/Debug/tests/tests

    The preceding code generates the following output in the terminal:

    Figure 1.23: Running the tests executable
    Figure 1.23: Running the tests executable

    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.

  5. One of the ways you can run your tests is by using the ctest command. Write the following commands in the terminal while you are in the project folder. We go to the folder where the tests executable resides, run ctest there, and come back:

    cd build/Debug/tests

    ctest

    cd ../../..

    And here is the output that you will see:

    Figure 1.24: Running ctest
    Figure 1.24: Running ctest

    Note

    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#.

  6. Another way to run the tests is to run them inside Eclipse, in a nice graphical report format. For this, we will create a run configuration that is test-aware. In Eclipse, click on Run | Run Configurations…, right-click C/C++ Unit on the left, and select New Configuration.
  7. Change the name from CxxTemplate Debug to CxxTemplate Tests as follows:
    Figure 1.25: Changing the name of the run configuration
    Figure 1.25: Changing the name of the run configuration
  8. Under C/C++ Application, select the Search Project option:
    Figure 1.26: Run Configurations
    Figure 1.26: Run Configurations
  9. Choose tests in the new dialog:
    Figure 1.27: Creating the test run configuration and selecting the tests executable
    Figure 1.27: Creating the test run configuration and selecting the tests executable
  10. Next, go to the C/C++ Testing tab and select Google Tests Runner in the dropdown. Click on Apply at the bottom of the dialog and click on the Run option for the test that we have to run for the first time:
    Figure 1.28: Run Configurations
    Figure 1.28: Run Configurations
  11. In the upcoming runs, you can either click the dropdown next to the play button in the toolbar, or choose Run | Run History to choose CxxTemplate Tests:
Figure 1.29: Finalizing the run configuration settings and selecting a configuration to run
Figure 1.29: Finalizing the run configuration settings and selecting a configuration to run

The result will be similar to the following screenshot:

Figure 1.30: Run results of the unit test
Figure 1.30: Run results of the unit test

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.

Exercise 6: Testing the Functionality of 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:

  • src/SumFunc.h:

    #ifndef SRC_SUMFUNC_H_

    #define SRC_SUMFUNC_H_

    int sum(int a, int b);

    #endif /* SRC_SUMFUNC_H_ */

  • src/SumFunc.cpp:

    #include "SumFunc.h"

    #include <iostream>

    int sum(int a, int b) {

      return a + b;

    }

  • Relevant lines of CMakeLists.txt:

    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:

  1. Open our CMake project in Eclipse CDT.
  2. Add a new test source file (tests/SumFuncTest.cpp) with the following content:

    #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.

  3. Make the following change in the tests/CMakeLists.txt file to build the test:

    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.

  4. Build the project and run the test as before. You should see the following report:
    Figure 1.31: Output after running the test
    Figure 1.31: Output after running the test

    We can add such tests to our project and all of them will appear on the screen as shown in the preceeding screenshot.

  5. Now, let's add one more test that will actually fail. In the tests/SumFuncTest.cpp file, make the following change:

    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.

  6. Now, build the project and run the test. You should see this report:
    Figure 1.32: The build report
    Figure 1.32: The build report

    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.

  7. Let's change the test and fix it. Change the test so that we expect the sum of -3 and 3 to be 0. Rename the test to reflect what this test actually does:

    TEST_F(SumFuncTest, CanSumCorrectly) {

      EXPECT_EQ(7, sum(3, 4));

    }

    // change this part

    TEST_F(SumFuncTest, CanUseNegativeValues) {

      EXPECT_EQ(0, sum(3, -3));

    }

  8. Run it now and observe in the report that all the tests pass:
Figure 1.33: Test execution is successful
Figure 1.33: Test execution is successful

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.

Activity 2: 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:

  1. Open the project that we have created in the Eclipse IDE.
  2. Add the LinearMotion1D.cpp and LinearMotion1D.h file pair to the project that contains the LinearMotion1D class. In this class, create two double fields: position and velocity. Also, create an advanceTimeBy(double dt) function that modifies position.
  3. Write tests for this in the tests/LinearMotion1DTest.cpp file. Write two tests that represent motion in two different directions.
  4. Build and run it in the Eclipse IDE.
  5. Verify that the tests have passed.

The final test results should look similar to the following:

Figure 1.34: Final test results
Figure 1.34: Final test results

Note

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.

 

Understanding Compilation, Linking, and Object File Contents

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.

Compilation and Linking Steps

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.

Exercise 7: Identifying Build Steps

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:

  1. Open the terminal.
  2. Navigate to the build folder wherein our Makefile file resides by typing the following command:

    cd build/Debug

  3. Clean the project and run the build in VERBOSE mode using the following command:

    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:

    Figure 1.35: The build process part 1
    Figure 1.35: The build process part 1
    Figure 1.36: The build process part 2
    Figure 1.36: The build process part 2
    Figure 1.37: The full build output
    Figure 1.37: The full build output

    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

  4. The c++ command here is just a symbolic link to the g++ compiler. To see that it's actually a chain of symbolic links, type the following command:

    namei /usr/bin/c++

    You will see the following output:

    Figure 1.38: The chain of symbolic links for /usr/bin/c++
    Figure 1.38: The chain of symbolic links for /usr/bin/c++

    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:

    Figure 1.39: Execution stages of a C++ project
    Figure 1.39: Execution stages of a C++ project

    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.

  5. To understand this process even further, let's carry out the compilation steps ourselves. In the terminal, go to the project folder and create a new folder named mybuild using the following commands:

    cd ~/CxxTemplate

    mkdir mybuild

  6. Then, run the following commands to compile the CPP source files to object files:

    /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

  7. Go into the mybuild directory and see what's there using the following command:

    cd mybuild

    ls

    We see the following output as expected. These are our object files:

    Figure 1.40: Compiled object files
    Figure 1.40: Compiled object files
  8. In the next step, link the object files together to form our executable. Type the following command:

    /usr/bin/c++  CxxTemplate.o ANewClass.o SumFunc.o LinearMotion1D.o  -o CxxTemplate

  9. Now, let's see our executable among the list of files here by typing the following command:

    ls

    This shows the new CxxTemplate file in the following figure:

    Figure 1.41: Linked executable file
    Figure 1.41: Linked executable file
  10. Now, run our executable by typing the following command:

    ./CxxTemplate

    And see the output that we had before:

    Figure 1.42: Executable file output
Figure 1.42: Executable file output

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.

The Linking Step

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:

Figure 1.43: The linking process
Figure 1.43: The linking process

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.

Diving Deeper: Viewing 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.

Note

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.

Exercise 8: Exploring Compiled Code

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:

  1. Add the following code in Compiler Explorer:

    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:

    Figure 1.44: The compiled code
    Figure 1.44: The compiled code

    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.

  2. Disable the Demangle checkbox and see how these function names are actually stored:
    Figure 1.45: Compiled code without demangling
    Figure 1.45: Compiled code without demangling

    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.

  3. Now, let's look at how classes are compiled. Add the following code into Compiler Explorer under the existing code:

    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:

    Figure 1.46: The compiled version
Figure 1.46: The compiled version

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.

 

Debugging C++ Code

There are different levels of problems that you can come across while developing a C++ project:

  • First, you may receive compiler errors. These can happen because of a mistake you made in syntax, or a wrong choice for a type, and so on. The compiler is the first hoop you have to jump through, and it catches some mistakes you may have made.
  • The second hoop is the linker. There, a typical mistake is to use something that is declared, but not actually defined. This happens often when you use the wrong header file for a library—the header file advertises a certain signature that does not exist in any of the source files or libraries. Once you also jump through the linker hoop, your program is ready to execute.
  • Now, the next hoop to jump through is to avoid any runtime errors. Your code may have compiled and linked properly, but it may be doing things that do not work, such as dereferencing a null pointer or dividing by zero.

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.

Exercise 9: Debugging with Eclipse CDT

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:

  1. Open the CMake project in Eclipse CDT.
  2. To ensure that we have an existing run configuration, click Run | Run Configurations. There, you should see a CxxTemplate entry under C/C++ Application.

    Note

    Since we ran our project before, it should be there. If not, please go back and create it again.

  3. Close the dialog box to continue.
  4. To start the debugger, find the toolbar entry that looks like an insect (bug) and click on the dropdown next to it. Select CxxTemplate to debug the main application. If it asks you to switch to the debug perspective, accept. Now, this is what Eclipse will look like:
    Figure 1.47: Eclipse debug screen
    Figure 1.47: Eclipse debug screen

    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.

  5. Click Step Over either under the Run menu or in the toolbar a couple of times and see that the application will terminate soon. In the end, you will see the libc-start.c library, which is the caller of the main function. You can close it and switch to your source files when done. When you do not see the red stop button anymore, you know that the program execution is over.
  6. Edit our main function by adding the following code:

    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.

  7. Start debugging after adding the preceding code. Click on the dropdown next to the Debug button in the toolbar and select CxxTemplate. Press F6 a couple of times to step over in the code. It will show us how the variables change as well as the line of code that will be executed next:
    Figure 1.48: Stepping over the code
    Figure 1.48: Stepping over the code
  8. Seeing the variables change after the execution of each line of code makes the algorithm much clearer to understand. As you press F6, note that the following are the values after each execution of the t += i++; line:
    Figure 1.49: Variable states through time
    Figure 1.49: Variable states through time

    The preceding output clearly explains how the values are changing and why 6 is printed at the end.

  9. Explore other features of the debugger. While the variable view is useful, you can also hover over any variable and browse its value:
    Figure 1.50: View option of the debugger
    Figure 1.50: View option of the debugger

    Furthermore, the Expression view helps you calculate things that are otherwise not clear from the values that you browse.

  10. Click on Expression on the right-hand side and click on the Add button:
    Figure 1.51: Adding an expression
    Figure 1.51: Adding an expression
  11. Type t+i and hit Enter. Now you see the total in the list of expressions:
    Figure 1.52: Expression view with a new expression
    Figure 1.52: Expression view with a new expression

    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.

  12. Now, instead of going line by line, add a breakpoint in the line that does the printing. For this, double-click on the area to the left of the line number of this line. In the following figure, the dot represents a breakpoint:
    Figure 1.53: Working with breakpoints
    Figure 1.53: Workin with breakpoints
  13. Now start the debugger. As usual, it will start paused. Now select Run | Resume or click on the toolbar button. It will run the three executions of the loop and pause at our breakpoint. This way, we saved time by stepping through code that we are not investigating:
    Figure 1.54: Working with the debugger
    Figure 1.54: Working with the debugger
  14. While we have been dealing with the loop that we added, we ignored the line that creates an app object. The Step Over command was skipping this line. However, we also have the option to go into the constructor call that is in this line. For that, we will use Run | Step Into or the corresponding toolbar button.
  15. Stop the debugger and start it again. Click on Step Over to go to the line where the application is created:
    Figure 1.55: Working with the debugger – the Step Over option
    Figure 1.55: Working with the debugger – the Step Over option
  16. The highlighted is the next line that would be executed if we step over again. Instead, press the Step Into button. This will take us into the constructor call:
Figure 1.56: Working with the debugger – the Step Into option
Figure 1.56: Working with the debugger – the Step Into option

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.

 

Writing Readable Code

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.

Indentation and Formatting

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.

Use Meaningful Names as Identifiers

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.

Keeping Algorithms Clear and Simple

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.

Exercise 10: Making Code Readable

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:

  1. Open Eclipse CDT.
  2. Create a new ArrayPlay.cpp file in the src folder and paste the preceding code. Make sure you do not have any text selected. Then, go to Source | Format from the top menu and accept the dialog to format the entire file. This makes our code look like the following:

    //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.

  3. Let's make that first change and rename a to input. If you are using Eclipse, you can select Refactor | Rename to rename one occurrence and all others will be renamed as well. Do the same for Len and rename it to inputLength.
  4. The updated code will look like the following. Note that we do not need the comment anymore since parameter names are self-explanatory:

    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;

            }

        }

    }

  5. We have a couple of other variables defined before the loop. Let's try to understand them. It seems all it does with S is to add each element to it. Therefore, S must be sum. M, on the other hand, seems to be the minimum element—let's name it smallest.
  6. Lim_value seems to be a threshold, where we simply want to know whether it has been crossed. Let's rename it topThreshold. The flag variable is set to true if this threshold is crossed. Let's rename it to isTopThresholdCrossed. Here is the state of the code after these changes with Refactor | Rename:

    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.

  7. Since all of these are done in the same loop, the algorithm is not very clear now. Fix that and have four separate loops:

    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.

Activity 3: 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:

  1. Below you will find the source for SpeedCalculator.cpp and SpeedCalculator.h. They contain the SpeedCalculator class. Add these two files to your project.
  2. Create an instance of this class in your main() function and call its run() method.
  3. Fix the style and naming problems in the code.
  4. Simplify the code to make it more understandable.
  5. Run the code and observe the problem at runtime.
  6. Use the debugger to fix the problem.

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;

}

Note

The solution for this activity can be found on page 626.

 

Summary

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.

About the Authors

  • Gazihan Alankus

    Gazihan Alankus holds a PhD in computer science from Washington University in St. Louis. Currently, he is an assistant professor at the Izmir University of Economics in Turkey. He teaches and conducts research on game development, mobile application development, and human-computer interaction. He is a Google developer expert in Dart and develops Flutter applications with his students in his company Gbot, which he founded in 2019.

    Browse publications by this author
  • Olena Lizina

    Olena Lizina is a software developer with 5 years experience in C++. She has practical knowledge of developing systems for monitoring and managing remote computers with a lot of users for an international product company. For the last 4 years, she has been working for international outsourcing companies on automotive projects for well-known automotive concerns. She has been participating in the development of complex and high-performance applications on different projects, such as HMI (Human Machine Interface), navigation, and applications for work with sensors.

    Browse publications by this author
  • Rakesh Mane

    Rakesh Mane has over 18 years of experience in the software industry. He has worked with proficient programmers from a variety of regions such as India, the US, and Singapore. He has mostly worked in C++, Python, shell scripting, and database. In his spare time, he likes to listen to music and travel. Also, he likes to play with, experiment with, and break things using software tools and code.

    Browse publications by this author
  • Vivek Nagarajan

    Vivek Nagarajan is a self-taught programmer who started out in the 1980s on 8-bit systems. He has worked on a large number of software projects and has 14 years of professional experience with C++. Aside from this, he has worked on a wide variety of languages and frameworks across the years. He is an amateur powerlifter, DIY enthusiast, and motorcycle racer. He currently works as an independent software consultant.

    Browse publications by this author
  • Brian Price

    Brian Price has over 30 years experience working in a variety of languages, projects, and industries, including over 20 years' experience in C++. He was worked on power station simulators, SCADA systems, and medical devices. He is currently crafting software in C++, CMake, and Python for a next-generation medical device. He enjoys solving puzzles and the Euler project in a variety of languages.

    Browse publications by this author

Latest Reviews

(1 reviews total)
incorrect publication dates on many products.

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial