License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright :
Frédéric Pennerath,
CentraleSupelec
Last modified : April 9, 2024 11:25
Link to the source : matrix.md
The goal of this programming exercise is for you to learn how to write the main forms of templates and to use some of their features, like partial specialization or conditional branching (if constexpr
).
To this end, we will develop a class template for mathematical matrices, that is very similar in its principle to the class template Vector
introduced during the lectures. In particular we consider matrices whose size \(n \times m\) (\(n\) is the number of row, \(m\) the number of column) is fixed at compile time and can therefore be stored in a fixed-size array. Matrices can then be created very quickly as local variables directly on the call stack without storing components in the heap, using dynamic allocation (new
). This feature can then be used intensively to proceed intermediary computations very efficiently. Integrating matrix dimension into the template parameters also makes these dimensions as an inherent part of the matrix type so that matrices with different dimensions are considered to be of different types. This is particularly interesting to detect dimension inconsistencies at compile time.
As a summary, the purpose of the template class Matrix
is to provide a generic, while at the same time, efficient and strongly typed implementation of matrices, where genericity refers to the freedom for the users of your template, to choose
Scalar
of components on which the matrix is defined (like double
, float
, or any other algebraic field like std::complex
)n
and m
of the matrix of type int
.In a file
Matrix.hpp
implement from scratch a class templateMatrix
parameterized byn
,m
andScalar
(by default equal todouble
) that defines
- An alias type
scalar_type
to “export” typeScalar
.- A constructor that sets its matrix to a scalar constant. If omitted, the scalar constant will be initialized by calling its default constructor
Scalar{}
. One will assume that this default constructor will return the zero for the considered field, i.e. the identity with respect to addition (this is actually true for standard numerical types likeint
,float
, etc).- A copy constructor using the default definition.
- A read/write accessor
operator[](std::pair<int,int> coords)
that returns a reference of typeScalar
to the component of the matrix of coordinatescoords
(i.e the pair \((i,j)\) where \(i\) is the row index and \(j\) the column index, both starting from \(0\)).
.hpp
(i.e do not use .cpp
file to define template functions and methods).std::array
, which is just a class wrapping a native array whose sole purpose is to provide utility methods like begin
, end
and size
so that loops like for(auto& x : array) { ... }
can be used. Components will be stored in this array, from top to bottom, left to right.operator[]
is only provided to the user in order to initialize a given component in a matrix. For sake of efficiency, you should not call it from any method or operator of Matrix
you will develop. Write your loops by incrementing iterators instead (which are actually native C pointer for std::array
).matrix
.#define QUESTION
to define which tests you want to compile (as their compilation will depend on your progress).Add an external operator template
<<
that serializes a matrix into an output streamstd::ostream
.
Do not forget to add the right friend
statement (see the similar example of class Vector
in the lectures). Test (see test-Matrix.cpp
).
Implement internal operators computing the sum and product of two matrices.
One recalls that given two matrices \(A\) and \(B\) of respective size \((n_A,m_A)\) and \((n_B,m_B)\) where \(n\) is the number of rows and \(m\) is the number of columns, then
Sum \(A + B\) requires that \(n_A = n_B\) and \(m_A = m_B\). Then \(A + B\) is a matrix of size \((n_A,m_A)\) such that its coefficient \((i,j)\) is \[(A + B)_{i,j} = A_{i,j} + B_{i,j}\]
Product \(A \times B\) requires that \(m_A = n_B\). Then \(A \times B\) is a matrix of size \((n_A,m_B)\) such that \[(A \times B)_{i,j} = \sum_{k=1}^{m_A} A_{i,k} \times B_{k,j}\]
Test what happens when one attempts to multiply two matrices with inconsistent dimensions?
if constexpr
When computing a product \(A \times B\) of two matrices such that \(A\) is a column vector (i.e. \(m_A = 1\)) and thus \(B\) is a row vector (i.e. \(n_B = 1\)), the standard implementation is particularly unefficient. Can you see why?
Using
if constexpr
as introduced inC++17
, specialize the code of the product operator when \(m_A = 1\) so that the code is more efficient.
static_assert
In order to produce interpretable error message, implement an internal operator operator*
that accept as right operand any matrix, including those with sizes incompatible with the right operand (i.e the current object *this
). In that operator, introduce a static_assert
statement that produces a compiler error if dimensions are inconsistent.
Matrices, as arrays (or std::array
), store their components directly inside the object Matrix
. This is only possible because the matrix has a fixed size known at compile time. This property allows the matrix components to be stored directly on the stack and thus to gain in performance (no dynamic allocation, no memory indirection). However, storing on the stack can quickly become a drawback when the matrix contains many coefficients to the point of causing stack overflow.
So we would like a class template Matrix
to store its components in the heap instead of in the stack as soon as the number of its components exceeds a certain threshold (for example 10000). To do this, one simple solution is to replace std::array
by std::vector
when the size exeeds a threshold. We could specialize directly the class matrix but this would require to duplicate all methods and operators. Instead we can introduce a template class Storage
whose sole purpose is to store components either in an array or in a vector. Class Matrix
then inherits from Storage
.
Modify your implementation accordingly, by defining class template
Storage
parameterized byScalar
, the dimension of the matrix and a fourth boolean argumentstack
that tells whetherStorage
must store components in an array (ifstack
is true) or in a vector (ifstack
is false). Use partial specialization of class template (see example in the lectures) to implement both versions (with array and with vector). Then make class templateMatrix
inherits fromStorage
by setting thestack
argument to the right value. To to this, one will introduce aconstexpr
functionconstexpr bool best_on_stack(size_t n_rows, size_t n_columns)
that will returns true if and only if the components are better stored on the stack.
resize
.if constexpr
is pointless here as it cannot help to specialize attributes. Only template specialization can do it.static const bool on_stack
in each specialization, that tells whether the components are stored in an array or not.We now wants to formalize and document syntactic constraints that Scalar
must fullfil to be an algebraic field.
Introduce a concept
Field
that lists syntactic constraints of an algebraic field (See the example in lectures on generic programming). Then constraint parameterScalar
of template classmatrix
to satisfy conceptField
. Test by instantiating an absurd matrix (e.g a matrix defined onstd::string
). See error messages.
C++20
. Be sure to compile with the option --std=c++20
and to use a recent compiler.When computing the product of two matrices \(A\) and \(B\), the termination test of the internal loop is particularly costly (why?).
Update your class template to do loop unrolling in the internal loop.
k
successive iterations using loop unrolling with k
typically equal to 4 or 8.k
. Then manage properly rounding when k
does not divide anymore \(m_A\).