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:
- 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.
- 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.
- 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.
- 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;
- Now, run all the tests. All 18 existing tests should pass again. We have successfully put our classes into a namespace.
- 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.
- Next, change the type of the declaration for m_data:
std::unique_ptr<float[]> m_data;
- 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.
- 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;
}
- 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;
}
- 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.
- 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;
}
}
- 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
}
- 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
}
}
- 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;
}
- 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.
- 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;
}
- 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);
- In matrix3d.hpp declare the move constructor.
Matrix3d(Matrix3d&& rhs);
- Rebuild the tests. Now, we get an error related to the move constructor not being present.
- 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);
}
- Rebuild and run the tests. They all pass again.
- 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.
- 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.
- Next, change the type of the declaration for m_data to be like so:
std::unique_ptr<float[]> m_data;
- 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;
}
- 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;
}
- Modify the other constructor by initializing the unique_ptr:
Point3d::Point3d(std::initializer_list<float> list)
: m_data{new float[NumberRows]}
- Now all the tests run and pass, like they did previously.
- 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.