In the previous section, you created the core, or the boilerplate, needed for
Matrix
objects. They can now be constructed, assigned to/from, and dumped to
the screen, but the library is still missing actual matrix operations: add,
subtract, and multiply.
In this section, you add the missing operations.
When designing matrix processing, it’s desirable to separate two concerns:
This allows you to develop the independent parts and then compose them, allowing for easy testing and extension of the library.
Unary operations are operations that take a single Matrix
as argument.
The very first unary operations to consider when processing a Matrix
are:
Neg
to negate a value.Abs
to get the absolute value.Sqrt
to get the square root of a value.Log
to get the natural logarithm of a value.You add these unary scalar operations, in the MatComp
namespace, but in a
separate file.
Add the code below to a new file include/Matrix/Operators.h
:
#pragma once
#include <cmath>
#include <type_traits>
namespace MatComp {
struct unaryOperation {};
/// Negate \p v.
template <typename Ty> class Neg : public unaryOperation {
public:
constexpr Ty operator()(const Ty &v) const { return -v; }
};
/// Get the absolute value of \p v.
template <typename Ty> class Abs : public unaryOperation {
public:
template <typename T = Ty>
constexpr std::enable_if_t<std::is_unsigned<T>::value, Ty>
operator()(const Ty &v) const {
return v;
}
template <typename T = Ty>
constexpr std::enable_if_t<std::is_signed<T>::value, Ty>
operator()(const Ty &v) const {
return std::abs(v);
}
};
/// Get the square root of \p v.
template <typename Ty> class Sqrt : public unaryOperation {
public:
constexpr Ty operator()(const Ty &v) const { return std::sqrt(v); }
};
/// Get the natural logarithm of \p v.
template <typename Ty> class Log : public unaryOperation {
public:
constexpr Ty operator()(const Ty &v) const { return std::log(v); }
};
} // namespace MatComp
All these unary operations derive from the empty unaryOperation
struct
,
which enable code bases to type/group those operations. This essentially
tags these operations. They are all implemented as templates, in order for
them to be type-generic. Although none of these carry a state, they have been
implemented as classes with the function operator (operator()
) defined. This
makes them suitable for using in bigger algorithms and is a common pattern used in the C++ standard library.
One point worth mentioning is related to the Abs
class: depending on the type
used at instantiation, the compiler selects an optimized implementation for
unsigned types, and there is no need to compute the absolute value of an always
positive value. This optimization is transparent to users.
Those operators are marked as constexpr
so that the compiler can optimize the
computation by performing them at compile time rather than runtime if the actual
values are known at compile time. They are marked const
, because the operator
does not modify the object state - as these objects don’t have a state.
The new operations must have tests, so add those into
a separate tests/Operators.cpp
file:
#include "Matrix/Operators.h"
#include <limits>
#include "gtest/gtest.h"
using std::numeric_limits;
TEST(unaryOperator, Neg) {
EXPECT_EQ(MatComp::Neg<uint8_t>()(1), uint8_t(-1));
EXPECT_EQ(MatComp::Neg<uint8_t>()(-2), uint8_t(2));
EXPECT_EQ(MatComp::Neg<uint16_t>()(1), uint16_t(-1));
EXPECT_EQ(MatComp::Neg<uint16_t>()(-2), uint16_t(2));
EXPECT_EQ(MatComp::Neg<uint32_t>()(1), uint32_t(-1));
EXPECT_EQ(MatComp::Neg<uint32_t>()(-2), uint32_t(2));
EXPECT_EQ(MatComp::Neg<uint64_t>()(1), uint64_t(-1));
EXPECT_EQ(MatComp::Neg<uint64_t>()(-2), uint64_t(2));
EXPECT_EQ(MatComp::Neg<int8_t>()(1), int8_t(-1));
EXPECT_EQ(MatComp::Neg<int8_t>()(-2), int8_t(2));
EXPECT_EQ(MatComp::Neg<int16_t>()(1), int16_t(-1));
EXPECT_EQ(MatComp::Neg<int16_t>()(-2), int16_t(2));
EXPECT_EQ(MatComp::Neg<int32_t>()(1), int32_t(-1));
EXPECT_EQ(MatComp::Neg<int32_t>()(-2), int32_t(2));
EXPECT_EQ(MatComp::Neg<int64_t>()(1), int64_t(-1));
EXPECT_EQ(MatComp::Neg<int64_t>()(-2), int64_t(2));
EXPECT_FLOAT_EQ(MatComp::Neg<float>()(1.0), float(-1.0));
EXPECT_FLOAT_EQ(MatComp::Neg<float>()(-2.0), float(2.0));
EXPECT_DOUBLE_EQ(MatComp::Neg<double>()(1.0), double(-1.0));
EXPECT_DOUBLE_EQ(MatComp::Neg<double>()(-2.0), double(2.0));
}
TEST(unaryOperator, Abs) {
EXPECT_EQ(MatComp::Abs<uint8_t>()(1), uint8_t(1));
EXPECT_EQ(MatComp::Abs<uint8_t>()(-2),
numeric_limits<uint8_t>::max() - 2 + 1);
EXPECT_EQ(MatComp::Abs<uint16_t>()(1), uint16_t(1));
EXPECT_EQ(MatComp::Abs<uint16_t>()(-2),
numeric_limits<uint16_t>::max() - 2 + 1);
EXPECT_EQ(MatComp::Abs<uint32_t>()(1), uint32_t(1));
EXPECT_EQ(MatComp::Abs<uint32_t>()(-2),
numeric_limits<uint32_t>::max() - 2 + 1);
EXPECT_EQ(MatComp::Abs<uint64_t>()(1), uint64_t(1));
EXPECT_EQ(MatComp::Abs<uint64_t>()(-2),
numeric_limits<uint64_t>::max() - 2 + 1);
EXPECT_EQ(MatComp::Abs<int8_t>()(1), int8_t(1));
EXPECT_EQ(MatComp::Abs<int8_t>()(-2), int8_t(2));
EXPECT_EQ(MatComp::Abs<int16_t>()(1), int16_t(1));
EXPECT_EQ(MatComp::Abs<int16_t>()(-2), int16_t(2));
EXPECT_EQ(MatComp::Abs<int32_t>()(1), int32_t(1));
EXPECT_EQ(MatComp::Abs<int32_t>()(-2), int32_t(2));
EXPECT_EQ(MatComp::Abs<int64_t>()(1), int64_t(1));
EXPECT_EQ(MatComp::Abs<int64_t>()(-2), int64_t(2));
EXPECT_FLOAT_EQ(MatComp::Abs<float>()(1.0), float(1.0));
EXPECT_FLOAT_EQ(MatComp::Abs<float>()(-2.0), float(2.0));
EXPECT_DOUBLE_EQ(MatComp::Abs<double>()(1.0), double(1.0));
EXPECT_DOUBLE_EQ(MatComp::Abs<double>()(-2.0), double(2.0));
}
TEST(unaryOperator, Sqrt) {
EXPECT_EQ(MatComp::Sqrt<uint8_t>()(4), uint8_t(2));
EXPECT_EQ(MatComp::Sqrt<uint16_t>()(4), uint16_t(2));
EXPECT_EQ(MatComp::Sqrt<uint32_t>()(4), uint32_t(2));
EXPECT_EQ(MatComp::Sqrt<uint64_t>()(4), uint64_t(2));
EXPECT_EQ(MatComp::Sqrt<int8_t>()(4), int8_t(2));
EXPECT_EQ(MatComp::Sqrt<int16_t>()(4), int16_t(2));
EXPECT_EQ(MatComp::Sqrt<int32_t>()(4), int32_t(2));
EXPECT_EQ(MatComp::Sqrt<int64_t>()(4), int64_t(2));
EXPECT_FLOAT_EQ(MatComp::Sqrt<float>()(4.0), float(2.0));
EXPECT_DOUBLE_EQ(MatComp::Sqrt<double>()(4.0), double(2.0));
}
TEST(unaryOperator, Log) {
EXPECT_EQ(MatComp::Log<uint8_t>()(64), uint8_t(4));
EXPECT_EQ(MatComp::Log<uint16_t>()(64), uint16_t(4));
EXPECT_EQ(MatComp::Log<uint32_t>()(64), uint32_t(4));
EXPECT_EQ(MatComp::Log<uint64_t>()(64), uint64_t(4));
EXPECT_EQ(MatComp::Log<int8_t>()(64), int8_t(4));
EXPECT_EQ(MatComp::Log<int16_t>()(64), int16_t(4));
EXPECT_EQ(MatComp::Log<int32_t>()(64), int32_t(4));
EXPECT_EQ(MatComp::Log<int64_t>()(64), int64_t(4));
EXPECT_FLOAT_EQ(MatComp::Log<float>()(64.0), std::log(float(64.0)));
EXPECT_DOUBLE_EQ(MatComp::Log<double>()(64.0), std::log(double(64.0)));
}
The tests do not attempt to check all corner cases. Please note the use of
type traits (from <numeric_limit>
) such as max
to get the maximum value
representable for a given type.
As those tests have been added to a new source file, it needs to be known to the
build system, so add it now to the matrix-test target in CMakeLists.txt
:
add_executable(matrix-test tests/main.cpp
tests/Matrix.cpp
tests/Operators.cpp
tests/Version.cpp)
cd build
ninja
ninja check
__output__...
__output__[==========] Running 23 tests from 2 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__[----------] 4 tests from unaryOperator
__output__[ RUN ] unaryOperator.Neg
__output__[ OK ] unaryOperator.Neg (0 ms)
__output__[ RUN ] unaryOperator.Abs
__output__[ OK ] unaryOperator.Abs (0 ms)
__output__[ RUN ] unaryOperator.Sqrt
__output__[ OK ] unaryOperator.Sqrt (0 ms)
__output__[ RUN ] unaryOperator.Log
__output__[ OK ] unaryOperator.Log (0 ms)
__output__[----------] 4 tests from unaryOperator (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 23 tests from 2 test suites ran. (0 ms total)
__output__[ PASSED ] 23 tests.
With the unary scalar operations in place, you can now add some matrix processing.
All the whole matrix operations have the same pattern of operation: each element in the matrix needs to be transformed with the unary scalar operation, the order in which the element are modified does not matter, so there is no need to apply the scalar operations taking into account rows and columns. Giving the compiler one simple big loop gives it the most optimization opportunities.
First, create a applyEltWiseUnaryOp
helper routine in the public section of
Matrix
in include/Matrix/Matrix.h
which is templated on the desired scalar
operation as follows:
/// Apply element wise unary scalar operator \p uOp to each element.
template <template <typename> class uOp>
Matrix &applyEltWiseUnaryOp(const uOp<Ty> &op) {
static_assert(std::is_base_of<unaryOperation, uOp<Ty>>::value,
"op must be a unaryOperation");
for (size_t i = 0; i < getNumElements(); i++)
data[i] = op(data[i]);
return *this;
}
Although applyEltWiseUnaryOp
does not need to be a public member, it allows
users to add their own operations should they lack one specific to their usage.
In other words, this provides easy extendability for the library.
applyEltWiseUnaryOp
also checks at compile time, with the static_assert
statement, that the operation that it has to perform is a unary operation.
Now include Matrix/Operators.h
at the top of Matrix/Matrix.h
so you can use
the Neg
, ‘Abs, 'Sqrt
and log
with applyEltWiseUnaryOp
. In the Matrix
public section in include/Matrix/Matrix.h
, add the neg
, abs
, sqrt
and
log
member operations:
/// Apply negate to each element of this Matrix.
Matrix &neg() { return applyEltWiseUnaryOp(Neg<Ty>()); }
/// Apply absolute value to each element of this Matrix.
Matrix &abs() { return applyEltWiseUnaryOp(Abs<Ty>()); }
/// Apply square root to each element of this Matrix.
Matrix &sqrt() { return applyEltWiseUnaryOp(Sqrt<Ty>()); }
/// Apply natural logarithm to each element of this Matrix.
Matrix &log() { return applyEltWiseUnaryOp(Log<Ty>()); }
Those member operations modify the object in place, which might not be
desirable, so you also provide a functional version of these
operators, which is a version that modifies a copy of the object
instead. This is achieved by providing functions outside of the Matrix
class - but still in the MatComp
namespace. Add these after the Matrix
class
closing };
in include/Matrix/Matrix.h
:
/// Functional version of neg.
template <typename Ty> Matrix<Ty> neg(const Matrix<Ty> &m) {
return Matrix(m).neg();
}
/// Functional version of abs.
template <typename Ty> Matrix<Ty> abs(const Matrix<Ty> &m) {
return Matrix(m).abs();
}
/// Functional version of neg.
template <typename Ty> Matrix<Ty> sqrt(const Matrix<Ty> &m) {
return Matrix(m).sqrt();
}
/// Functional version of abs.
template <typename Ty> Matrix<Ty> log(const Matrix<Ty> &m) {
return Matrix(m).log();
}
Users can now write either:
m.abs()
which modifies matrix m
in-place so that it contains the
absolute value of each of its elementsabs(m)
which returns a copy of m
with the absolute value of each of
m
elements, leaving m
untouched.Of course, each of these function needs to have tests.
Add the following
tests in tests/Matrix.cpp
:
TEST(Matrix, neg) {
Matrix<int16_t> m0(2, 2, {1, -2, -3, 4});
Matrix<int16_t> m1 = neg(m0); // Functional version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col), -m1.get(row, col));
m0.neg(); // In-place version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m0.get(row, col), m1.get(row, col));
}
TEST(Matrix, abs) {
Matrix<int64_t> m0(2, 2, {1, -2, -3, 4});
Matrix<int64_t> m1 = abs(m0); // Functional version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m1.get(row, col), std::abs(m0.get(row, col)));
m0.abs(); // In-place version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_EQ(m1.get(row, col), m0.get(row, col));
}
TEST(Matrix, sqrt) {
Matrix<double> m0(1, 4, {0, 4, 16, 64});
Matrix<double> m1 = sqrt(m0); // Functional version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_DOUBLE_EQ(m1.get(row, col), std::sqrt(m0.get(row, col)));
m0.sqrt(); // In-place version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_DOUBLE_EQ(m0.get(row, col), m1.get(row, col));
}
TEST(Matrix, log) {
Matrix<float> m0(1, 3, {4, 16, 64});
Matrix<float> m1 = log(m0); // Functional version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_FLOAT_EQ(m1.get(row, col), std::log(m0.get(row, col)));
m0.log(); // In-place version
EXPECT_EQ(m0.getNumRows(), m1.getNumRows());
EXPECT_EQ(m0.getNumColumns(), m1.getNumColumns());
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
EXPECT_FLOAT_EQ(m0.get(row, col), m1.get(row, col));
}
cd build
ninja
ninja check
__output__...
__output__[==========] Running 27 tests from 2 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 23 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.neg
__output__[ OK ] Matrix.neg (0 ms)
__output__[ RUN ] Matrix.abs
__output__[ OK ] Matrix.abs (0 ms)
__output__[ RUN ] Matrix.sqrt
__output__[ OK ] Matrix.sqrt (0 ms)
__output__[ RUN ] Matrix.log
__output__[ OK ] Matrix.log (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 23 tests from Matrix (0 ms total)
__output__
__output__[----------] 4 tests from unaryOperator
__output__[ RUN ] unaryOperator.Neg
__output__[ OK ] unaryOperator.Neg (0 ms)
__output__[ RUN ] unaryOperator.Abs
__output__[ OK ] unaryOperator.Abs (0 ms)
__output__[ RUN ] unaryOperator.Sqrt
__output__[ OK ] unaryOperator.Sqrt (0 ms)
__output__[ RUN ] unaryOperator.Log
__output__[ OK ] unaryOperator.Log (0 ms)
__output__[----------] 4 tests from unaryOperator (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 27 tests from 3 test suites ran. (0 ms total)
__output__[ PASSED ] 27 tests.
Users also need to have binary operations; operations that have two
inputs: addition, subtraction, and multiplication are the most popular
representatives of binary operations. Using the same pattern than for unary
operations, add those binary operations to include/Matrix/Operators.h
in
the MatComp
namespace:
struct binaryOperation {};
/// Add \p lhs and \p rhs.
template <typename Ty> class Add : public binaryOperation {
public:
constexpr Ty operator()(const Ty &lhs, const Ty &rhs) const {
return lhs + rhs;
}
};
/// Substract \p rhs from \p lhs.
template <typename Ty> class Sub : public binaryOperation {
public:
constexpr Ty operator()(const Ty &lhs, const Ty &rhs) const {
return lhs - rhs;
}
};
/// Multiply \p lhs by \p rhs.
template <typename Ty> class Mul : public binaryOperation {
public:
constexpr Ty operator()(const Ty &lhs, const Ty &rhs) const {
return lhs * rhs;
}
};
Add the tests to tests/Operators.cpp
:
TEST(binaryOperator, Add) {
EXPECT_EQ(MatComp::Add<uint8_t>()(1, 2), uint8_t(3));
EXPECT_EQ(MatComp::Add<uint16_t>()(3, 4), uint16_t(7));
EXPECT_EQ(MatComp::Add<uint32_t>()(5, 6), uint32_t(11));
EXPECT_EQ(MatComp::Add<uint64_t>()(7, 8), uint64_t(15));
EXPECT_EQ(MatComp::Add<int8_t>()(9, 10), int8_t(19));
EXPECT_EQ(MatComp::Add<int16_t>()(11, 12), int16_t(23));
EXPECT_EQ(MatComp::Add<int32_t>()(13, 14), int32_t(27));
EXPECT_EQ(MatComp::Add<int64_t>()(15, 16), int64_t(31));
EXPECT_FLOAT_EQ(MatComp::Add<float>()(8.0, 12.0), float(20.0));
EXPECT_DOUBLE_EQ(MatComp::Add<double>()(32.0, 64.0), double(96.0));
}
TEST(binaryOperator, Sub) {
EXPECT_EQ(MatComp::Sub<uint8_t>()(2, 1), uint8_t(1));
EXPECT_EQ(MatComp::Sub<uint16_t>()(3, 1), uint16_t(2));
EXPECT_EQ(MatComp::Sub<uint32_t>()(5, 2), uint32_t(3));
EXPECT_EQ(MatComp::Sub<uint64_t>()(8, 3), uint64_t(5));
EXPECT_EQ(MatComp::Sub<int8_t>()(9, 10), int8_t(-1));
EXPECT_EQ(MatComp::Sub<int16_t>()(13, 11), int16_t(2));
EXPECT_EQ(MatComp::Sub<int32_t>()(14, 14), int32_t(0));
EXPECT_EQ(MatComp::Sub<int64_t>()(15, 16), int64_t(-1));
EXPECT_FLOAT_EQ(MatComp::Sub<float>()(8.0, 12.0), float(-4.0));
EXPECT_DOUBLE_EQ(MatComp::Sub<double>()(32.0, 64.0), double(-32.0));
}
TEST(binaryOperator, Mul) {
EXPECT_EQ(MatComp::Mul<uint8_t>()(1, 2), uint8_t(2));
EXPECT_EQ(MatComp::Mul<uint16_t>()(3, 4), uint16_t(12));
EXPECT_EQ(MatComp::Mul<uint32_t>()(5, 6), uint32_t(30));
EXPECT_EQ(MatComp::Mul<uint64_t>()(7, 8), uint64_t(56));
EXPECT_EQ(MatComp::Mul<int8_t>()(9, 10), int8_t(90));
EXPECT_EQ(MatComp::Mul<int16_t>()(11, 12), int16_t(11 * 12));
EXPECT_EQ(MatComp::Mul<int32_t>()(13, 14), int32_t(13 * 14));
EXPECT_EQ(MatComp::Mul<int64_t>()(15, 16), int64_t(15 * 16));
EXPECT_FLOAT_EQ(MatComp::Mul<float>()(8.0, 12.0), float(96.0));
EXPECT_DOUBLE_EQ(MatComp::Mul<double>()(32.0, 64.0), double(2048.0));
}
Check the code compiles and passes the checks:
cd build
ninja
ninja check
__output__...
__output__[==========] Running 30 tests from 3 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 23 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.neg
__output__[ OK ] Matrix.neg (0 ms)
__output__[ RUN ] Matrix.abs
__output__[ OK ] Matrix.abs (0 ms)
__output__[ RUN ] Matrix.sqrt
__output__[ OK ] Matrix.sqrt (0 ms)
__output__[ RUN ] Matrix.log
__output__[ OK ] Matrix.log (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 23 tests from Matrix (0 ms total)
__output__
__output__[----------] 4 tests from unaryOperator
__output__[ RUN ] unaryOperator.Neg
__output__[ OK ] unaryOperator.Neg (0 ms)
__output__[ RUN ] unaryOperator.Abs
__output__[ OK ] unaryOperator.Abs (0 ms)
__output__[ RUN ] unaryOperator.Sqrt
__output__[ OK ] unaryOperator.Sqrt (0 ms)
__output__[ RUN ] unaryOperator.Log
__output__[ OK ] unaryOperator.Log (0 ms)
__output__[----------] 4 tests from unaryOperator (0 ms total)
__output__
__output__[----------] 3 tests from binaryOperator
__output__[ RUN ] binaryOperator.Add
__output__[ OK ] binaryOperator.Add (0 ms)
__output__[ RUN ] binaryOperator.Sub
__output__[ OK ] binaryOperator.Sub (0 ms)
__output__[ RUN ] binaryOperator.Mul
__output__[ OK ] binaryOperator.Mul (0 ms)
__output__[----------] 3 tests from binaryOperator (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 30 tests from 3 test suites ran. (0 ms total)
__output__[ PASSED ] 30 tests.
Now that scalar binary operators are implemented, you can add Matrix
level
binary operators. The pattern is very similar to the one used for unary
operators: you add a helper routine that traverses the matrices and
applies the scalar operator, but with an extra step. Although the element wise
processing of matrices does not require a specific order for the rows or
columns, the matrices must have the same dimensions; if not, this is an error
and the program must be terminated.
Add this applyEltWiseBinaryOp
helper routine to the public section of Matrix
in include/Matrix/Matrix.h
:
/// Apply element wise binary scalar operator \p bOp to each element.
template <template <typename> class bOp>
Matrix &applyEltWiseBinaryOp(const bOp<Ty> &op, const Matrix &rhs) {
static_assert(std::is_base_of<binaryOperation, bOp<Ty>>::value,
"op must be a binaryOperation");
if (getNumRows() != rhs.getNumRows() ||
getNumColumns() != rhs.getNumColumns())
die(__FILE__, __LINE__,
"Inconsistent Matrix dimensions for elementwise operation");
for (size_t i = 0; i < getNumElements(); i++)
data[i] = op(data[i], rhs.data[i]);
return *this;
}
Add to the public section of Matrix
in include/Matrix/Matrix.h
the
add
, sub
and mul
operators, composing the matrices traversal
applyEltWiseBinaryOp
with the scalar operators you’ve just added:
// Add Matrix \p rhs to this Matrix.
Matrix &operator+=(const Matrix &rhs) {
return applyEltWiseBinaryOp(Add<Ty>(), rhs);
}
// Substract Matrix \p rhs from this Matrix.
Matrix &operator-=(const Matrix &rhs) {
return applyEltWiseBinaryOp(Sub<Ty>(), rhs);
}
// Element wise multiplication of this Matrix by \p rhs (a.k.a dot product).
Matrix &operator*=(const Matrix &rhs) {
return applyEltWiseBinaryOp(Mul<Ty>(), rhs);
}
These operators do modify the objects in-place, which explains the +=
, -=
and *=
, so
you now add functional versions of these operations. Outside of the
Matrix
class in include/Matrix/Operators.h
, but in the MatComp
namespace,
add:
/// Add Matrix \p lhs and \p rhs.
template <typename Ty>
Matrix<Ty> operator+(const Matrix<Ty> &lhs, const Matrix<Ty> &rhs) {
return Matrix(lhs) += rhs;
}
/// Subtract Matrix \p rhs from Matrix \p lhs.
template <typename Ty>
Matrix<Ty> operator-(const Matrix<Ty> &lhs, const Matrix<Ty> &rhs) {
return Matrix(lhs) -= rhs;
}
/// Elementwise multiplication (a.k.a dot product) of Matrix \p lhs by \p rhs.
template <typename Ty>
Matrix<Ty> operator*(const Matrix<Ty> &lhs, const Matrix<Ty> &rhs) {
return Matrix(lhs) *= rhs;
}
You can see that those operation first make a copy of the lhs
object with
Matrix(lhs)
, then modify the copy in place with rhs
and then return the
result without modifying lhs
or rhs
.
Add tests for all these in-place and functional operators in
tests/Matrix.cpp
:
TEST(Matrix, inPlaceAdd) {
Matrix<int16_t> m0 = Matrix<int16_t>::ones(3, 3);
const Matrix<int16_t> m1 = Matrix<int16_t>::identity(3);
m0 += m1;
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 3);
EXPECT_EQ(m0.getNumColumns(), 3);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), 2);
else
EXPECT_EQ(m0.get(row, col), 1);
}
TEST(Matrix, inPlaceSub) {
Matrix<int32_t> m0 = Matrix<int32_t>::ones(4, 4);
const Matrix<int32_t> m1 = Matrix<int32_t>::identity(4);
m0 -= m1;
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 4);
EXPECT_EQ(m0.getNumColumns(), 4);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), 0);
else
EXPECT_EQ(m0.get(row, col), 1);
}
TEST(Matrix, inPlaceDotProduct) {
Matrix<int64_t> m0(2, 2, {1, 2, 3, 4});
const Matrix<int64_t> m1 = Matrix<int64_t>::identity(2);
m0 *= m1;
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 2);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), row * m0.getNumColumns() + col + 1);
else
EXPECT_EQ(m0.get(row, col), 0);
}
TEST(Matrix, functionalAdd) {
Matrix<int16_t> m0 =
Matrix<int16_t>::ones(3, 3) + Matrix<int16_t>::identity(3);
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 3);
EXPECT_EQ(m0.getNumColumns(), 3);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), 2);
else
EXPECT_EQ(m0.get(row, col), 1);
}
TEST(Matrix, functionalSub) {
Matrix<int32_t> m0 =
Matrix<int32_t>::ones(4, 4) - Matrix<int32_t>::identity(4);
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 4);
EXPECT_EQ(m0.getNumColumns(), 4);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), 0);
else
EXPECT_EQ(m0.get(row, col), 1);
}
TEST(Matrix, functionalDotProduct) {
Matrix<int64_t> m0 =
Matrix<int64_t>(2, 2, {1, 2, 3, 4}) * Matrix<int64_t>::identity(2);
EXPECT_TRUE(m0);
EXPECT_EQ(m0.getNumRows(), 2);
EXPECT_EQ(m0.getNumColumns(), 2);
for (size_t row = 0; row < m0.getNumRows(); row++)
for (size_t col = 0; col < m0.getNumColumns(); col++)
if (row == col)
EXPECT_EQ(m0.get(row, col), row * m0.getNumColumns() + col + 1);
else
EXPECT_EQ(m0.get(row, col), 0);
}
cd build
ninja
ninja check
__output__...
__output__[==========] Running 36 tests from 3 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 29 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.neg
__output__[ OK ] Matrix.neg (0 ms)
__output__[ RUN ] Matrix.abs
__output__[ OK ] Matrix.abs (0 ms)
__output__[ RUN ] Matrix.sqrt
__output__[ OK ] Matrix.sqrt (0 ms)
__output__[ RUN ] Matrix.log
__output__[ OK ] Matrix.log (0 ms)
__output__[ RUN ] Matrix.inPlaceAdd
__output__[ OK ] Matrix.inPlaceAdd (0 ms)
__output__[ RUN ] Matrix.inPlaceSub
__output__[ OK ] Matrix.inPlaceSub (0 ms)
__output__[ RUN ] Matrix.inPlaceDotProduct
__output__[ OK ] Matrix.inPlaceDotProduct (0 ms)
__output__[ RUN ] Matrix.functionalAdd
__output__[ OK ] Matrix.functionalAdd (0 ms)
__output__[ RUN ] Matrix.functionalSub
__output__[ OK ] Matrix.functionalSub (0 ms)
__output__[ RUN ] Matrix.functionalDotProduct
__output__[ OK ] Matrix.functionalDotProduct (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 29 tests from Matrix (0 ms total)
__output__
__output__[----------] 4 tests from unaryOperator
__output__[ RUN ] unaryOperator.Neg
__output__[ OK ] unaryOperator.Neg (0 ms)
__output__[ RUN ] unaryOperator.Abs
__output__[ OK ] unaryOperator.Abs (0 ms)
__output__[ RUN ] unaryOperator.Sqrt
__output__[ OK ] unaryOperator.Sqrt (0 ms)
__output__[ RUN ] unaryOperator.Log
__output__[ OK ] unaryOperator.Log (0 ms)
__output__[----------] 4 tests from unaryOperator (0 ms total)
__output__
__output__[----------] 3 tests from binaryOperator
__output__[ RUN ] binaryOperator.Add
__output__[ OK ] binaryOperator.Add (0 ms)
__output__[ RUN ] binaryOperator.Sub
__output__[ OK ] binaryOperator.Sub (0 ms)
__output__[ RUN ] binaryOperator.Mul
__output__[ OK ] binaryOperator.Mul (0 ms)
__output__[----------] 3 tests from binaryOperator (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 36 tests from 3 test suites ran. (0 ms total)
__output__[ PASSED ] 36 tests.
Last, but not least, the Matrix
class still requires the most important operation:
matrix multiplication.
Add the multiply
function, outside of the Matrix
class, but in the
MatComp
namespace in include/Matrix/Matrix.h
. multiply
checks that
the arguments have compatible dimensions, then create an uninitialized
matrix that holds the multiplication result and eventually perform the
multiplication to fill it.
/// Matrix multiplication.
template <typename Ty>
Matrix<Ty> multiply(const Matrix<Ty> &lhs, const Matrix<Ty> &rhs) {
if (lhs.getNumColumns() != rhs.getNumRows())
die(__FILE__, __LINE__, "Incompatible dimensions in Matrix multiply");
Matrix<Ty> result(lhs.getNumRows(), rhs.getNumColumns());
for (size_t row = 0; row < result.getNumRows(); row++)
for (size_t col = 0; col < result.getNumColumns(); col++) {
Ty acc = Ty(0);
for (size_t t = 0; t < lhs.getNumColumns(); t++)
acc += lhs.get(row, t) * rhs.get(t, col);
result.get(row, col) = acc;
}
return result;
}
Add some tests in tests/Matrix.cpp
:
TEST(Matrix, multiplication) {
// Test matrix x matrix.
Matrix<double> m0 = multiply(Matrix<double>(2, 2, {1., 2., 3., 4.}),
Matrix<double>(2, 2, {0., 1., 2., 3.}));
EXPECT_TRUE(m0);
const Matrix<double> m0_exp(2, 2, {4., 7., 8., 15.});
EXPECT_EQ(m0.getNumColumns(), m0_exp.getNumColumns());
EXPECT_EQ(m0.getNumRows(), m0_exp.getNumRows());
EXPECT_EQ(m0.getNumElements(), m0_exp.getNumElements());
for (size_t row = 0; row < m0_exp.getNumRows(); row++)
for (size_t col = 0; col < m0_exp.getNumColumns(); col++)
EXPECT_DOUBLE_EQ(m0.get(row, col), m0_exp.get(row, col));
// Test matrix x vector
Matrix<float> m1 =
multiply(Matrix<float>(3, 3, {1., 2., 3., 4., 5., 6., 7., 8., 9.}),
Matrix<float>(3, 1, {1., 2., 3.}));
EXPECT_TRUE(m1);
const Matrix<float> m1_exp(3, 1, {14., 32., 50.});
EXPECT_EQ(m1.getNumColumns(), m1_exp.getNumColumns());
EXPECT_EQ(m1.getNumRows(), m1_exp.getNumRows());
EXPECT_EQ(m1.getNumElements(), m1_exp.getNumElements());
for (size_t row = 0; row < m1_exp.getNumRows(); row++)
for (size_t col = 0; col < m1_exp.getNumColumns(); col++)
EXPECT_FLOAT_EQ(m1.get(row, col), m1_exp.get(row, col));
}
cd build
ninja
ninja check
__output__...
__output__[==========] Running 37 tests from 3 test suites.
__output__[----------] Global test environment set-up.
__output__[----------] 30 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.neg
__output__[ OK ] Matrix.neg (0 ms)
__output__[ RUN ] Matrix.abs
__output__[ OK ] Matrix.abs (0 ms)
__output__[ RUN ] Matrix.sqrt
__output__[ OK ] Matrix.sqrt (0 ms)
__output__[ RUN ] Matrix.log
__output__[ OK ] Matrix.log (0 ms)
__output__[ RUN ] Matrix.inPlaceAdd
__output__[ OK ] Matrix.inPlaceAdd (0 ms)
__output__[ RUN ] Matrix.inPlaceSub
__output__[ OK ] Matrix.inPlaceSub (0 ms)
__output__[ RUN ] Matrix.inPlaceDotProduct
__output__[ OK ] Matrix.inPlaceDotProduct (0 ms)
__output__[ RUN ] Matrix.functionalAdd
__output__[ OK ] Matrix.functionalAdd (0 ms)
__output__[ RUN ] Matrix.functionalSub
__output__[ OK ] Matrix.functionalSub (0 ms)
__output__[ RUN ] Matrix.functionalDotProduct
__output__[ OK ] Matrix.functionalDotProduct (0 ms)
__output__[ RUN ] Matrix.multiplication
__output__[ OK ] Matrix.multiplication (0 ms)
__output__[ RUN ] Matrix.getVersion
__output__[ OK ] Matrix.getVersion (0 ms)
__output__[----------] 30 tests from Matrix (0 ms total)
__output__
__output__[----------] 4 tests from unaryOperator
__output__[ RUN ] unaryOperator.Neg
__output__[ OK ] unaryOperator.Neg (0 ms)
__output__[ RUN ] unaryOperator.Abs
__output__[ OK ] unaryOperator.Abs (0 ms)
__output__[ RUN ] unaryOperator.Sqrt
__output__[ OK ] unaryOperator.Sqrt (0 ms)
__output__[ RUN ] unaryOperator.Log
__output__[ OK ] unaryOperator.Log (0 ms)
__output__[----------] 4 tests from unaryOperator (0 ms total)
__output__
__output__[----------] 3 tests from binaryOperator
__output__[ RUN ] binaryOperator.Add
__output__[ OK ] binaryOperator.Add (0 ms)
__output__[ RUN ] binaryOperator.Sub
__output__[ OK ] binaryOperator.Sub (0 ms)
__output__[ RUN ] binaryOperator.Mul
__output__[ OK ] binaryOperator.Mul (0 ms)
__output__[----------] 3 tests from binaryOperator (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 37 tests from 3 test suites ran. (0 ms total)
__output__[ PASSED ] 37 tests.
A complete matrix processing library requires some other important operations:
transpose
operator for the Matrix
class?Matrix
users, as they allow
to transform a scalar or a row-vector/column-vector by replicating it to make
it look like a 2-D matrix. Again, on top of syntactic sugar for the users, this
provides a large performance improvement, especially in memory usage, as one can
play with how the elements are accessed rather than duplicating the matrix
content.At this stage, the code structure looks like:
Matrix/
├── CMakeLists.txt
├── build/
...
├── external/
│ └── CMakeLists.txt
├── include/
│ └── Matrix/
│ ├── Matrix.h
│ └── Operators.h
├── lib/
│ └── Matrix/
│ └── Matrix.cpp
├── src/
│ ├── getVersion.cpp
│ └── howdy.cpp
└── tests/
├── Matrix.cpp
├── Operators.cpp
├── Version.cpp
└── main.cpp
Congratulations, you now have a minimalistic, yet fully-functional, matrix processing library, with some level of regression testing, that can be easily built and used.
The testing could - and should - go much deeper, as a number of corner cases have not been covered.
You can continue to add more functions, and more tests.