Book Image

Advanced C++

By : Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price
5 (1)
Book Image

Advanced C++

5 (1)
By: Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price

Overview of 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.
Table of Contents (11 chapters)
7
6. Streams and I/O

Chapter 2A - No Ducks Allowed – Types and Deduction

Activity 1: Graphics Processing

In this activity, we will implement two classes (Point3d and Matrix3d), along with the multiplication operators so that we can translate, scale, and rotate points. We will also implement some helper methods that create the necessary matrices for the transformations. Follow these steps to implement this activity:

  1. Load the prepared project from the Lesson2A/Activity01 folder and configure the Current Builder for the project to be CMake Build (Portable). Build and configure the launcher and run the unit tests (which fail). Recommend that the name that's used for the test runner is L2AA1graphicstests.

    CMake Configuration

    Follow step 9 of Exercise 1, Declaring Variables and Exploring Sizes, to configure the project as a CMake project.

  2. Add a test for the Point3d class to verify that the default constructor creates an origin point [0, 0, 0, 1].
  3. Open the point3dTests.cpp file and add the following line at the top.
  4. Replace the failing existing test with the following test:

    TEST_F(Point3dTest, DefaultConstructorIsOrigin)

    {

        Point3d pt;

        float expected[4] = {0,0,0,1};

        for(size_t i=0 ; i < 4 ; i++)

        {

            ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";

        }

    }

    This test requires us to write an access operator.

  5. Replace the current class definition in point3d.hpp file with the following code:

    include <cstddef>

    class Point3d

    {

    public:

        static constexpr size_t NumberRows{4};

        float operator()(const int index) const

        {

            return m_data[index];

        }

    private:

        float m_data[NumberRows];

    };

    The test now builds and runs but fails.

  6. Add the declaration for the default constructor to the Point3d declaration:

    Point3d();

  7. Add the implementation to the point3d.cpp file:

    Point3d::Point3d()

    {

        for(auto& item : m_data)

        {

            item = 0;

        }

        m_data[NumberRows-1] = 1;

    }

    The test now builds, runs, and passes.

  8. Add the next test:

    TEST_F(Point3dTest, InitListConstructor3)

    {

        Point3d pt {5.2, 3.5, 6.7};

        float expected[4] = {5.2,3.5,6.7,1};

        for(size_t i=0 ; i < 4 ; i++)

        {

            ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";

        }

    }

    This test fails to compile. Therefore, we need to implement another constructor – the one that takes std::initializer_list<> as an argument.

  9. Add the following include to the header file:

    #include <initializer_list>

  10. Add the following constructor declaration to the Point3d class in the header file:

    Point3d(std::initializer_list<float> list);

  11. Add the following code to the implementation file. This code ignores error handling, which will be added in Lesson 3, The Distance Between Can and Should – Objects, Pointers, and Inheritance:

    Point3d::Point3d(std::initializer_list<float> list)

    {

        m_data[NumberRows-1] = 1;

        int i{0};

        for(auto it1 = list.begin();

            i<NumberRows && it1 != list.end();

            ++it1, ++i)

        {

            m_data[i] = *it1;

        }

    }

    The tests should now build, run, and pass.

  12. Add the following test:

    TEST_F(Point3dTest, InitListConstructor4)

    {

        Point3d pt {5.2, 3.5, 6.7, 2.0};

        float expected[4] = {5.2,3.5,6.7,2.0};

        for(size_t i=0 ; i < 4 ; i++)

        {

            ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";

        }

    }

    The tests should still build, run, and pass.

  13. It is now time to refactor the test cases by moving the verification loop into a templated function in the Point3dTest class. Add the following template inside this class:

    template<size_t size>

    void VerifyPoint(Point3d& pt, float (&expected)[size])

    {

        for(size_t i=0 ; i< size ; i++)

        {

            ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";

        }

    }

  14. This now means that the last test can be rewritten as follows:

    TEST_F(Point3dTest, InitListConstructor4)

    {

        Point3d pt {5.2, 3.5, 6.7, 2.0};

        float expected[4] = {5.2,3.5,6.7,2.0};

        VerifyPoint(pt, expected);

    }

    It is just as important to keep your tests readable in the same way as your production code.

  15. Next, add support for the equality and inequality operators through the following tests:

    TEST_F(Point3dTest, EqualityOperatorEqual)

    {

        Point3d pt1 {1,3,5};

        Point3d pt2 {1,3,5};

        ASSERT_EQ(pt1, pt2);

    }

    TEST_F(Point3dTest, EqualityOperatorNotEqual)

    {

        Point3d pt1 {1,2,3};

        Point3d pt2 {1,2,4};

        ASSERT_NE(pt1, pt2);

    }

  16. To implement these, add the following declarations/definition in the header file:

    bool operator==(const Point3d& rhs) const;

    bool operator!=(const Point3d& rhs) const

    {

        return !operator==(rhs);

    }

  17. Now, add the equality implementation in the .cpp file:

    bool Point3d::operator==(const Point3d& rhs) const

    {

        for(int i=0 ; i<NumberRows ; i++)

        {

            if (m_data[i] != rhs.m_data[i])

            {

                return false;

            }

        }

        return true;

    }

  18. When we first added Point3d, we implemented a constant accessor. Add the following test, where we need a non-constant accessor so that we can assign it to the member:

    TEST_F(Point3dTest, AccessOperator)

    {

        Point3d pt1;

        Point3d pt2 {1,3,5};

        pt1(0) = 1;

        pt1(1) = 3;

        pt1(2) = 5;

        ASSERT_EQ(pt1, pt2);

    }

  19. To get this test to build, add the following accessor to the header:

    float& operator()(const int index)

    {

        return m_data[index];

    }

    Note that it returns a reference. Thus, we can assign it to a member value.

  20. To finish off Point3d, add lines to the class declaration for the default copy constructor and copy assignment:

    Point3d(const Point3d&) = default;

    Point3d& operator=(const Point3d&) = default;

  21. Now, add the Matrix3d classes. First, create two empty files, matrix3d.hpp and matrix3d.cpp, in the top-level folder of the current project and then add an empty file in the tests folder called matrix3dTests.cpp.
  22. Open the CmakeLists.txt file in the top folder and add matrix3d.cpp to the following line:

    add_executable(graphics point3d.cpp main.cpp matrix3d.cpp)

  23. Open the CMakeLists.txt file in the tests folder, add ../matrix3d.cpp to the definition of SRC_FILES, and add matrix3dTests.cpp to the definition of TEST_FILES:

    SET(SRC_FILES

        ../matrix3d.cpp

        ../point3d.cpp)

    SET(TEST_FILES

        matrix3dTests.cpp

        point3dTests.cpp)

    The existing point3d tests should still build, run, and pass if you made those changes correctly.

  24. Add the following test plumbing to matrix3dTests.cpp:

    #include "gtest/gtest.h"

    #include "../matrix3d.hpp"

    class Matrix3dTest : public ::testing::Test

    {

    public:

    };

    TEST_F(Matrix3dTest, DummyTest)

    {

        ASSERT_TRUE(false);

    }

  25. Build and run the tests. The test that we just added should fail.
  26. Replace DummyTest with the following test in matrix3dTests.cpp:

    TEST_F(Matrix3dTest, DefaultConstructorIsIdentity)

    {

        Matrix3d mat;

        for( int row{0} ; row<4 ; row++)

            for( int col{0} ; col<4 ; col++)

            {

                int expected = (row==col) ? 1 : 0;

                ASSERT_FLOAT_EQ(expected, mat(row,col)) << "cell[" << row << "][" << col << "]";

            }

    }

    Building the tests will now fail because we have not defined the Matrix3d class. We will do this now in matrix3d.hpp.

  27. Add the following definition to matrix3d.hpp:

    class Matrix3d

    {

    public:

        float operator()(const int row, const int column) const

        {

            return m_data[row][column];

        }

    private:

        float m_data[4][4];

    };

    The tests will now build but still fail because we haven't created a default constructor that creates an identity matrix.

  28. Add the declaration of the default constructor to the header file in the public section of Matrix3d:

    Matrix3d();

  29. Add this definition to matrix3d.cpp:

    #include "matrix3d.hpp"

    Matrix3d::Matrix3d()

    {

        for (int i{0} ; i< 4 ; i++)

            for (int j{0} ; j< 4 ; j++)

                m_data[i][j] = (i==j);

    }

    The tests now build and pass.

  30. Refactor the code slightly to make it more readable. Modify the header to read like so:

    #include <cstddef>   // Required for size_t definition

    class Matrix3d

    {

    public:

        static constexpr size_t NumberRows{4};

        static constexpr size_t NumberColumns{4};

        Matrix3d();

        float operator()(const int row, const int column) const

        {

        return m_data[row][column];

        }

    private:

        float m_data[NumberRows][NumberColumns];

    };

  31. Update the matrix3d.cpp file to use the constants:

    Matrix3d::Matrix3d()

    {

        for (int i{0} ; i< NumberRows ; i++)

            for (int j{0} ; j< NumberColumns ; j++)

                m_data[i][j] = (i==j);

    }

  32. Rebuild the tests and make sure that they still pass.
  33. Now, we need to add the initializer list constructor. To do that, add the following test:

    TEST_F(Matrix3dTest, InitListConstructor)

    {

        Matrix3d mat{ {1,2,3,4}, {5,6,7,8},{9,10,11,12}, {13,14,15,16}};

        int expected{1};

        for( int row{0} ; row<4 ; row++)

            for( int col{0} ; col<4 ; col++, expected++)

            {

                ASSERT_FLOAT_EQ(expected, mat(row,col)) << "cell[" << row << "][" << col << "]";

            }

    }

  34. Add the include file for the initializer list support and declare the constructor in matrix3d.hpp:

    #include <initializer_list>

    class Matrix3d

    {

    public:

        Matrix3d(std::initializer_list<std::initializer_list<float>> list);

  35. Finally, add the implementation of the constructor to the .cpp file:

    Matrix3d::Matrix3d(std::initializer_list<std::initializer_list<float>> list)

    {

        int i{0};

        for(auto it1 = list.begin(); i<NumberRows ; ++it1, ++i)

        {

            int j{0};

            for(auto it2 = it1->begin(); j<NumberColumns ; ++it2, ++j)

                m_data[i][j] = *it2;

        }

    }

  36. To improve the readability of our tests, add a helper method to the test framework. In the Matrix3dTest class, declare the following:

    static constexpr float Epsilon{1e-12};

    void VerifyMatrixResult(Matrix3d& expected, Matrix3d& actual);

  37. Add the definition of the helper method:

    void Matrix3dTest::VerifyMatrixResult(Matrix3d& expected, Matrix3d& actual)

    {

        for( int row{0} ; row<4 ; row++)

            for( int col{0} ; col<4 ; col++)

            {

            ASSERT_NEAR(expected(row,col), actual(row,col), Epsilon)

    << "cell[" << row << "][" << col << "]";

            }

    }

  38. Write a test to multiply two matrices and get a new matrix (expected will be calculated by hand):

    TEST_F(Matrix3dTest, MultiplyTwoMatricesGiveExpectedResult)

    {

        Matrix3d mat1{ {5,6,7,8}, {9,10,11,12}, {13,14,15,16}, {17,18,19,20}};

        Matrix3d mat2{ {1,2,3,4}, {5,6,7,8},    {9,10,11,12},  {13,14,15,16}};

        Matrix3d expected{ {202,228,254,280},

                           {314,356,398,440},

                           {426,484,542,600},

                           {538,612,686,760}};

        Matrix3d result = mat1 * mat2;

        VerifyMatrixResult(expected, result);

    }

  39. In the header file, define operator*=:

    Matrix3d& operator*=(const Matrix3d& rhs);

    Then, implement the inline version of operator* (outside the class declaration):

    inline Matrix3d operator*(const Matrix3d& lhs, const Matrix3d& rhs)

    {

        Matrix3d temp(lhs);

        temp *= rhs;

        return temp;

    }

  40. And the implementation to the matrix3d.cpp file:

    Matrix3d& Matrix3d::operator*=(const Matrix3d& rhs)

    {

        Matrix3d temp;

        for(int i=0 ; i<NumberRows ; i++)

            for(int j=0 ; j<NumberColumns ; j++)

            {

                temp.m_data[i][j] = 0;

                for (int k=0 ; k<NumberRows ; k++)

                    temp.m_data[i][j] += m_data[i][k] * rhs.m_data[k][j];

            }

        *this = temp;

        return *this;

    }

  41. Build and run the tests – again, they should pass.
  42. Introduce a second helper function to the test class by declaring it in the Matrix3dTest class:

    void VerifyMatrixIsIdentity(Matrix3d& mat);

    Then, declare it so that we can use it:

    void Matrix3dTest::VerifyMatrixIsIdentity(Matrix3d& mat)

    {

    for( int row{0} ; row<4 ; row++)

        for( int col{0} ; col<4 ; col++)

        {

            int expected = (row==col) ? 1 : 0;

            ASSERT_FLOAT_EQ(expected, mat(row,col))

                                 << "cell[" << row << "][" << col << "]";

        }

    }

  43. Update the one test to use it:

    TEST_F(Matrix3dTest, DefaultConstructorIsIdentity)

    {

        Matrix3d mat;

        VerifyMatrixIsIdentity(mat);

    }

  44. Write one sanity check test:

    TEST_F(Matrix3dTest, IdentityTimesIdentityIsIdentity)

    {

        Matrix3d mat;

        Matrix3d result = mat * mat;

        VerifyMatrixIsIdentity(result);

    }

  45. Build and run the tests – they should still pass.
  46. Now, we need to be able to multiply points and matrices. Add the following test:

    TEST_F(Matrix3dTest, MultiplyMatrixWithPoint)

    {

        Matrix3d mat { {1,2,3,4}, {5,6,7,8},    {9,10,11,12},  {13,14,15,16}};

        Point3d pt {15, 25, 35, 45};

        Point3d expected{350, 830, 1310, 1790};

        Point3d pt2 = mat * pt;

        ASSERT_EQ(expected, pt2);

    }

  47. In matrix3d.hpp, add the include directive for point3d.hpp and add the following declaration after the Matrix3d class declaration:

    Point3d operator*(const Matrix3d& lhs, const Point3d& rhs);

  48. Add the definition of the operator to the matrix3d.cpp file:

    Point3d operator*(const Matrix3d& lhs, const Point3d& rhs)

    {

        Point3d pt;

        for(int row{0} ; row<Matrix3d::NumberRows ; row++)

        {

            float sum{0};

            for(int col{0} ; col<Matrix3d::NumberColumns ; col++)

            {

                sum += lhs(row, col) * rhs(col);

            }

            pt(row) = sum;

        }

        return pt;

    }

  49. Build and run the tests. They should all be passing again.
  50. At the top of matrix3dtests.cpp, add the include file:

    #include <cmath>

  51. Start adding the transformation matrix factory methods. Using the following tests, we will develop the various factory methods (the tests should be added one at a time):

    TEST_F(Matrix3dTest, CreateTranslateIsCorrect)

    {

        Matrix3d mat = createTranslationMatrix(-0.5, 2.5, 10.0);

        Matrix3d expected {{1.0, 0.0, 0.0, -0.5},

                           {0.0, 1.0, 0.0, 2.5},

                           {0.0, 0.0, 1.0, 10.0},

                           {0.0, 0.0, 0.0, 1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateScaleIsCorrect)

    {

        Matrix3d mat = createScaleMatrix(3.0, 2.5, 11.0);

        Matrix3d expected {{3.0, 0.0,  0.0, 0.0},

                           {0.0, 2.5,  0.0, 0.0},

                           {0.0, 0.0, 11.0, 0.0},

                           {0.0, 0.0,  0.0, 1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateX90IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutX(90.0F);

        Matrix3d expected {{1.0, 0.0,  0.0, 0.0},

                           {0.0, 0.0, -1.0, 0.0},

                           {0.0, 1.0,  0.0, 0.0},

                           {0.0, 0.0,  0.0, 1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateX60IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutX(60.0F);

        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);

        Matrix3d expected {{1.0, 0.0,     0.0,     0.0},

                           {0.0, 0.5,    -sqrt3_2, 0.0},

                           {0.0, sqrt3_2,  0.5,    0.0},

                           {0.0, 0.0,     0.0,     1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateY90IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutY(90.0F);

        Matrix3d expected {{0.0, 0.0,  1.0, 0.0},

                           {0.0, 1.0,  0.0, 0.0},

                           {-1.0, 0.0, 0.0, 0.0},

                           {0.0, 0.0,  0.0, 1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateY60IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutY(60.0F);

        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);

        Matrix3d expected {{0.5,      0.0,   sqrt3_2,  0.0},

                           {0.0,      1.0,    0.0,     0.0},

                           {-sqrt3_2, 0.0,    0.5,     0.0},

                           {0.0,      0.0,    0.0,     1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateZ90IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutZ(90.0F);

        Matrix3d expected {{0.0, -1.0,  0.0, 0.0},

                           {1.0, 0.0,  0.0, 0.0},

                           {0.0, 0.0,  1.0, 0.0},

                           {0.0, 0.0,  0.0, 1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

    TEST_F(Matrix3dTest, CreateRotateZ60IsCorrect)

    {

        Matrix3d mat = createRotationMatrixAboutZ(60.0F);

        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);

        Matrix3d expected {{0.5,     -sqrt3_2,   0.0,  0.0},

                           {sqrt3_2,      0.5,   0.0,  0.0},

                           {0.0,          0.0,   1.0,  0.0},

                           {0.0,          0.0,   0.0,  1.0}

        };

        VerifyMatrixResult(expected, mat);

    }

  52. Add the following declarations to the matrix3d header file:

    Matrix3d createTranslationMatrix(float dx, float dy, float dz);

    Matrix3d createScaleMatrix(float sx, float sy, float sz);

    Matrix3d createRotationMatrixAboutX(float degrees);

    Matrix3d createRotationMatrixAboutY(float degrees);

    Matrix3d createRotationMatrixAboutZ(float degrees);

  53. At the top of the matrix3d implementation file, add #include <cmath>.
  54. Finally, add the following implementations to the matrix3d implementation file:

    Matrix3d createTranslationMatrix(float dx, float dy, float dz)

    {

        Matrix3d matrix;

        matrix(0, 3) = dx;

        matrix(1, 3) = dy;

        matrix(2, 3) = dz;

        return matrix;

    }

    Matrix3d createScaleMatrix(float sx, float sy, float sz)

    {

        Matrix3d matrix;

        matrix(0, 0) = sx;

        matrix(1, 1) = sy;

        matrix(2, 2) = sz;

        return matrix;

    }

    Matrix3d createRotationMatrixAboutX(float degrees)

    {

        Matrix3d matrix;

        double pi{4.0F*atan(1.0F)};

        double radians = degrees / 180.0 * pi;

        float cos_theta = static_cast<float>(cos(radians));

        float sin_theta = static_cast<float>(sin(radians));

        matrix(1, 1) =  cos_theta;

        matrix(2, 2) =  cos_theta;

        matrix(1, 2) = -sin_theta;

        matrix(2, 1) =  sin_theta;

        return matrix;

    }

    Matrix3d createRotationMatrixAboutY(float degrees)

    {

        Matrix3d matrix;

        double pi{4.0F*atan(1.0F)};

        double radians = degrees / 180.0 * pi;

        float cos_theta = static_cast<float>(cos(radians));

        float sin_theta = static_cast<float>(sin(radians));

        matrix(0, 0) =  cos_theta;

        matrix(2, 2) =  cos_theta;

        matrix(0, 2) =  sin_theta;

        matrix(2, 0) = -sin_theta;

        return matrix;

    }

    Matrix3d createRotationMatrixAboutZ(float degrees)

    {

        Matrix3d matrix;

        double pi{4.0F*atan(1.0F)};

        double radians = degrees / 180.0 * pi;

        float cos_theta = static_cast<float>(cos(radians));

        float sin_theta = static_cast<float>(sin(radians));

        matrix(0, 0) =  cos_theta;

        matrix(1, 1) =  cos_theta;

        matrix(0, 1) = -sin_theta;

        matrix(1, 0) =  sin_theta;

        return matrix;

    }

  55. To get this to compile and pass the test, we need to add one more accessor to the declaration of matrix3d:

    float& operator()(const int row, const int column)

    {

        return m_data[row][column];

    }

  56. Build and run all the tests again to show that they all pass.
  57. In point3d.hpp, add the include for <ostream> and add the following friend declaration to the Point3d class at the end:

    friend std::ostream& operator<<(std::ostream& , const Point3d& );

  58. Write the inline implementation of the operator after the class:

    inline std::ostream&

    operator<<(std::ostream& os, const Point3d& pt)

    {

        const char* sep = "[ ";

        for(auto value : pt.m_data)

        {

            os << sep  << value;

            sep = ", ";

        }

        os << " ]";

        return os;

    }

  59. Open the main.cpp file and remove the comment delimiters, //, from the line:

    //#define ACTIVITY1

  60. Build and run the application called graphics – you will need to create a new Run Configuration. If your implementations of Point3d and Matrix3d are correct, then the program will display the following output:
Figure 2A.53: Successfully running the activity program

In this activity, we implemented two classes that form the basis of all the operations that are required to implement 3D graphics rendering. We used operator overloading to achieve this so that Matrix3d and Point3d can be used as if they were native types. This can be easily extended to deal with vectors of points, which is required if we wish to manipulate whole objects.