0

So I am trying to overload two operators for my "Matrix" class (+ and +=). I am attempting to make + chainable and += non-chainable:

template <class T>
Matrix<T>& Matrix<T>::operator+=(const Matrix& M) 
{
  if (this->m_capacity != M.capacity()) 
  {
    throw std::out_of_range("Input is invalid");
  }

  for (unsigned int i = 0; i < M.rows(); i++) 
  {
    for (unsigned int j = 0; j < M.cols(); j++) 
    {
      this->m_vec[i + m_cols * j] += M(i, j);
    }
  }
  return *this;
}


template <class T>
Matrix<T> operator+(Matrix<T> M1, Matrix<T>& M2) 
{
  if (M1.capacity() != M2.capacity()) 
  {
    throw std::out_of_range("Input is invalid");
  }

  return M1 += M2;
}

It compiles just fine, no issues there, but when I try to do a unit test on this, the entire test program just crashes when attempting to chain the + operator.

Example:

TEST(add, Matrix)
{
  Matrix<int> M1 = Matrix<int>(2, 3);
  Matrix<int> M2 = { 1, 2, 3, 4 };
  Matrix<int> M3 = M2;
  Matrix<int> M4 = { 2, 4, 6, 8 };

  ASSERT_THROW(M1 + M2, std::out_of_range);
  ASSERT_EQ((M2 + M3) == M4, true);

  M2 += M3;
  M2 = M4 + M4 + M4; // As soon as this line is added, it crashes, without it, test works fine

  ASSERT_EQ(M2 == M4, true);
}

Any ideas why it crashes? How can I rewrite my operator overloads so that ´+´ is chainable (and += isn't)?

EDIT: Here is my = operator (upon request)

template <class T>
void Matrix<T>::operator=(Matrix & M){
  T*temp = new T[M.m_capacity];
  for(unsigned int  j = 0; j < M.m_capacity; j++){
    temp[j] = M.m_vec[j];
  }
  delete[] this -> m_vec;
  size_t rows = M.get_m_rows();
  size_t cols = M.get_m_cols();
  this -> m_rows = rows;
  this -> m_cols = cols;
  this -> m_vec = new T [rows*cols];
  this -> m_capacity = rows*cols;
  for(size_t i = 0;i < rows;i++){
    for(size_t j = 0;j < cols;j++){
      this -> m_vec[i*cols+j] = temp[i*cols +j];
    }
  }
  delete [] temp;
}

EDIT2: Added more context (upon request, header, constructors etc.)

Header:

template <class T>
class Matrix {
public:
   // constructor
   Matrix(unsigned int n);
   Matrix(unsigned int n, unsigned int m);
   Matrix();
   Matrix(const T n);
   Matrix(Matrix &obj);
   ~Matrix();
   Matrix(Matrix &&obj);
   Matrix(std::initializer_list<T> l);

   // operators
   void operator=(Matrix & obj);
   T& operator()(unsigned int row, unsigned int col);
   Matrix& operator=( Matrix &&obj);
   Matrix& operator+=(const Matrix& M)
   void operator+=(const T number);
   void operator-=(const T number);
   void operator-=(Matrix &obj);
   void operator*=(const T number);
   void operator*=(Matrix &obj);
   bool operator==(Matrix & rhs);

private:
   std::size_t m_rows;
   std::size_t m_cols;
   std::size_t m_capacity;
   T * m_vec;
};

Copy constructor:

template <class T>
Matrix<T>::Matrix(Matrix &obj){
  size_t rows = obj.get_m_rows();
  size_t cols = obj.get_m_cols();
  this -> m_rows = rows;
  this -> m_cols = cols;
  this -> m_vec = new T [rows*cols];
  this -> m_capacity = rows*cols;
  for(size_t i = 0;i < rows;i++){
    for(size_t j = 0;j < cols;j++){
      this -> m_vec[i*cols+j] = obj(i,j);
    }
  }
}

Destructor:

template  <class T>
Matrix<T>::~Matrix(){
  delete [] m_vec;
}

Move constructor (possibly broken)

template <class T>
Matrix<T>::Matrix(Matrix &&obj){
  size_t rows = obj.get_m_rows();
  size_t cols = obj.get_m_cols();
  this -> m_rows = rows;
  this -> m_cols = cols;
  this -> m_vec = new T [rows*cols];
  this -> m_capacity = rows*cols;
  m_vec = nullptr;
}

Move assignment (possibly broken)

template <class T>
Matrix<T>& Matrix<T>::operator=(Matrix &&obj){
  if (this !=&obj)
  {
    delete [] m_vec;
    obj.m_rows = 0;
    obj.m_cols = 0;
    obj.m_capacity = 0;
    obj.m_vec = nullptr;
  }
  return *this;
  }
Schytheron
  • 715
  • 8
  • 28
  • 1
    Is your `operator =` correct? Please include it in the question. – Yksisarvinen Mar 01 '20 at 20:00
  • 2
    Please provide a complete [repro], at the very least the definitions of the special member functions of `Matrix`. Your copy/move constructor or copy/move assignment seems to be broken. Also `+=` is chainable in your example as well (and that is a good thing). – walnut Mar 01 '20 at 20:03
  • @Yksisarvinen Included it now :) – Schytheron Mar 01 '20 at 20:09
  • 1
    @Schytheron Copy assignment operators should *always* take their argument as `const` reference. (Btw. you probably also want `operator+` to take the second argument by `const` reference.) How do the copy constructor and the destructor look? Please include *at least* the definition of `Matrix` and the definitions of *all* the special member functions. Reduce your problem to a [repro]. – walnut Mar 01 '20 at 20:10
  • @Schytheron How about the copy constructor? That also needs to be user-defined and working. – PaulMcKenzie Mar 01 '20 at 20:16
  • @walnut Added it now. I hope it's enough :) – Schytheron Mar 01 '20 at 20:19
  • 1
    Your assignment operator could be written with only 4 lines of code that calls `std::swap`. Use the [copy / swap idiom](https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom). `Matrix t(obj); std::swap(t.m_rows, m_rows); std::swap(t.m_cols, m_cols); std::swap(t.m_capacity, m_capacity); std::swap(t.m_vec, m_vec); return *this;` – PaulMcKenzie Mar 01 '20 at 20:22
  • @PaulMcKenzie Hmm... interesting. I had no idea you could do it like that. On the other hand... I am still quite new to C++. – Schytheron Mar 01 '20 at 20:26
  • @Schytheron So according to the class definition you also have a move constructor and a move assignment operator. The definition of both of them are also relevant to the question. Also, as with the copy assignment, the copy constructor should *always* take a `const` argument. – walnut Mar 01 '20 at 20:26
  • The concept is simple -- you make a copy, swap out the guts of the copy with the guts of `*this`, copy then dies off with the old data. – PaulMcKenzie Mar 01 '20 at 20:26
  • @walnut I didn't include them because I think that they're broken. But Ill add it then anyway, because you asked. Gimme a minute. – Schytheron Mar 01 '20 at 20:29
  • @Schytheron Well, you are calling the move constructor and the move assignment operator in your example, so if they are broken, then so are your tests. – walnut Mar 01 '20 at 20:30
  • The move constructor and move assignment are both wrong – M.M Mar 01 '20 at 20:34
  • 1
    You could eliminate a whole class of errors by using `vector` for the storage instead of naked news – M.M Mar 01 '20 at 20:36
  • @Schytheron `std::vector m_vec;` -- Then the assignment op, copy constructor, move copy /assignment op, destructor, and `m_capacity` could all be eliminated. – PaulMcKenzie Mar 01 '20 at 20:38
  • @PaulMcKenzie bearing in mind that doing it that way, the moved-from object might have nonsense values , so I would probably add a move-constructor and move-assignment anyway. Although whether to support use-after-move can be a bigger topic than fits in comment discussion – M.M Mar 01 '20 at 20:48

2 Answers2

2

Both your move constructor and your move assignment operator do not implement the correct semantics (and it seems you are aware of that), leading to UB somewhere later. (I didn't bother checking exactly where.)

I guess you were assuming that you don't actually call these operators, but that is wrong.

You are calling the move assignment operator at the = sign of

M2 = M4 + M4 + M4;

because the right-hand side is a prvalue (operator+ returns by-value) which can bind to a rvalue reference.

(Before C++17) You are calling the (possibly elided) move constructor at the second + in the same line to construct the first parameter of operator+, because the first + results in a prvalue.

If you intend to implement the move operations later and you are fine with using the copy implementations instead for the time being, then don't declare the move operations in your class at all. Then the compiler will choose your copy implementations instead.

Additionally, the copy constructor and the copy assignment operator should always take a const (lvalue) reference as parameter, not a non-const reference.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • I fixed my move constructor and move assignment operator and everything seems to be working fine. Except for one problem. My `+=` operator is chainable and I don't want that. What's an easy way to make `+=` non-chainable while still keeping the `+` operator chainable? – Schytheron Mar 02 '20 at 13:19
  • 1
    @Schytheron I don't see any good reason to make `+=` non-chainable. It is very non-idiomatic and everyone using your class will be surprised by that behavior. But if you want to have it that way, you need to change the return type of `operator+=` to `void`. Of course you won't be able to use `return M1 += M2;` then, but instead you would need to write `M1 += M2; return M1;` and this redundancy is exactly why making `+=` chainable is a good idea. – walnut Mar 02 '20 at 13:22
-1

I’m not sure, but since your += operator is destructive, in that it doesn’t return a separate matrix but instead overwrites the previous one, adding m4 to itself may cause weird bugs as the “this” that is getting added and return is constantly in flux. There is no reason why the operator would be chainable as the compiler takes the chain into sequences of binary operations anyway, so the problem is most likely with your structure of the += overload. Also it would make more sense to add within the + overload and define x += x as x = x + x rather than the other way round.

FShrike
  • 323
  • 1
  • 10
  • 1
    No, that is not the case. `operator+` takes the first argument by-value (so a copy) and modifies only that. It then also returns a copy of that by-value. – walnut Mar 01 '20 at 20:32
  • Actually since you printed the definition as taking a const Matrix& it’s passing by reference! – FShrike Mar 01 '20 at 20:33
  • And for what it’s worth it’s returning by reference as well – FShrike Mar 01 '20 at 20:34
  • Sorry, I meant `operator+`, fixed in comment above. `operator+=` is supposed to modify itself. It doesn't modify its argument here either. If `this` and the argument are the same, care must be taken, but OP's implementation handles that case correctly. – walnut Mar 01 '20 at 20:34
  • 1
    It's recommended practice to define `+` in terms of `+=` as in this code. https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading – M.M Mar 01 '20 at 20:34
  • But when you’re adding M4+M4+M4, after the first addition M4 is no longer the same as what it used to be, since inside your addition logic you’re changing the operands’ data rather than the data of a newly created matrix. This means that the second addition isn’t (M4+M4) + M4 but (M4+M4) + some garbled matrix no longer the same as the original m4 – FShrike Mar 01 '20 at 20:38
  • 1
    @user12962917 As I mentioned in my (corrected) first comment,`operator+` is making a copy on which it calls `operator+=`. Therefore `M4` is never modified. – walnut Mar 01 '20 at 20:49
  • In the += logic, you say this->data[blah][blah] += stuff, (loosely speaking!) so this is being modified. M4+M4 will be modifying M4 permanently, regardless of how values are being passed or returned. – FShrike Mar 01 '20 at 20:55
  • 1
    @user12962917 That is simply not true. `operator+` is declared as `Matrix operator+(Matrix M1, Matrix& M2) `. The first parameter, corresponding to the left-hand side of `M4 + M4` is not a reference, so `M1` in the operator overload will be a *copy* of `M4`. Then the operator overload calls `M1 += M2`, where only `M2` refers to `M4`. But `operator+=` takes the second argument as `const` reference and doesn't modify it, so there is no modification of `M4` through `M2` and neither through `M1` which is a copy of it. – walnut Mar 01 '20 at 21:02