Search code examples
c++arraysoopassignassignment-operator

Does the = operator call the constructor/new in C++?


Say I have a (immutable) matrix class that dynamically creates an array in the constructor, and deletes it in the deconstructor.

template <typename T>
class matrix {
private:
    T* data;
public:
    size_t const rows, cols;
    matrix(size_t rows, size_t cols) : rows(rows), cols(cols) {
        data = new T[rows*cols];
    }
    ~matrix() {
        delete [] data;
    }
    //access data
    T& operator()(size_t row, size_t col) {
        return data[row*cols + col];
    }
    matrix<T>& operator=(const matrix<T>& other) {
        //what will this->data contain? do I need to delete anything here?
        //should I call the constructor?
        rows = other.rows;
        cols = other.cols;
        data = new T[rows*cols];
        std::copy(&data[0],&data[0] + (sizeof(T)*rows*cols),&other.data[0]);
        return *this;
    }
}

Because I don't have a default constructor within the operator= function, the data in this is just garbage, right? Even if I had a default constructor would it be called? I'm basing the above code off of the example here. Note that I have left out input validation/bounds checking for brevity.

EDIT: I would like to clarify that I'm only concerned for a call like this:

matrix<int> A = B;

Where B has already been initialized.


Solution

  • Say I have a (immutable) matrix class that dynamically creates an array in the constructor, and deletes it in the deconstructor.

    You are violating the Rule of Three by not implementing a copy constructor (and the Rule of Five in C++11 by not implementing a move constructor and a move assignment operator).

    Your copy assignment operator has a memory leak, as it is not delete[]'ing the old data array before assigning the new[]'ed array.

    Try this instead:

    template <typename T>
    class matrix {
    private:
        T* data;
        size_t const rows, cols;
    
    public:
        matrix(size_t rows, size_t cols) : rows(rows), cols(cols) {
            data = new T[rows*cols];
        }
    
        matrix(const matrix<T> &src) : rows(src.rows), cols(src.cols) {
            data = new T[rows*cols];
            std::copy(data, &data[rows*cols], src.data);
        }
    
        /* for C++11:
    
        matrix(matrix<T> &&src) : rows(0), cols(0), data(nullptr) {
            std::swap(rows, src.rows);
            std::swap(cols, src.cols);
            std::swap(data, src.data);
        }
        */
    
        ~matrix() {
            delete [] data;
        }
    
        T& operator()(size_t row, size_t col) {
            return data[(row * cols) + col];
        }
    
        T operator()(size_t row, size_t col) const {
            return data[(row * cols) + col];
        }
    
        matrix<T>& operator=(const matrix<T>& other) {
            if (&other != this) {
                delete[] data;
                rows = other.rows;
                cols = other.cols;
                data = new T[rows*cols];
                std::copy(data, &data[rows*cols], other.data);
            }
            return *this;
        }
    
        /* for C++11:
    
        matrix<T>& operator=(matrix<T> &&other) {
            delete[] data;
            data = nullptr;
            rows = cols = 0;
    
            std::swap(rows, other.rows);
            std::swap(cols, other.cols);
            std::swap(data, other.data);
    
            return *this;
        }
        */
    };
    

    However, the copy-and-swap idiom would be safer:

    template <typename T>
    class matrix {
    private:
        T* data;
        size_t const rows, cols;
    
    public:
        matrix(size_t rows, size_t cols) : rows(rows), cols(cols) {
            data = new T[rows*cols];
        }
    
        matrix(const matrix<T> &src) : rows(src.rows), cols(src.cols) {
            data = new T[rows*cols];
            std::copy(data, &data[rows*cols], src.data);
        }
    
        /* for C++11:
    
        matrix(matrix<T> &&src) : rows(0), cols(0), data(nullptr) {
            src.swap(*this);
        }
        */
    
        ~matrix() {
            delete [] data;
        }
    
        T& operator()(size_t row, size_t col) {
            return data[(row * cols) + col];
        }
    
        T operator()(size_t row, size_t col) const {
            return data[(row * cols) + col];
        }
    
        void swap(matrix<T>& other) noexcept
        {
            std::swap(rows, other.rows);
            std::swap(cols, other.cols);
            std::swap(data, other.data);
        }
    
        matrix<T>& operator=(const matrix<T>& other) {
            if (&other != this) {
                matrix<T>(other).swap(*this);
            }
            return *this;
        }
    
        /* for C++11:
    
        matrix<T>& operator=(matrix<T> &&other) {
            other.swap(*this);
            return *this;
        }
        */
    };
    

    In the latter case, the copy assignment and move assignment operators can be merged together into a single operator in C++11:

    matrix<T>& operator=(matrix<T> other) {
        other.swap(*this);
        return *this;
    }
    

    Or, you could just follow the Rule of Zero by using std::vector and let the compiler and STL do all of the work for you:

    template <typename T>
    class matrix {
    private:
        std::vector<T> data;
        size_t const rows, cols;
    
    public:
        matrix(size_t rows, size_t cols) : rows(rows), cols(cols), data(rows*cols) {
        }
    
        T& operator()(size_t row, size_t col) {
            return data[(row * cols) + col];
        }
    
        T operator()(size_t row, size_t col) const {
            return data[(row * cols) + col];
        }
    };
    

    Because I don't have a default constructor within the operator= function, the data in this is just garbage, right?

    No, because operator= can only be called on a previously constructed object, just like any other class instance method.

    Even if I had a default constructor would it be called?

    In the example you showed, no.

    I would like to clarify that I'm only concerned for a call like this:

    matrix<int> A = B;
    

    That statement does not call operator= at all. The use of = is just syntax sugar, the compiler actually performs copy construction as if you had written this instead:

    matrix<int> A(B);
    

    Which requires a copy constructor, which you have not implemented, and the compiler-generated copy constructor is not sufficient to make a deep copy of your array.

    Copy assignment would look more like this instead:

    matrix<int> A; // <-- default construction
    A = B; // <-- copy assignment
    

    matrix<int> A(B); // <-- copy construction
    A = C; // <-- copy assignment