1

I am trying to understand the access operator for a Matrix Multiplication.

template<typename T> 
class Matrix44 
{ 
public: 
    Matrix44() {} 
    // The next two lines are totally confusing for me
    const T* operator [] (uint8_t i) const { return m[i]; }
    T* operator [] (uint8_t i) { return m[i]; }
    // initialize the coefficients of the matrix with the coefficients of the identity matrix
    T m[4][4] = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}; // Why can you do this m[4][4]
}; 
 
typedef Matrix44<float> Matrix44f; 

So what I understood is that they defined an own access operator for accessing the matrix indices:

Matrx44f mat; 
mat[0][3] = 1.f; 

But how does that relate to their defintion

...
const T* operator [] (uint8_t i) const { return m[i]; }
T* operator [] (uint8_t i) { return m[i]; }

Thank you very much for helping a C++ noob <3

Source: https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/matrices

LeSchwambo
  • 674
  • 6
  • 12
  • Honestly that’s a fairly poor matrix implementation which leaks implementation details via its interface. A *proper* implementation doesn’t do this. – Konrad Rudolph Apr 27 '21 at 12:56
  • Thanks for your replay but that is good to know then I won't dig deeper :D – LeSchwambo Apr 27 '21 at 13:02
  • 1
    Is the point you're missing that pointers can be indexed like arrays? (If `p` is a pointer, `p[k]` is equivalent to `*(p + k)`, and `mat[0]` is a pointer.) For more mystery, consider that you can also write `3[mat[0]] = 1.f;`... – molbdnilo Apr 27 '21 at 13:15
  • @KonradRudolph Have you looked through the header files for the STL on any major implementations? Not sure how your definition of *proper* would apply there. – Adrian Mole Apr 27 '21 at 13:29
  • @molbdnilo haha your last sentence is great. More mystery is adding up ;) – LeSchwambo Apr 27 '21 at 13:31
  • @AdrianMole not yet I am quite at the beginning of the C++ Odyssey :D – LeSchwambo Apr 27 '21 at 13:32
  • @AdrianMole I’m not sure what you’re referring to. There’s no matrix implementation in the standard library, and it wouldn’t implement the element access operator via this hack. – Konrad Rudolph Apr 27 '21 at 13:46
  • @Konrad My comment was more about the *leaks implementation details* part. I agree that this is a poor matrix implementation but not that "leaking details" is inherently bad. What about header-only libraries? – Adrian Mole Apr 27 '21 at 13:54
  • @AdrianMole Header-only libraries are a necessary evil since C++ hasn’t got proper modules (yet). It has little to nothing to do with the topic at hand (API design) and only serves to confuse OP. – Konrad Rudolph Apr 27 '21 at 14:26

2 Answers2

2

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).

2b-t
  • 2,414
  • 1
  • 10
  • 18
  • Thank you for your detailed explanation and for taking the time to give such a thorough and nice answer! I didn't think my question would reveal such a deep problem. It certainly took time to formulate all this. For this I apologiz, but I deeply appreciate it! – LeSchwambo Apr 28 '21 at 15:30
  • You are welcome, always glad if I can help! – 2b-t Apr 28 '21 at 17:54
1

First, remember that the only value of this class is to give value semantics to a C-array and that is not yet achieved with the shown code. (Sort of what std::array<T, N> does.)

Besides that, this is a bad implementation that relies in pointer decay, the reference to the 4-element array (once you take the first index), decays into an pointer in your implementation. At best the decay looses useful type information.

So, as it is, m[i] returns a reference to a C-array and it is later decayed into a pointer by the return type of operator[] of the class. A better implementation could simply be decltype(auto) operator[](int i){return m[i];}.

If you want a more explicit description of what is going on you can do

    using reference = T(&)[4]; // reference to a 4-element subarray
    reference const operator[](int i) const { return m[i]; }
    reference       operator[](int i)       { return m[i]; }

https://godbolt.org/z/EjbWY691x

At this point, it might be better to rely in an existing good wrapper and simply say

template<class T> using Matrix44 = std::array<std::array<T, 4>, 4>;

If the initialization to identity is important (not a good idea IMO)...

template<typename T> 
class Matrix44 
{ 
public: 
    Matrix44() {} 
    // The next two lines are totally confusing for me
    decltype(auto) operator [] (uint8_t i) const { return m[i]; }
    decltype(auto) operator [] (uint8_t i)       { return m[i]; }
    // initialize the coefficients of the matrix with the coefficients of the identity matrix
    std::array<std::array<T, 4>, 4> m = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};
}; 
 
typedef Matrix44<float> Matrix44f; 
alfC
  • 14,261
  • 4
  • 67
  • 118
  • Thank you very much for the explanation for a beginner and especially taking your precious time to write an answer! And especially thanks for writing it down to the website to illustrate the process. Didnt know about that tool. Certainly helpful for me in the future! It's really cool that C++ is so versatile, but with that I see that I still have a long way to go to master C++ :D – LeSchwambo Apr 28 '21 at 15:33