With the infrastructure set up to build and test the Matrix library, you can now code the library.
In this section, you add the core functionality of the library as well as unit tests to ensure functional integrity.
Error handling is a critical aspect of programming; balancing safety and security with performance. Depending on the context, the correct balance point may vary. For example, in the case of high performance computing for weather forecast, the dataset is extremely large and ever-increasing, but can be considered secure and curated. On the other hand, if the data used in the computations can be adversely computed or altered, it is preferable to have checks, such as out-of-bound access, enabled.
In the Matrix processing library, you implement two types of checks:
Debug
builds and the program exits with an assertion failure if a check fails.The idea here is to make the program fail in a noticeable way. Of course, in a real world application, the error should be caught and dealt with by the application, if it can. Error handling, and especially recovering from errors, can be a complex topic.
At the top of file include/Matrix/Matrix.h
, include <cassert>
to get the C-style assertions declarations for checks in Debug
mode only:
#pragma once
#include <cassert>
namespace MatComp {
Next, add a die
function to call whenever the library needs to exit. Paste the code below right under the const Version &getVersion();
declaration in
include/Matrix/Matrix.h
:
/// Get the Matrix library version information.
const Version &getVersion();
/// Immediately terminates the application with \p reason as the error message
/// and the EXIT_FAILURE error code. It will also print the file name (\p
/// fileName) and line number (\p lineNumber) that caused that application to
/// exit.
[[noreturn]] void die(const char *fileName, std::size_t lineNumber,
const char *reason);
Note that die
has been annotated with the noreturn
attribute. This is a new feature of C++ that allows the compiler to take advantage of the fact that die
will not return, so no code will be executed after it has been
called. This information was previously passed through compiler-specific annotations, but now C++ provides a generic way to pass such information to the different compilers.
You also need to provide a body for the die
function.
Open lib/Matrix/Matrix.cpp
and include at the top of the file:
<cstdlib>
: to get the declaration of exit
and EXIT_FAILURE
.<iostream>
: to get support for input/output, in order to emit useful
information about the reason for exiting the program.Add die
’s body as shown below:
#include "Matrix/Matrix.h"
#include <cstdlib>
#include <iostream>
namespace {
const MatComp::Version version = {.major = 0, .minor = 1, .patch = 0};
}
namespace MatComp {
const Version &getVersion() { return version; }
void die(const char *fileName, size_t lineNumber, const char *reason) {
std::cerr << "Fatal: " << reason << " from " << fileName << ':'
<< lineNumber << '\n';
exit(EXIT_FAILURE);
}
} // namespace MatComp
At this stage, the project should still build and compile, try it to confirm:
cd build
ninja
The Matrix library is able to deal with 1x1 matrices (scalar numbers), 1xN matrices (row-vector), Nx1 matrices (column vectors), and NxM matrices.
The matrix array is a single memory region, where the matrix elements are stored in row major order .
Matrices store elements. The Matrix library supports all arithmetic element types - signed and unsigned integer types - as well as floating point types.
The Matrix data structure has the following private data members:
numRows
: the number of rows in a matrix.numColumns
: the number of columns in a matrix.data
: the actual array of elements in a matrix.Modern C++ offers constructs in the language to deal safely with memory; you will use std::unique_ptr
which guaranties that the Matrix class will be safe from a whole range of memory management errors.
Add the following includes at the top of include/Matrix/Matrix.h
, right under
the ‘’ include:
#include <cassert>
#include <cstring>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <type_traits>
<cstring>
provides useful declarations like memcpy
that will be used to copy matrices’ content around. <ìnitializer_list>
, introduced with C++11, provides the declaration of the initializer_list
type, which is a lightweight abstraction that allows creating an array of constant objects. <memory>
gives access to the unique_ptr
and type_traits
allows to query information on the Matrix element types, either to check that a type is allowed or to select an optimized implementation at compile time.
Add the following lines to include/Matrix/Matrix.h
in the MatComp namespace under the die
function declaration:
/// The Matrix class represents N x M matrices for all arithmetic types.
template <typename Ty> class Matrix {
static_assert(std::is_arithmetic<Ty>::value,
"Matrix only accept arithmetic (i.e. integer or floating "
"point) element types.");
public:
private:
size_t numRows; //< The number of rows in this matrix.
size_t numColumns; //< The number of columns in this matrix.
std::unique_ptr<Ty[]>
data; //< The actual data in this matrix, in row-major order.
};
This Matrix
declaration deserves some comments:
template
, a C++ language feature which allows to support all element types with a single and simple code base.static_assert
that the Matrix
class was instantiated with an arithmetic data type, so that compilation can fail early with a simple and descriptive error message.Ty
being an arithmetic type is achieved thru the use of the std::is_arithmetic
type traits. Modern C++ provides many standard type traits to analyze types.Matrix
data is declared as a std::unique_ptr<Ty[]>
. This means each Matrix
instance owns its memory array, and that it is in charge of freeing it whenever a matrix is destructed.At this stage, the project should still build and compile, try it to confirm:
cd build
ninja
You have added a bare Matrix
class which is not very useful because there is no way to create a Matrix
just yet.
First, add a private helper function allocate
that the Matrix constructors will use. Add the following in the private section of the Matrix
class (in include/Matrix/Matrix.h
):
/// Allocate (if need be) numElements to the Matrix. Die if the allocation
/// went wrong.
void allocate(size_t numElements) {
if (numElements != 0) {
data = std::make_unique<Ty[]>(numElements);
if (!data)
die(__FILE__, __LINE__, "Matrix allocation failure");
}
}
You use this method to allocate memory for the element array. new
will get enough memory to store numElements
of type Ty
. This is stored in the data
unique_ptr
with the reset
function which enforces freeing memory
previously referred to by data
. If allocation fails, which is signaled with a nullptr
(a zero pointer), data
will not be valid and the program should be terminated. This helper method is made private because it is only
intended to be used by other methods from the Matrix
class. Users of the Matrix
objects have no reason for directly invoking this method.
With this in place, you can add some constructors in the public section of the Matrix
class.
The very first Matrix
you should be able to construct is an invalid Matrix
. While this might sound strange, this is useful in practice, to signal errors. In this case, an invalid Matrix
is a matrix with 0 rows and 0 columns. You can use the default constructor - that is a constructor with no parameters) for this.
Add the following code
in the public section of class Matrix
in include/Matrix/Matrix.h
:
/// Default construct an invalid Matrix.
constexpr Matrix() : numRows(0), numColumns(0), data(nullptr) {}
This constructor has been marked as constexpr
which instructs the compiler that it can optimize this case at compile time because you are essentially constructing a matrix that has no run time dependency. It has zero rows, zero columns, and no memory allocated to it. This is known at compile time and can be propagated for optimizations.
You can now add a boolean conversion, using the conversion operator, that allows allow to check whether a Matrix
instance is valid or not. It returns false
if the Matrix
object is invalid, true
otherwise.
Add the following method in the public section of Matrix
:
/// Returns true if this matrix is valid.
operator bool() const {
return numRows != 0 && numColumns != 0;
}
With these two methods in place, it’s now time to add some tests.
Create file tests/Matrix.cpp
and add the following code to it:
#include "Matrix/Matrix.h"
#include "gtest/gtest.h"
#include <cstdint>
using MatComp::Matrix;
TEST(Matrix, defaultConstruct) {
Matrix<int8_t> m0;
EXPECT_FALSE(m0);
Matrix<float> m1;
EXPECT_FALSE(m1);
}
TEST(Matrix, booleanConversion) {
EXPECT_FALSE(Matrix<int8_t>());
EXPECT_FALSE(Matrix<double>());
EXPECT_TRUE(Matrix<int8_t>(1, 1));
EXPECT_TRUE(Matrix<double>(1, 1));
EXPECT_TRUE(Matrix<int8_t>(1, 1, 1));
EXPECT_TRUE(Matrix<double>(1, 1, 2.0));
}
Next, add tests/Matrix.cpp
to the list of files used for the Matrix
unit testing in the top-level CMakeLists.txt
by adding file to the list of source files used by matrix-test
target:
add_executable(matrix-test tests/main.cpp
tests/Matrix.cpp
tests/Version.cpp)
The defaultConstruct
test does not do much, it construct two matrices, one with 8-bit signed integers, the other one with floating point numbers with the default constructor. In both cases, these matrices are expected to be invalid.
You should now check if tests pass:
cd build
ninja
ninja check
__output__...
__output__[==========] Running 3 tests from 1 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 3 tests from Matrix
__output__[ RUN ] Matrix.defaultConstruct
__output__[ OK ] Matrix.defaultConstruct (0 ms)
__output__[ RUN ] Matrix.booleanConversion
__output__[ OK ] Matrix.booleanConversion (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 3 tests from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 3 tests from 1 test suites ran. (0 ms total)
__output__[ PASSED ] 3 tests.
Constructing an invalid Matrix
is very useful, but does not really make the Matrix
class very helpful.
You can add some getters, methods that allow you to query some information about a Matrix
object:
getNumRows
: get the number of rows this matrix has.getNumColumns
: get the number of rows this matrix has.getNumElements
: get the number of elements this matrix has (rows x
columns elements).getSizeInBytes
: get size in bytes used by this matrix (rows x columns
x sizeof(element)).Add those methods in the public section of Matrix
in
include/Matrix/Matrix.h
:
/// Get the number of rows in this matrix.
size_t getNumRows() const { return numRows; }
/// Get the number of columns in this matrix.
size_t getNumColumns() const { return numColumns; }
/// Get the number of elements in this matrix.
size_t getNumElements() const { return numRows * numColumns; }
/// Get the storage size in bytes of the Matrix array.
size_t getSizeInBytes() const { return numRows * numColumns * sizeof(Ty); }
Next, modify the defaultConstruct
in tests/Matrix.cpp
test that you previously added so that it now looks like:
TEST(Matrix, defaultConstruct) {
Matrix<int8_t> m0;
EXPECT_FALSE(m0);
EXPECT_EQ(m0.getNumRows(), 0);
EXPECT_EQ(m0.getNumColumns(), 0);
EXPECT_EQ(m0.getNumElements(), 0);
EXPECT_EQ(m0.getSizeInBytes(), 0);
Matrix<float> m1;
EXPECT_FALSE(m1);
EXPECT_EQ(m1.getNumRows(), 0);
EXPECT_EQ(m1.getNumColumns(), 0);
EXPECT_EQ(m1.getNumElements(), 0);
EXPECT_EQ(m1.getSizeInBytes(), 0);
}
The tests should still pass, check for yourself.
The next step is to be able to construct valid matrices, so add this constructor to the public section of class Matrix
in include/Matrix/Matrix.h
:
/// Construct a \p numRows x \p numColumns uninitialized Matrix
Matrix(size_t numRows, size_t numColumns)
: numRows(numRows), numColumns(numColumns), data() {
allocate(getNumElements());
}
Next, add this test to tests/Matrix.cpp
:
TEST(Matrix, uninitializedConstruct) {
Matrix<int16_t> m0(2, 3);
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 3);
EXPECT_EQ(m0.getNumElements(), 6);
EXPECT_EQ(m0.getSizeInBytes(), 6 * sizeof(int16_t));
Matrix<double> m1(3, 4);
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 3);
EXPECT_EQ(m1.getNumColumns(), 4);
EXPECT_EQ(m1.getNumElements(), 12);
EXPECT_EQ(m1.getSizeInBytes(), 12 * sizeof(double));
}
This constructs a valid Matrix
if it contains elements), and the uninitializedConstruct
test checks that two valid matrices of different types and dimensions can be constructed.
Compile and test again, all should pass:
cd build
ninja
ninja check
__output__...
__output__[==========] Running 4 tests from 1 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 4 tests from Matrix
__output__[ RUN ] Matrix.defaultConstruct
__output__[ OK ] Matrix.defaultConstruct (0 ms)
__output__[ RUN ] Matrix.uninitializedConstruct
__output__[ OK ] Matrix.uninitializedConstruct (0 ms)
__output__[ RUN ] Matrix.booleanConversion
__output__[ OK ] Matrix.booleanConversion (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 4 tests from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 4 tests from 1 test suites ran. (0 ms total)
__output__[ PASSED ] 4 tests.
The Matrix
class is missing two important methods:
Add them now in the public section of Matrix
in include/Matrix/Matrix.h
:
/// Access Matrix element at (\p row, \p col) by reference.
Ty &get(size_t row, size_t col) {
assert(*this && "Invalid Matrix");
assert(row < numRows && "Out of bounds row access");
assert(col < numColumns && "Out of bounds column access");
return data[row * numColumns + col];
}
/// Access Matrix element at (\p row, \p col) by reference (const version).
const Ty &get(size_t row, size_t col) const {
assert(*this && "Invalid Matrix");
assert(row < numRows && "Out of bounds row access");
assert(col < numColumns && "Out of bounds column access");
return data[row * numColumns + col];
}
Another constructor that is missing is one that will create and initialize matrices to a known value. Let’s add it to Matrix
in include/Matrix/Matrix.h
:
/// Construct a \p numRows x \p numColumns Matrix with all elements
/// initialized to value \p val.
Matrix(size_t numRows, size_t numCols, Ty val) : Matrix(numRows, numCols) {
allocate(getNumElements());
for (size_t i = 0; i < getNumElements(); i++)
data[i] = val;
}
You should be getting the pattern now.
Add tests for those 3 methods in tests/Matrix.cpp
:
TEST(Matrix, fillConstruct) {
Matrix<uint32_t> m0(2, 2, 13);
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 2);
EXPECT_EQ(m0.getNumElements(), 4);
EXPECT_EQ(m0.getSizeInBytes(), 4 * sizeof(uint32_t));
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col), uint32_t(13));
Matrix<double> m1(2, 2, 16.0);
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 2);
EXPECT_EQ(m1.getNumColumns(), 2);
EXPECT_EQ(m1.getNumElements(), 4);
EXPECT_EQ(m1.getSizeInBytes(), 4 * sizeof(double));
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++)
EXPECT_EQ(m1.get(row, col), double(16.0));
}
TEST(Matrix, getElement) {
Matrix<uint32_t> m0(2, 3, 3);
EXPECT_TRUE(m0);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col), 3);
}
TEST(Matrix, setElement) {
Matrix<uint32_t> m0(2, 3, 3);
EXPECT_TRUE(m0);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col), 3);
m0.get(1, 2) = 65;
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == 1 && col == 2)
EXPECT_EQ(m0.get(row, col), 65);
else
EXPECT_EQ(m0.get(row, col), 3);
}
These three tests and methods need to be added all together as they make use of each other.
Compile and check again. It’s important to ensure that the project works at each step and did not regress any of the previous steps.
cd build
ninja
ninja check
__output__...
__output__[==========] Running 7 tests from 1 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 7 tests from Matrix
__output__[ RUN ] Matrix.defaultConstruct
__output__[ OK ] Matrix.defaultConstruct (0 ms)
__output__[ RUN ] Matrix.uninitializedConstruct
__output__[ OK ] Matrix.uninitializedConstruct (0 ms)
__output__[ RUN ] Matrix.fillConstruct
__output__[ OK ] Matrix.fillConstruct (0 ms)
__output__[ RUN ] Matrix.getElement
__output__[ OK ] Matrix.getElement (0 ms)
__output__[ RUN ] Matrix.setElement
__output__[ OK ] Matrix.setElement (0 ms)
__output__[ RUN ] Matrix.booleanConversion
__output__[ OK ] Matrix.booleanConversion (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 7 tests from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 7 tests from 1 test suites ran. (0 ms total)
__output__[ PASSED ] 7 tests.
Congratulations, you are almost done with constructors!
The last needed constructor is one that will allow you to build matrices with arbitrary values.
Add the constructor below to the public part of Matrix
in include/Matrix/Matrix.h
.
The C++ std::initializer_list
enables users to provide a list of literal
values (in row major order) to use to initialize the matrix with:
/// Construct a \p numRows x \p numColumns Matrix with elements
/// initialized from the values from \p il in row-major order.
Matrix(size_t numRows, size_t numCols, std::initializer_list<Ty> il)
: Matrix(numRows, numCols) {
if (il.size() != getNumElements())
die(__FILE__, __LINE__,
"the number of initializers does not match the Matrix number "
"of elements");
allocate(getNumElements());
size_t i = 0;
for (const auto &val : il)
data[i++] = val;
}
Again, you should add the corresponding test in tests/Matrix.cpp
:
TEST(Matrix, initializerListConstruct) {
Matrix<int64_t> m0(2, 3, {1, 2, 3, 4, 5, 6});
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 3);
EXPECT_EQ(m0.getNumElements(), 6);
EXPECT_EQ(m0.getSizeInBytes(), m0.getNumElements() * sizeof(int64_t));
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col),
int64_t(row * m0.getNumColumns() + col + 1));
Matrix<float> m1(3, 2, {1., 2., 3., 4., 5., 6.});
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 3);
EXPECT_EQ(m1.getNumColumns(), 2);
EXPECT_EQ(m1.getNumElements(), 6);
EXPECT_EQ(m1.getSizeInBytes(), m1.getNumElements() * sizeof(float));
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++)
EXPECT_FLOAT_EQ(m1.get(row, col),
double(row * m1.getNumColumns() + col + 1));
}
You can now construct a Matrix
using arbitrary values with the above constructor, but Matrix
users will want to assign an existing object with new content, without affecting its shape. This is done with the C++ copy-assignment
operator that you will add now in the public part of Matrix
in
include/Matrix/Matrix.h
:
/// Assign from the \p il initializer list.
Matrix &operator=(std::initializer_list<Ty> il) {
if (il.size() != getNumElements())
die(__FILE__, __LINE__, "number of elements do not match");
size_t i = 0;
for (const auto &val : il)
data[i++] = val;
return *this;
}
Add a test in tests/Matrix.cpp
:
TEST(Matrix, initializerListAssign) {
Matrix<uint64_t> m0(2, 3);
m0 = {1, 2, 3, 4, 5, 6};
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 3);
EXPECT_EQ(m0.getNumElements(), 6);
EXPECT_EQ(m0.getSizeInBytes(), m0.getNumElements() * sizeof(uint64_t));
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col),
uint64_t(row * m0.getNumColumns() + col + 1));
Matrix<double> m1(3, 2);
m1 = {1., 2., 3., 4., 5., 6.};
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 3);
EXPECT_EQ(m1.getNumColumns(), 2);
EXPECT_EQ(m1.getNumElements(), 6);
EXPECT_EQ(m1.getSizeInBytes(), m1.getNumElements() * sizeof(double));
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++)
EXPECT_DOUBLE_EQ(m1.get(row, col),
double(row * m1.getNumColumns() + col + 1));
}
Make sure that the project builds and the tests pass.
So far, the Matrix
constructors have dealt with Matrix
objects in isolation.
But in real life, matrices are not isolated. Users will want to copy them or to assign to them for example, which raises the important issue of memory management.
Modern C++ allows you to easily express and control the copy
and the move
semantics. In the copy
semantic, the content of an object is copied to another object, and both object instances do not share memory. But in
some cases, it is important (for performance) to avoid unnecessary memory allocation and data copying, and this can be expressed with the move
semantic, where a destination object steals the content from the source object, making the source object invalid.
One important time in an object life is to use the copy
or the move
semantic is at construction time, when an object is built from another object.
Add a copy constructor and a move constructor in the public part of Matrix
in
include/Matrix/Matrix.h
:
/// Copy-construct from the \p other Matrix.
Matrix(const Matrix &other)
: numRows(other.numRows), numColumns(other.numColumns), data() {
allocate(getNumElements());
std::memcpy(data.get(), other.data.get(), getSizeInBytes());
}
/// Move-construct from the \p other Matrix.
Matrix(Matrix &&other)
: numRows(other.numRows), numColumns(other.numColumns),
data(std::move(other.data)) {
// Invalidate other.
other.numRows = 0;
other.numColumns = 0;
}
You can test by adding the following lines to tests/Matrix.cpp
:
TEST(Matrix, copyConstruct) {
const Matrix<int8_t> m0(3, 3, {1, 2, 3, 4, 5, 6, 7, 8, 9});
Matrix<int8_t> m1(m0);
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
EXPECT_EQ(m0.getNumElements(), m1.getNumElements());
EXPECT_EQ(m0.getSizeInBytes(), m1.getSizeInBytes());
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++) {
EXPECT_EQ(m0.get(row, col), m1.get(row, col));
EXPECT_NE(&m0.get(row, col), &m1.get(row, col));
}
}
TEST(Matrix, moveConstruct) {
Matrix<int16_t> m0(3, 2, {1, 2, 3, 4, 5, 6});
EXPECT_TRUE(m0);
Matrix<int16_t> m1(std::move(m0));
EXPECT_FALSE(m0);
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 3);
EXPECT_EQ(m1.getNumColumns(), 2);
EXPECT_EQ(m1.getNumElements(), 6);
EXPECT_EQ(m1.getSizeInBytes(), m1.getNumElements() * sizeof(int16_t));
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++)
EXPECT_EQ(m1.get(row, col),
int16_t(row * m1.getNumColumns() + col + 1));
}
The other important time is when an object is assigned to, with the copy assignment and the move assignment operators that you will add now in the public part of Matrix
in include/Matrix/Matrix.h
:
/// Copy-assign from the \p rhs Matrix.
Matrix &operator=(const Matrix &rhs) {
reallocate(rhs.getNumElements());
if (getNumElements() != 0)
std::memcpy(data.get(), rhs.data.get(), rhs.getSizeInBytes());
return *this;
}
/// Move-assign from the \p rhs Matrix.
Matrix &operator=(Matrix &&rhs) {
numRows = rhs.numRows;
numColumns = rhs.numColums;
data = std::move(rhs.data);
return *this;
}
The copy assignment makes use of the reallocate
helper routine, which you should add in the private section of class Matrix
in include/Matrix/Matrix.h
:
/// Reallocate (if need be) numElements to the Matrix. Die if the allocation
/// went wrong. This method assumes \p numRows and \p numColumns have
/// their 'old' values, that will get updated as part of the re-allocation.
void reallocate(size_t newNumRows, size_t newNumColumns) {
const size_t newNumElements = newNumRows * newNumColumns;
if (getNumElements() != newNumElements) {
if (newNumElements != 0) {
data = std::make_unique<Ty[]>(newNumElements);
if (!data)
die(__FILE__, __LINE__, "Matrix re-allocation failure");
} else
data.reset(nullptr);
}
numRows = newNumRows;
numColumns = newNumColumns;
}
Re-allocation might be necessary in the case of copy-assignment because the destination object might have been constructed with a different number of elements.
With all this in place, you can add the corresponding tests to tests/Matrix.cpp
:
TEST(Matrix, copyAssign) {
const Matrix<int32_t> m0(3, 3, {1, 2, 3, 4, 5, 6, 7, 8, 9});
Matrix<int32_t> m1 = m0;
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
EXPECT_EQ(m0.getNumElements(), m1.getNumElements());
EXPECT_EQ(m0.getSizeInBytes(), m1.getSizeInBytes());
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++) {
EXPECT_EQ(m0.get(row, col), m1.get(row, col));
EXPECT_NE(&m0.get(row, col), &m1.get(row, col));
}
}
TEST(Matrix, moveAssign) {
Matrix<int16_t> m0(3, 2, {1, 2, 3, 4, 5, 6});
EXPECT_TRUE(m0);
Matrix<int16_t> m1 = std::move(m0);
EXPECT_FALSE(m0);
EXPECT_TRUE(m1);
EXPECT_EQ(m1.getNumRows(), 3);
EXPECT_EQ(m1.getNumColumns(), 2);
EXPECT_EQ(m1.getNumElements(), 6);
EXPECT_EQ(m1.getSizeInBytes(), m1.getNumElements() * sizeof(int16_t));
for (size_t row = 0; row < m1.getNumRows(); row++)
for (size_t col = 0; col < m1.getNumColumns(); col++)
EXPECT_EQ(m1.get(row, col),
int16_t(row * m1.getNumColumns() + col + 1));
}
It’s now time to build and check the code base - all tests should pass.
For convenience, users should be provided with some useful methods to get specific types of matrices:
zeros
to get a zero initialized Matrix
.ones
to get a Matrix
initialized with 1.identity
to get the identity Matrix
, which is a square matrix, with 1 on the
diagonal and 0 elsewhere.Users with Python with NumPy
experience are used to those shortcuts, and they make the user code much more readable, this is often referred to as syntactic sugar, so let’s add these to Matrix
’s public section in
include/Matrix/Matrix.h
:
/// Get a zero initialized Matrix.
static Matrix zeros(size_t numRows, size_t numColumns) {
return Matrix(numRows, numColumns, Ty(0));
}
/// Get a one initialized Matrix.
static Matrix ones(size_t numRows, size_t numColumns) {
return Matrix(numRows, numColumns, Ty(1));
}
/// Get the identity Matrix.
static Matrix identity(size_t dimension) {
Matrix id = zeros(dimension, dimension);
for (size_t i = 0; i < dimension; i++)
id.get(i, i) = Ty(1);
return id;
}
They have been marked as static
, which means these methods are not instance-specific, they are class methods.
Of course, you should have tests for these methods in tests/Matrix.cpp
:
TEST(Matrix, zeros) {
Matrix<int16_t> z0 = Matrix<int16_t>::zeros(2, 6);
EXPECT_TRUE(z0);
EXPECT_EQ(z0.getNumRows(), 2);
EXPECT_EQ(z0.getNumColumns(), 6);
EXPECT_EQ(z0.getNumElements(), 12);
EXPECT_EQ(z0.getSizeInBytes(), 12 * sizeof(int16_t));
for (size_t row = 0; row < z0.getNumRows(); row++)
for (size_t col = 0; col < z0.getNumColumns(); col++)
EXPECT_EQ(z0.get(row, col), int16_t(0));
}
TEST(Matrix, ones) {
Matrix<uint8_t> o0 = Matrix<uint8_t>::ones(4, 3);
EXPECT_TRUE(o0);
EXPECT_EQ(o0.getNumRows(), 4);
EXPECT_EQ(o0.getNumColumns(), 3);
EXPECT_EQ(o0.getNumElements(), 12);
EXPECT_EQ(o0.getSizeInBytes(), 12 * sizeof(uint8_t));
for (size_t row = 0; row < o0.getNumRows(); row++)
for (size_t col = 0; col < o0.getNumColumns(); col++)
EXPECT_EQ(o0.get(row, col), uint8_t(1));
}
TEST(Matrix, identity) {
Matrix<uint8_t> i0 = Matrix<uint8_t>::identity(5);
EXPECT_TRUE(i0);
EXPECT_EQ(i0.getNumRows(), 5);
EXPECT_EQ(i0.getNumColumns(), 5);
EXPECT_EQ(i0.getNumElements(), 25);
EXPECT_EQ(i0.getSizeInBytes(), 25 * sizeof(uint8_t));
for (size_t row = 0; row < i0.getNumRows(); row++)
for (size_t col = 0; col < i0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(i0.get(row, col), uint8_t(1));
else
EXPECT_EQ(i0.get(row, col), uint8_t(0));
}
Compile and check again - all test should pass:
cd build
ninja
ninja check
__output__[==========] Running 16 tests from 1 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 16 tests from Matrix
__output__[ RUN ] Matrix.defaultConstruct
__output__[ OK ] Matrix.defaultConstruct (0 ms)
__output__[ RUN ] Matrix.uninitializedConstruct
__output__[ OK ] Matrix.uninitializedConstruct (0 ms)
__output__[ RUN ] Matrix.fillConstruct
__output__[ OK ] Matrix.fillConstruct (0 ms)
__output__[ RUN ] Matrix.getElement
__output__[ OK ] Matrix.getElement (0 ms)
__output__[ RUN ] Matrix.setElement
__output__[ OK ] Matrix.setElement (0 ms)
__output__[ RUN ] Matrix.initializerListConstruct
__output__[ OK ] Matrix.initializerListConstruct (0 ms)
__output__[ RUN ] Matrix.copyConstruct
__output__[ OK ] Matrix.copyConstruct (0 ms)
__output__[ RUN ] Matrix.moveConstruct
__output__[ OK ] Matrix.moveConstruct (0 ms)
__output__[ RUN ] Matrix.copyAssign
__output__[ OK ] Matrix.copyAssign (0 ms)
__output__[ RUN ] Matrix.moveAssign
__output__[ OK ] Matrix.moveAssign (0 ms)
__output__[ RUN ] Matrix.initializerListAssign
__output__[ OK ] Matrix.initializerListAssign (0 ms)
__output__[ RUN ] Matrix.zeros
__output__[ OK ] Matrix.zeros (0 ms)
__output__[ RUN ] Matrix.ones
__output__[ OK ] Matrix.ones (0 ms)
__output__[ RUN ] Matrix.identity
__output__[ OK ] Matrix.identity (0 ms)
__output__[ RUN ] Matrix.booleanConversion
__output__[ OK ] Matrix.booleanConversion (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 16 tests from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 16 tests from 3 test suites ran. (0 ms total)
__output__[ PASSED ] 16 tests.
At some point, one will want to see the content of a Matrix
, so add a simple output operator to dump the matrix content to a stream.
Add this code at the very end of include/Matrix/Matrix.h
, outside of the MatComp
namespace:
/// Dump this Matrix in textual format to output stream \p os.
template <typename Ty>
std::ostream &operator<<(std::ostream &os, const MatComp::Matrix<Ty> &m) {
for (size_t row = 0; row < m.getNumRows(); row++) {
for (size_t col = 0; col < m.getNumColumns(); col++)
os << '\t' << m.get(row, col) << ',';
os << '\n';
}
return os;
}
This will print each row of the matrix to a different line, separating the values with commas and tab spaces.
Of course, you also need to add a test for this output operator in tests/Matrix.cpp
:
TEST(Matrix, dump) {
std::ostringstream osstr;
// Test horizontal vector.
osstr << Matrix<int16_t>(1, 3, {1, 2, 3});
EXPECT_EQ(osstr.str(), "\t1,\t2,\t3,\n");
osstr.str("");
// Test vertical vector.
osstr << Matrix<int32_t>(3, 1, {1, 2, 3});
EXPECT_EQ(osstr.str(), "\t1,\n\t2,\n\t3,\n");
osstr.str("");
// Test matrix.
osstr << Matrix<int64_t>::identity(2);
EXPECT_EQ(osstr.str(), "\t1,\t0,\n\t0,\t1,\n");
}
This test makes uses of string streams, which enable you to capture and check the output without writing to the standard output. You need to add an include file at the top of tests/Matrix.cpp
for the above test to compile:
#include <sstream>
The last mundane operations you need are Matrix
equality and inequality operators.
Add these to the public section of Matrix
in include/Matrix/Matrix.h
:
/// Returns true iff both matrices compare equal.
bool operator==(const Matrix &rhs) const {
// Invalid matrices compare equal.
if (!*this && !rhs)
return true;
// If one is invalid, they can never compare equal.
if (*this ^ rhs)
return false;
// Matrices with different dimensions are not equal.
if (numRows != rhs.numRows || numColumns != rhs.numColumns)
return false;
// Every thing else is equal and sound, compare the elements !
for (size_t i = 0; i < getNumElements(); i++)
if (data[i] != rhs.data[i])
return false;
return true;
}
/// Returns true iff matrices do not compare equal.
bool operator!=(const Matrix &rhs) const { return !(*this == rhs); }
Add tests for these two operators in tests/Matrix.cpp
:
TEST(Matrix, equal) {
EXPECT_TRUE(Matrix<int16_t>() == Matrix<int16_t>());
EXPECT_FALSE(Matrix<int16_t>(2, 3) == Matrix<int16_t>());
EXPECT_FALSE(Matrix<int16_t>(3, 2, 2) == Matrix<int16_t>());
EXPECT_FALSE(Matrix<int16_t>() == Matrix<int16_t>(2, 3));
EXPECT_FALSE(Matrix<int16_t>() == Matrix<int16_t>(2, 3, 2));
EXPECT_FALSE(Matrix<uint8_t>(3, 2) == Matrix<uint8_t>(1, 4));
EXPECT_FALSE(Matrix<uint8_t>(3, 2) == Matrix<uint8_t>(3, 4));
EXPECT_FALSE(Matrix<uint8_t>(3, 2) == Matrix<uint8_t>(1, 2));
EXPECT_FALSE(Matrix<uint8_t>(3, 2, 1) == Matrix<uint8_t>(3, 2, 2));
EXPECT_TRUE(Matrix<uint8_t>(3, 2, 1) == Matrix<uint8_t>(3, 2, 1));
}
TEST(Matrix, notEqual) {
EXPECT_FALSE(Matrix<int32_t>() != Matrix<int32_t>());
EXPECT_TRUE(Matrix<int32_t>(2, 3) != Matrix<int32_t>());
EXPECT_TRUE(Matrix<int32_t>(3, 2, 2) != Matrix<int32_t>());
EXPECT_TRUE(Matrix<int32_t>() != Matrix<int32_t>(2, 3));
EXPECT_TRUE(Matrix<int32_t>() != Matrix<int32_t>(2, 3, 2));
EXPECT_TRUE(Matrix<uint64_t>(3, 2) != Matrix<uint64_t>(1, 4));
EXPECT_TRUE(Matrix<uint64_t>(3, 2) != Matrix<uint64_t>(3, 4));
EXPECT_TRUE(Matrix<uint64_t>(3, 2) != Matrix<uint64_t>(1, 2));
EXPECT_TRUE(Matrix<uint64_t>(3, 2, 1) != Matrix<uint64_t>(3, 2, 2));
EXPECT_FALSE(Matrix<uint64_t>(3, 2, 1) != Matrix<uint64_t>(3, 2, 1));
}
Check again if the tests build and pass:
cd build
ninja
ninja check
__output__[==========] Running 19 tests from 1 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 19 tests from Matrix
__output__[ RUN ] Matrix.defaultConstruct
__output__[ OK ] Matrix.defaultConstruct (0 ms)
__output__[ RUN ] Matrix.uninitializedConstruct
__output__[ OK ] Matrix.uninitializedConstruct (0 ms)
__output__[ RUN ] Matrix.fillConstruct
__output__[ OK ] Matrix.fillConstruct (0 ms)
__output__[ RUN ] Matrix.getElement
__output__[ OK ] Matrix.getElement (0 ms)
__output__[ RUN ] Matrix.setElement
__output__[ OK ] Matrix.setElement (0 ms)
__output__[ RUN ] Matrix.initializerListConstruct
__output__[ OK ] Matrix.initializerListConstruct (0 ms)
__output__[ RUN ] Matrix.copyConstruct
__output__[ OK ] Matrix.copyConstruct (0 ms)
__output__[ RUN ] Matrix.moveConstruct
__output__[ OK ] Matrix.moveConstruct (0 ms)
__output__[ RUN ] Matrix.copyAssign
__output__[ OK ] Matrix.copyAssign (0 ms)
__output__[ RUN ] Matrix.moveAssign
__output__[ OK ] Matrix.moveAssign (0 ms)
__output__[ RUN ] Matrix.initializerListAssign
__output__[ OK ] Matrix.initializerListAssign (0 ms)
__output__[ RUN ] Matrix.zeros
__output__[ OK ] Matrix.zeros (0 ms)
__output__[ RUN ] Matrix.ones
__output__[ OK ] Matrix.ones (0 ms)
__output__[ RUN ] Matrix.identity
__output__[ OK ] Matrix.identity (0 ms)
__output__[ RUN ] Matrix.booleanConversion
__output__[ OK ] Matrix.booleanConversion (0 ms)
__output__[ RUN ] Matrix.equal
__output__[ OK ] Matrix.equal (0 ms)
__output__[ RUN ] Matrix.notEqual
__output__[ OK ] Matrix.notEqual (0 ms)
__output__[ RUN ] Matrix.dump
__output__[ OK ] Matrix.dump (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 19 tests from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 19 tests from 1 test suites ran. (0 ms total)
__output__[ PASSED ] 19 tests.
Congratulations, you now have a working library!
At this stage, the code looks like this:
Matrix/
├── CMakeLists.txt
├── build/
...
├── external/
│ └── CMakeLists.txt
├── include/
│ └── Matrix/
│ └── Matrix.h
├── lib/
│ └── Matrix/
│ └── Matrix.cpp
├── src/
│ ├── getVersion.cpp
│ └── howdy.cpp
└── tests/
├── Matrix.cpp
├── Version.cpp
└── main.cpp
After this rather long exercise, you have a minimalistic, yet fully-functional core for the matrix processing library, with some level of regression testing.
Modern C++ enables you to express move and copy semantics, and to use smart pointers to make memory management easy.
The compiler also catch a large number of type or misuse errors. With this core functionality in place, you have all you need to implement matrix operations in the next section.