Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Advanced C++

You're reading from   Advanced C++ Master the technique of confidently writing robust C++ code

Arrow left icon
Product type Paperback
Published in Oct 2019
Publisher
ISBN-13 9781838821135
Length 762 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (5):
Arrow left icon
Olena Lizina Olena Lizina
Author Profile Icon Olena Lizina
Olena Lizina
Rakesh Mane Rakesh Mane
Author Profile Icon Rakesh Mane
Rakesh Mane
Gazihan Alankus Gazihan Alankus
Author Profile Icon Gazihan Alankus
Gazihan Alankus
Brian Price Brian Price
Author Profile Icon Brian Price
Brian Price
Vivek Nagarajan Vivek Nagarajan
Author Profile Icon Vivek Nagarajan
Vivek Nagarajan
+1 more Show less
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

About the Book 1. Anatomy of Portable C++ Software 2A. No Ducks Allowed – Types and Deduction FREE CHAPTER 2B. No Ducks Allowed – Templates and Deduction 3. No Leaks Allowed - Exceptions and Resources 4. Separation of Concerns - Software Architecture, Functions, and Variadic Templates 5. The Philosophers' Dinner – Threads and Concurrency 6. Streams and I/O 7. Everybody Falls, It's How You Get Back Up – Testing and Debugging 8. Need for Speed – Performance and Optimization 1. Appendix

Chapter 3 - The Distance between Can and Should – Objects, Pointers and Inheritance

Activity 1: Implementing Graphics Processing with RAII and Move

In this activity, we will develop our previous Matrix3d and Point3d classes to use a unique_ptr<> to manage the memory associated with the data structures that are required to implement these graphics classes. Let's get started:

  1. Load the prepared project from the Lesson3/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. We recommend that the name that's used for the tests runner is L3A1graphicstests.
  2. Open point3d.hpp and add the lines marked with a comment to the file:

    // ... lines omitted

    #include <initializer_list>

    #include <ostream>

    namespace acpp::gfx { // Add this line

    class Point3d

    {

    // ... lines omitted

    };

    } // Add this line

    Note that the closing brace that's added to the end of the file does NOT have a closing semi-colon. The nested namespace syntax acpp::gfx, is a new feature of C++17. Previously, it would have required the explicit use of the namespace keyword twice. Also, beware that, in trying to be helpful, your friendly neighborhood IDE may insert the closing brace just after the line that you put the namespace declaration.

  3. Repeat the same treatment for matrix3d.hpp, matrix3d.cpp, and point3d.cpp – ensure that the include files are not included in the scope of the namespace.
  4. In the respective files (main.cpp, matrix3dTests.cpp, and point3dTests.cpp), just after completing the #include directives, insert the following line:

    using namespace acpp::gfx;

  5. Now, run all the tests. All 18 existing tests should pass again. We have successfully put our classes into a namespace.
  6. Now we will move onto converting the Matrix3d class to use heap allocated memory. In the matrix3d.hpp file, add an #include <memory> line to give us access to the unique_ptr<> template.
  7. Next, change the type of the declaration for m_data:

    std::unique_ptr<float[]> m_data;

  8. From this point forward, we will use the compiler and its errors to give us hints as to what needs fixing. Attempting to build the tests now reveals that we have a problem with the following two methods in the header file

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

    {

        return m_data[row][column];

    }

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

    {

        return m_data[row][column];

    }

    The problem here is that unique_ptr holds a pointer to a single dimension array and not a two- dimensional array. So, we need to convert the row and column into a single index.

  9. Add a new method called get_index() to get the one-dimensional index from the row and column and update the preceding functions to use it:

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

    {

        return m_data[get_index(row,column)];

    }

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

    {

        return m_data[get_index(row,column)];

    }

    private:

    size_t get_index(const int row, const int column) const

    {

        return row * NumberColumns + column;

    }

  10. After recompiling, the next error from the compiler refers to the following inline function:

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

    {

        Matrix3d temp(lhs);   // <=== compiler error – ill formed copy constructor

        temp *= rhs;

        return temp;

    }

  11. Whereas before, the default copy constructor was sufficient for our purposes, it just did a shallow copy of all the elements of the array and that was correct. We now have indirection to the data we need to copy and so we need to implement a deep copy constructor and copy assignment. We will also need to address the existing constructors. For now, just add the constructor declarations to the class (adjacent to the other constructors):

    Matrix3d(const Matrix3d& rhs);

    Matrix3d& operator=(const Matrix3d& rhs);

    Attempting to build the tests will now show that we have resolved all the issues in the header file, and that we can move onto the implementation file.

  12. Modify the two constructors to initialize unique_ptr as follows:

    Matrix3d::Matrix3d() : m_data{new float[NumberRows*NumberColumns]}

    {

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

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

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

    }

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

        : m_data{new float[NumberRows*NumberColumns]}

    {

        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;

        }

    }

  13. We now need to address the single-dimensional array look-up. We need to change the statements of the m_data[i][j] type with m_data[get_index(i,j)]. Change the default constructor to read like so:

    Matrix3d::Matrix3d() : m_data{new float[NumberRows*NumberColumns]}

    {

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

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

                m_data[get_index(i, j)] = (i==j);          // <= change here

    }

  14. Change the initializer list constructor to be the following:

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

          : m_data{new float[NumberRows*NumberColumns]}

    {

        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[get_index(i, j)] = *it2;         // <= change here

        }

    }

  15. Change the multiplication operator, being careful with the indices:

    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[get_index(i, j)] = 0;        // <= change here

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

                    temp.m_data[get_index(i, j)] += m_data[get_index(i, k)]

                                              * rhs.m_data[get_index(k, j)];

                                                         // <= change here

            }

        *this = temp;

        return *this;

    }

  16. With these changes in place, we have fixed all the compiler errors, but now we have a linker error to deal with – the copy constructor that we only declared back in step 11.
  17. In the matrix3d.cpp file add the following definitions:

    Matrix3d::Matrix3d(const Matrix3d& rhs) :

        m_data{new float[NumberRows*NumberColumns]}

    {

        *this = rhs;

    }

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

    {

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

            m_data[i] = rhs.m_data[i];

        return *this;

    }

  18. The tests will now build and all of them will pass. The next step is to force a move constructor. Locate the createTranslationMatrix() method in matrix3d.cpp and change the return statement as follows:

    return std::move(matrix);

  19. In matrix3d.hpp declare the move constructor.

    Matrix3d(Matrix3d&& rhs);

  20. Rebuild the tests. Now, we get an error related to the move constructor not being present.
  21. Add the implementation of the constructor into matrix3d.cpp and rebuild the tests.

    Matrix3d::Matrix3d(Matrix3d&& rhs)

    {

        //std::cerr << "Matrix3d::Matrix3d(Matrix3d&& rhs)\n";

        std::swap(m_data, rhs.m_data);

    }

  22. Rebuild and run the tests. They all pass again.
  23. Just to confirm that the move constructor is being called, add #include <iostream> to matrix3d.cpp, remove the comment from the output line in the move constructor. and re-run the test. It will report an error after the tests have completed because we sent it to the standard error channel (cerr). After the check, make the line a comment again.

    Note

    Just a quick note about the move constructor – we did not explicitly initialize m_data like we did for the other constructors. This means that it will be initialized as empty and then swapped with the parameter that is passed in, which is a temporary and so it is acceptable for it to not hold an array after the transaction – it removes one allocation and deallocation of memory.

  24. Now let's convert the Point3d class so that it can use heap allocated memory. In the point3d.hpp file, add an #include <memory> line so that we have access to the unique_ptr<> template.
  25. Next, change the type of the declaration for m_data to be like so:

    std::unique_ptr<float[]> m_data;

  26. The compiler now tells us that we have a problem with the insertion operator (<<) in point3d.hpp because we can't use a ranged-for on unique_ptr: Replace the implementation with the following:

    inline std::ostream&

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

    {

        const char* sep = "[ ";

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

        {

            os << sep << pt.m_data[i];

            sep = ", ";

        }

        os << " ]";

        return os;

    }

  27. Open point3d.cpp and modify the default constructors to initialize the unique_ptr and change the initialization loop since a ranged for cannot be used on the unique_ptr:

    Point3d::Point3d() : m_data{new float[NumberRows]}

    {

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

            m_data[i] = 0;

        }

        m_data[NumberRows-1] = 1;

    }

  28. Modify the other constructor by initializing the unique_ptr:

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

                : m_data{new float[NumberRows]}

  29. Now all the tests run and pass, like they did previously.
  30. Now, if we run the original application, L3graphics, then the output will be identical to the original, but the implementation uses RAII to allocate and manage the memory that's used for the matrices and points.
Figure 3.52: Activity 1 output after successful conversion to use RAII
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime