I am implementing a litte generic math library. What I have done is to write my generic matrix and vector class. I'm curious if I have it done right so far (implementation wise not totally mathematical correctness wise). Reason: I'm relatively new to template programming and I am not sure what is right, what could be done better and what should I try to avoid at all costs if I use templates in C++.

So far I managed to multiply different matrices of (obviously) different sizes while maintaining static allocation by using only std::array, so no dynamic memory allocation.

One thing is needed to be mentioned: my Vectors and Matrices start count their elements with #1 not #0 like it is usually done in C++ with arrays. The reason for this is to be near as possible to 'proper' math, but I am open to get convinced otherwise.

First of all, my generic Vector class is quite boring but needed for my matrix class, so I put it inside this post.

The most complicated part of my code so far. I have to calculate the size of my new matrix at compile time so that I am able to keep my goal to avoid dynamic allocation. The syntax of template templates is quite odd to me but it does work.

With the help of an online compiler I already found out that VS is not particularly great at compiling this code example compared to gcc and clang. What totally surprised me was that clang was so good at optimizing my code that in the and all that was left were a single return statement with value 14!

Clang did all the computation of multiplying two different sized matrices at compile time. I am flabbergasted about that. It is also the reason why I posted this code here. I want to know from you what I did well and what could be improved. I'm not really sure how I did that on the first try without even thinking of active optimization from my side.

2 Answers
2

Some Points

So far I managed to multiply different matrices of (obviously) different sizes while maintaining static allocation by using only std::array, so no dynamic memory allocation.

Sure. If this is something you really want.

One thing is needed to be mentioned: my Vectors and Matrices start count their elements with #1 not #0 like it is usually done in C++ with arrays. The reason for this is to be near as possible to 'proper' math, but I am open to get convinced otherwise.

That depends entirely on the user base who will be using this class. If they are mathematicians who are used to 1 based array then fine. But think that most people who use C like languages are already used to using 0 based arrays so have this may confuse people.

Some Thoughts

A lot of Matrix libraries actually delay the multiplication (and other operations) until the value inside the matrix is required. That way you don't pay for operations that you don't need.

Also by deferring operations you can potentially eliminate null operations or simplify operations that are cumulative.

Example:

{
Matrix<4,5> x(init);
Matrix<4,5> y(init);
Matrix<4,5> z = x + y; // Here the + operator
// Can loop over all the elements
// and do the operation for each element.
std::cout << z[1][1] << "\n"; // Here we use only one value
} // Then z goes out of scope and is destroyed.
// So we just did a bunch of operations
// that are not needed.
// If at the point where we did the operation + we returned an
// object that knows about x and y but did not immediately do the operation
// Then we accesses element [1][1] we see the work has not been done
// and just do the operation for that location. We don't work out all
// the elements just the one we want and only when we need it.
// That is a deferred operation.

In this example we can see that all elements of e will be zero. So calculating the value of c first is a waste of time. By deferring calculations of the elements we can sometimes determine the result without having to do all the expensive operations.

So at run-time you can perform operations that cancel out other operations and thus you do not need to perform expensive operations if there results do not generate a value that effects the result.

Code Review

If your Value (or NumberType) is always a simple POD then this is fine.

In C++ normally the operator[] is unchecked to allow for optimal speed (you don't need to check the ranges in the function if you have already checked it outside the function) while the method at() does do a range check (because you have not done the check externally).

The second thing to think about is returning by value. Usually when you have a container you return accesses to the element by reference. Thus allowing you to modify the value intuitively. This also makes writting operator+= simpler (then operator+ can be written in terms of operator+=.

The third thing is that throw; (without an expression is wrong). This is used to re-throw a currently propogating exception from within a catch clause. There is no catch here so this results in a call to std::terminate

\$\begingroup\$About the perfect forwarding, you are absolutely right about that. What do you mean by delaying operation? Should store them in terms of commands and then later on execute them on demand? And my second question : I thought if returned by value the move ctor would kick in, am I wrong? If yes I agree returning by reference is the better choice.\$\endgroup\$
– ExOfDeJun 26 '17 at 5:19

\$\begingroup\$A returned value can be an R-Value reference and thus allow move semantics to kick in. Which is useful for you Row() and Col() methods. But does not really apply to operator[]. What you want to do is return a reference to the Value which is an int/double etc so that you can update it in place. If you return by value you can't update in place.\$\endgroup\$
– Martin YorkJun 26 '17 at 5:49

\$\begingroup\$The operators += and + should also have plain copy versions. If move semantics are implemented properly then the compiler can decide, if he wants to copy or move. Also the operator+= cannot be const qualified, as it changes the container\$\endgroup\$
– misccoJun 26 '17 at 6:10

\$\begingroup\$According to the recommendations given here and here, the operator+() should probably be implemented as a non-member function.\$\endgroup\$
– moooeeeepJun 26 '17 at 8:42

Consider utilizing the empty base class optimization

This will be the hardest to implement as it requires careful refactoring of your class. A typical container is implemented like this:

It has an allocator member

It has begin, end and capacity pointer members

Because this bloats the container, it's possible to utilize the EBCO to make the size of these members just a single pointer by either:

Encapsulating it in a base class that has these as members, then have the derived class inherit it and use it to access the members; typically called something like "impl"

Wrap it in a boost::compressed_pair which requires less legwork

I'm unsure how useful this will be since you are using an std::array for storage; however, consider this. The size of Matrix is proportional to its geometry (Matrix<3, 3> is 72 bytes on a 64-bit system) even though it is a non-owning view. So further refactoring leads to these possible routes:

Give Matrix a pointer to the storage owned by someone else (i.e Vector) and a capacity; have Matrix perform all of its operations in-place (rather than using temporary variables)

Remove implementation details from Matrix that it doesn't "know" about. You hard-code the storage type, but could use using Storage = Vector<...>::Storage

Consider an STL compatible interface

This one is a no-brainer and just requires refactoring your typedef's and interface to look similar to an STL container. Using value_type, reference etc. Make the Storage typedef private since it is an implementation-detail.

Having a constructor that takes an initializer_list might be useful too.

Throw std::out_of_range

This is a freebie. You throw inside at, might as well make it a logical exception type.

\$\begingroup\$What are you talking about. I can not make heads or tails of what you are trying to recommend. The EBCO does nothing for you in any way (its a compiler optimization).\$\endgroup\$
– Martin YorkJun 26 '17 at 2:21

\$\begingroup\$I agree about my throw statement... It was just quick hack which needs improvement or better replacement. Future implementation will probably have the in-place operation but again just the quickest solution. Thanks a lot for your input I will work through it later the day\$\endgroup\$
– ExOfDeJun 26 '17 at 5:33