-1

I am building a template matrix class for use in my future c++ code. I have a few questions regarding value passing for overloaded operators, exceptions vs asserts, and general class design.

  1. Am I passing the values correctly? Is it efficient? What can I do otherwise to make it better?

  2. This library is built with future application design in mind (terminal or gui), where a user could define their own matrices and run calculations. Would using exceptions instead of asserts be better in this case?

  3. I have looked up the rule of 5 for c++, where it states that:

    Because the presence of a user-defined destructor, copy- constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions.

    Can I get away with not implementing this rule by just not having any of those three?** What would be the standard way to make this class more functional?

I have subtraction, multiplication, and division (scalar) defined in my program with the same/similar structure as the provided addition operator definitions, so not all of that code is necessary here.

Any hard advice or criticism on the overall design is accepted!

#ifndef MACMATRICES_H
#define MACMATRICES_H

#include <vector>
#include <iomanip>
#include <iostream>
#include <exception>
#include "../../DMF-Terminal.h"


namespace DMF
{
    template <typename T>
    class matrix
    {
    public:

    // Constructors 
    matrix();
    matrix(int p_rows, int p_columns);

    // Operators
    std::vector<T>& operator[] (size_t i) { return m[i]; }
    matrix<T> operator+(const matrix<T>& rhs);
    matrix<T> operator+(const T& rhs);
    matrix<T>& operator+=(const matrix<T>& rhs);
    matrix<T>& operator+=(const T& rhs);


    // Class Methods 
    void print() const;
    matrix<T> inverse();
    T determinant();

    // Observers 
    bool isSquare() const;
    int rowSize() const { return m_rows; } 
    int colSize() const { return m_cols; } 

private:

    int m_rows, m_cols;
    std::vector< std::vector<T> > m;
};

/* Constructors -----------------------------------------------------------------------------------*/

template <typename T>
matrix<T>::matrix(){}

template <typename T>
matrix<T>::matrix(int p_rows, int p_cols) :
    m(p_rows, std::vector<T>(p_cols)), m_rows(p_rows), m_cols(p_cols) {}

/* Addition ---------------------------------------------------------------------------------------*/

template <typename T>
matrix<T> matrix<T>::operator+(const matrix<T>& rhs)
{
    try
    {
        if((this->rowSize() == rhs.rowSize()) && (this->colSize() == rhs.colSize()))
        {
            matrix<T> sum (this->rowSize(), this->colSize()); 
            for(int i = 0; i < this->rowSize() ; ++i)
            {
                for(int j = 0; j < this->colSize(); ++j)
                    sum.m[i][j] = this->m[i][j] + rhs.m[i][j];
            }
            return sum; 
        }
        else throw std::runtime_error("Cannot add matrices, invalid row/column sizes."); 
    }
    catch (std::exception &e)
    {
        std::cout << "Error: " << e.what(); DMF::wait();
    }
}

template <typename T>
matrix<T> matrix<T>::operator+(const T& rhs)
{
    matrix<T> sum (this->rowSize(), this->colSize()); 
    for(int i = 0; i < this->rowSize() ; ++i)
    {
        for(int j = 0; j < this->colSize(); ++j)
            sum.m[i][j] = this->m[i][j] + rhs;
    }
    return sum; 
}

template <typename T>
matrix<T>& matrix<T>::operator+=(const matrix<T>& rhs)
{
    try
    {
        if((this->rowSize() == rhs.rowSize()) && (this->colSize() == rhs.colSize()))
        {
            for(int i = 0; i < this->rowSize() ; ++i)
            {
                for(int j = 0; j < this->colSize(); ++j)
                    this->m[i][j] += rhs.m[i][j];
            }
            return *this; 
        }
        else throw std::runtime_error("Cannot add matrices, invalid row/column sizes."); 
    }
    catch (std::exception &e)
    {
        std::cout << "Error: " << e.what(); DMF::wait();
    }
}

template <typename T>
matrix<T>& matrix<T>::operator+=(const T& rhs)
{
    matrix<T> sum (this->rowSize(), this->colSize()); 
    for(int i = 0; i < this->rowSize() ; ++i)
    {
        for(int j = 0; j < this->colSize(); ++j)
            this->m[i][j] += rhs;
    }
    return *this; 
}
}
#endif /* MACMATRICES_H */

As of right now, this code works within a mini terminal program. I also have matrix * matrix and matrix *= matrix operators overloaded and it seems to be working correctly, with the result matrix size being correct.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
dmf254
  • 1
  • 2
  • That makes perfect sense. I just threw those in at the last second. Thank you! – dmf254 Jan 03 '19 at 21:42
  • 1
    For performance, don't use a vector of vectors. Use a single block of memory with member functions to access it. And write it as a non-templated class of `int` before you try to turn it into a template. –  Jan 03 '19 at 21:43
  • You forgot to initialise all your members in the default constructor. Does it even make sense for an empty matrix to exist, though? – molbdnilo Jan 03 '19 at 21:44
  • @molbdnilo The default constructor was used for testing of assignment for my multiplication operators. I'm guessing I wouldn't really need it, however I would like to implement a constructor using list initialization like you can with std::vector. What would be the easiest way to do that? – dmf254 Jan 03 '19 at 21:51
  • @NeilButterworth I was hoping that I could make use of the std::vector member functions further within my code. Are you meaning that I should have the matrix as a dynamically allocated 2d array? – dmf254 Jan 03 '19 at 21:52
  • No, I mean it should be a dynamically allocated 1D array - you could in the first instance make it a vector. –  Jan 03 '19 at 21:54
  • @NeilButterworth Hmm, so a 1D array that holds all the elements, and split it into rows/collumns using member methods? Im sorry, I’m a little confused. – dmf254 Jan 03 '19 at 22:34
  • Yep, that's right. Doing it like that is far more cache friendly. –  Jan 03 '19 at 22:42
  • @NeilButterworth I like it, So I would have private members stating the amount of rows and collums, how would I edit the [][] operator accordingly? Do you have any comments on how I’m passing values for my operators? – dmf254 Jan 03 '19 at 22:57
  • [Example code](https://stackoverflow.com/a/53952344/2684539) to handle "`[][]`" with 1D array. – Jarod42 Jan 03 '19 at 23:17
  • @Jarod42 Thank you, that thread was great to read! – dmf254 Jan 04 '19 at 04:44

1 Answers1

0

Am I passing the values correctly? Is it efficient? What can I do otherwise to make it better?

You pass your matrix by (const) reference, so you avoid copy, so it is fine.

You have some "typo" as variable used as matrix<T> sum in operator+=.

You duplicate some information, std::vector knows its size.

linearise std::vector<std::vector<T>> into std::vector<T>> would be more cache friendly, but requires then a proxy class (with another operator[]) to handle operator[], or you might use instead an accessor as operator()(int, int) or operator[](std::pair<int, int>).

This library is built with future application design in mind (terminal or gui), where a user could define their own matrices and run calculations. Would using exceptions instead of asserts be better in this case?

Exceptions are to communicate the error to the user, but currently, you catch it directly to ignore the error with some log... So instead of throwing, you could directly log the error currently.

There are several to fix that issue:

  • Have matrix size in template argument and use type system to check those error at compilation. require to know size at compilation though.

  • If you consider that user might be able to ignore/recover from the error, then let's propagate exception. You might probably have dedicated exceptions.

  • If you consider that user might not be able to ignore/recover from the error, then assert/terminate/UB are possible way.

I have looked up the rule of 5 for c++, where it states that: "Because the presence of a user-defined destructor, copy- constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions." Can I get away with not implementing this rule by just not having any of those three? What would be the standard way to make this class more functional?

The rule of 3 has variants:

  • rule of 5 to also include move constructor and move assignation.
  • rule of 0, where all member are already RAII friendly and so default implementation is fine.

using std::vector allows to use rule of 0 :-) So your are fine.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Thank you so much for your reply! I will be editing my overall class design in the following days. Would you be so kind to take a quick look again once I clean it up? – dmf254 Jan 05 '19 at 19:48