0

I have overloaded assignment operator for the class with a 2D array, but in order to do memory management and resizing correct I have to delete previous matrix first, then construct a new one, and only then I can start assigning.

Matrix& Matrix::operator = (const Matrix& m1){
    for (int i = 0; i < m_rows; ++i)
        delete[] m_matrix[i];
    delete[] m_matrix;

    m_matrix = new double*[m1.rows()];
    for (int i = 0; i < m1.rows(); ++i)
        m_matrix[i] = new double[m1.cols()]();

    for (int k = 0; k < m1.rows(); ++k)
        for (int j = 0; j < m1.cols(); ++j)
            m_matrix[k][j] = m1.m_matrix[k][j];

    m_rows = m1.rows();
    m_cols = m1.cols();

    return *this;
}

In fact, this part is destructor of my class:

for (int i = 0; i < m_rows; ++i)
    delete[] m_matrix[i];
delete[] m_matrix;

And this part is similar to a constructor:

m_matrix = new double*[m1.rows()];
for (int i = 0; i < m_rows; ++i)
    m_matrix[i] = new double[m1.cols()]();

What annoys me is that I have to copy constructors' and destructors' code in the assignment function (and some other functions too!) to make it work properly. Is there a better way to write it?

aaamourao
  • 128
  • 1
  • 9
  • 7
    Use `std::vector`, then no manual memory management. – Jarod42 Mar 15 '18 at 13:52
  • [Using the copy-and-swap idiom will make it easier to implement this operator](https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) – Kos Mar 15 '18 at 13:53
  • @Sergei Gorskiy Your operator is wrong because in case when the operands are the same object you will get undefined behavior. – Vlad from Moscow Mar 15 '18 at 14:00
  • 1
    Use one array to store the entire matrix. – Maxim Egorushkin Mar 15 '18 at 14:00
  • @Sergei Gorskiy As for your question then you can write two separate private functions that allocate and delete the memory and will be called from a constructor, the copy assignment operator and destructor. – Vlad from Moscow Mar 15 '18 at 14:04

2 Answers2

3

The ideal improvement would be Matrix& Matrix::operator=(const Matrix&) = default;.

If you switch to using std::vector for matrix storage you won't need to implement the copy/move constructors/assignments and destructor at all.

If what you are doing is a programming exercise, create your own dynamic array and use that in the implementation of your matrix.

I cannot recommend enough watching Better Code: Runtime Polymorphism by Sean Parent, he makes an effective demonstration of why you should strive to write classes that do not require non-default implementations of copy/move constructors/assignments and destructor.

Example:

template<class T>
class Matrix
{
    std::vector<T> storage_;
    unsigned cols_ = 0;

public:
    Matrix(unsigned rows, unsigned cols)
        : storage_(rows * cols)
        , cols_(cols)
    {}

    // Because of the user-defined constructor above
    // the default constructor must be provided.
    // The default implementation is sufficient.
    Matrix() = default;

    unsigned columns() const { return cols_; }
    unsigned rows() const { return storage_.size() / cols_; }

    // Using operator() for indexing because [] can only take one argument.
    T& operator()(unsigned row, unsigned col) { return storage_[row * cols_ + col]; }
    T const& operator()(unsigned row, unsigned col) const { return storage_[row * cols_ + col]; }

    // Canonical swap member function.
    void swap(Matrix& b) {
        using std::swap;
        swap(storage_, b.storage_);
        swap(cols_, b.cols_);
    }

    // Canonical swap function. Friend name injection.
    friend void swap(Matrix& a, Matrix& b) { a.swap(b); }

    // This is what the compiler does for you, 
    // not necessary to declare these at all.
    Matrix(Matrix const&) = default;
    Matrix(Matrix&&) = default;
    Matrix& operator=(Matrix const&) = default;
    Matrix& operator=(Matrix&&) = default;
    ~Matrix() = default;
};
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
1

The canonical implementation of the assignment operator leverages existing functionality (copy/move ctor, dtor, andswap(); note that using a non-specialized std::swap() would be bad). It looks like this:

T& T::operator= (T val) {
    val.swap(*this);
    return *this;
}

It nicely avoids reimplementing otherwise existing logic. It also deals gracefully with self-assignment which is a problem in your original code (it will do work but self-assignment is generally rather uncommon; optimizing it with a check against self-assignment typically pessimizes code).

The argument is passed by value to take advantage of copy elision.

The primary caveats with this approach are below. In general I prefer the canonical implementation as it is generally more correct and the outlined issue are often not really that relevant (e.g., when the object was just created anyway the transferred memory is actually "hot").

  1. It does not attempt to reuse already allocated and possibly "hot" memory. Instead it always uses new memory.
  2. If the amount of held data is huge, there are copies temporarily held which may exceed system limits. Reusing existing memory and/or release memory first would both address this issue.
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380