1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
\title{A5: Generic Programming}
\input{preamble.tex}
In this exercise a matrix class is being developed and gradually
evolved into a typesafe templated component.
\bigskip
I decided to use one of the standard containers for storing the elements of
the matrix and initially went with \texttt{std::vector<int>}. But
since we were going to do arithmentics on the data, and no
requirements were made as to the arithmentics actually being those of
a real matrix, I figure why not use \texttt{std::valarray<int>} and
have all arithmentic operations be per-entry, re-using the behaviour
of \texttt{std::valarray} directly.
During this change I discovered that \texttt{std::valarray} and
\texttt{std::vector} ctor args for creating with a size and a default
value for all items are reversed, which would not be caught by the
compiler with both of them being integral:
\footnotesize\begin{lstlisting}[language=C++]
explicit vector( size_type count,
const T& value = T(),
const Allocator& alloc = Allocator() );
valarray( const T& val, std::size_t count );
\end{lstlisting}\normalsize
That, I think, is rather unfortunate.
When using a standard container, all memory managing is handled by the
container, even when throwing an exception inside the constructor, so
tere is no need to do anything clever inside the matrix implementation
itself. Otherwise a allocating base-class could have been made for
memory ownership as seen during one of the presentations.
The implementation for the statically typed \texttt{Imatrix} can be
seen in the \texttt{imatrix.h} file.
I first wrote the entire \texttt{Imatrix} class with the values
dynamically allocated inside the \texttt{std::valarray} but wanted to
explore having the matrix dimenions available at compile-time
(matrices doesn't have the habit of changing their sizes dynamically,
so they should be checkable at compile-time). I did this with template
size arguments.
First of all, this removed a lot of error checks on matrix dimension
matching, which is now part of the type itself.
It also removed the need for the two integer members from the first
imeplementation, carrying the matrix dimensions, so the change now
also saved 16 bytes of memory.
And, best of all, we get a compile error if for example we try
multiplying two matrices where their dimensions doesn't adhere to the
algebraic rules!
For the \texttt{Row()} and \texttt{Column()} I briefly considered
returning a span to be able to return references to a row or column
for dynamic iteration, but decided to stick with
the \texttt{std::vector} as described in the assignment.
The code for the size-templated \texttt{Imatrix<N,M>} can be seen in
the \texttt{imatrix\_nm.h} file.
\bigskip
Next step was to templatize the underlying type of the matrix. This
was more or less done by simply replacing \texttt{int} with \texttt{T}
and all the places where 0 is assigned to the value type, replacing it
with the default value \texttt{T\{\}}.
For the \texttt{Move()} function a $0$ assignment was needed and this
I replaced with an empty initializer list, \texttt{\{\}}.
The new templatized class is called \texttt{Matrix} and can be found in
the \texttt{matrix.h} file.
When instantiating a template only the required/used functions are
generated (or so it appears).
In other words; a \texttt{Matrix<Chess\_piece,2,2> c1} can be instantiated
without getting any compile errors as long as no algebraic methods are
being called on it.
If the line \texttt{c1 + 1} is added somewhere it will complain about
not being able to add with \texttt{int}, but not any of the other unused
operations.
When adding the requirements of concepts, I expect to get errors for
all the uses, not just the ones used in the program.
In other words, concepts might end up requirering more work to be done
by a developer than strictly needed. This might not always be
desirebale - or at least it is something that should be thought about
when designing a new component.
One option to mimic the same behaviour, but with concepts, could
perhaps be to add requirements to each of the member functions. This
is however not what I did in this exercise.
I made a concept, \texttt{supports\_modulo<T1,T2>}, used for only
``activating'' the modulo operator if the underlying type supports it.
I made an extensive, templated, test function that exercised all
operator types of any of the matrix types - and here the compiler errors
for \texttt{Chess\_piece} were not very helpful (the same goes
for \texttt{std::string}). This can be seen in the \texttt{matrix.cc}
file.
In the test program, to prevent performing modulo operations on
matrices that doesn't support them, I used
the \texttt{supports\_modulo} concept together with a \texttt{if
constexpr} to only try to call the module operator when enabled for
the class.
I made another concept, \texttt{supports\_matrix<T>}, used for the
underlying type of the \texttt{Matrix<T>} class itself, requirering
algebraic functionality on the underlying type.
After adding requirements for all the needed operators,
the \texttt{Chess\_piece} and \texttt{std::string} matrices now failed
as expected but this time with a much shorter and to-the-point error
message telling me exactly what would be needed for them work.
I added empty dummy operator functions to fulfill the requirements (and
commented out the \texttt{std::string} one) and now the program
compiles and runs as expected.
In hindsight, using the \texttt{std::valarray} may have resulted in me
writing less code in the \texttt{Matrix} implementation, but because
of implementation details inside \texttt{std::valarray} I may have
ended up having to write more algebraic operators
on \texttt{Chess\_piece} to get it to work.
\bigskip
The approach of first writing the statically typed class, then
templatizing it, and finally analyzing the actual requirements of the
underlying type and expressing those as a concept requirements, was a
nice way to ``eat the elephant'', being able to focus on just one
aspect of the code at one time. My guess is also that it would have
been hard to foresee what the actual type requirements would end up being
without having written the code yet.
\end{document}
|