As quite a few people have already pointed out it is quite a poor matrix implementation: It let's you make assumptions about the internal implementation and has quite a few flaws. But I would lie if I said I would not have already seen implementations like this in research codes. ;) Instead of bashing the implementation I would like to briefly point out how it works as nonetheless it shows a few particularities of C++.
Overview
// Class for a generic data type T (e.g. T = float)
template<typename T>
class Matrix44 {
public:
// Constructor
Matrix44() {
return;
}
// Access operator for constant objects
const T* operator [] (uint8_t i) const {
return m[i];
}
// Access operator for non-constant objects
T* operator [] (uint8_t i) {
return m[i];
}
// Declaration of a stack-allocated array as class member
T m[4][4]
// Initialisation of this class member
= {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};
};
// Alias Matrix44f for a matrix of floats (T = float)
typedef Matrix44<float> Matrix44f;
Stack allocated arrays
In C++ unlike in other programming languages one can allocate arrays on the stack as well as on the heap. A stack allocated one-dimensional array can (of integers) be declared as
int v[3];
while
int m[2][3];
would be the declaration of a multidimensional (in this case two-dimensional) row-major array which can be initialised with
int m[2][3] = {{1,2},{3,4},{5,6}};
(similar to how you could initialise a vector of vectors with an initialiser list) and whose elements can then be accessed with a similar syntax m[i][j]
(also similar to a vector of vectors std::vector<std::vector<T>>
, Watch out: No out-of-bound checks!).
As you can see your class is only a wrapper for a such a stack-allocated two-dimensional array of a generic type T
,
T m[4][4];
a so called template class
. Instantiating the class as Matrix44<float>
makes it a matrix of T = float
while for a matrix Matrix44<int>
the underlying data type would be int
. With typedef Matrix44<float> Matrix44f
at the bottom a new alias for this data type was create. So one can declare a variable Matrix44f mat
.
Stack-allocated means that its size is quite limited compared to a heap-allocated array which isn't very prohibiting for a 4x4
array though (but why would you not extend this implementation at least to NxN
?). Furthermore the way it is written it does not even have a proper constructor (Matrix44() {}
) but is instead default-initialised with an identity matrix
{{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}
(why would somebody do that?).
Operator []
C++ gives you the possibility to overload operators. So one can define (overload) basic operations such as addition operator +
, multiplication operator *
, comparisons etc. Similarly one can also overload []
(but only with a single argument and therefore numerical libraries such as Eigen
opt to overload the ()
operator instead), ()
and ,
. But there is no such thing as a [][]
overload! In order to allow a matrix access from outside the class with [][]
similar to the stack-allocated array it was chosen to write an operator []
that returns a pointer to the first element of the second dimension of the underlying two-dimensional array at the corresponding index i
of the first dimension m[i][0]
: return m[i]
is equivalent to return &m[i][0]
. One can apply then the operator []
of the pointer p[j]
which is equivalent to *(p+j)
(see here for more details) to move this pointer by j-entries to access the element m[i][j]
. (This is sort of similar to using a vector-of-vectors instead of a stack-allocated-array, returning a std::vector<T>&
from the operator []
and then using the []
operator of the vector to access the precise element.)
Member functions can be declared constant (see the const
after the argument list) which means they can also be called for constant objects while if not declared constant they can only be called for non-constant objects. For constant objects the constant version is called while for non-constant objects the non-constant implementation is called (const
overloading). In order to allow assignments the non-constant version
T* operator [] (uint8_t i) {
return m[i];
}
returns a non-constant pointer T*
which can be modified while the constant implementation returns a pointer to a constant variable const T*
.
const T* operator [] (uint8_t i) const {
return m[i];
}
This is the reason why there are two of these implementations. For a constant object the latter one will be called which does not allow assignment (but you can read the element 1, 2
with m[1][2]
) while for a non-constant one the first one which allows assignment (e.g. m[1][2] = 1
will set the element 1, 2
to 1
).