Skip to Content
Course content

208: Practice Exercise: Building a Simple Matrix Class with Operator Overloading

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this mistake more times than I can count, even from developers who think they've mastered C++ memory management. When you start overloading operators for your own classes, it's tempting to try and "optimize" by returning references to avoid copies. But there's a very specific place where that logic will blow up in your face.

// BUGGY CODE: Don't do this!
class Matrix {
    int rows, cols;
    double** data;
    // ... constructor and destructor implemented ...

    Matrix& operator+(const Matrix& other) {
        Matrix result(this->rows, this->cols);
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                result.data[i][j] = this->data[i][j] + other.data[i][j];
            }
        }
        return result; // CRASH IMMINENT
    }
};

The Dangling Reference Trap

Take a close look at that operator+ signature. It returns a Matrix&. Inside the function, we create a local object called result. This object lives on the stack. The moment the function hits that closing brace, result is destroyed. By returning a reference to it, you're handing the caller a pointer to a memory location that no longer holds a valid object. If you try to use the result of A + B, your program will likely segfault or, worse, give you garbage data that makes you question your sanity for three hours.

Returning by Value for New Objects

The fix is simple: return by value. When you use operator+, you aren't modifying the existing matrices; you're creating a brand new one. Therefore, the return type must be Matrix, not Matrix&.

// FIXED CODE
Matrix operator+(const Matrix& other) const {
    Matrix result(this->rows, this->cols);
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            result.data[i][j] = this->data[i][j] + other.data[i][j];
        }
    }
    return result; // The compiler uses RVO (Return Value Optimization) to make this efficient
}

I also added const to the end of the member function. This is crucial. It tells the compiler that adding two matrices doesn't change the original matrices. Without this, you won't be able to add two matrices if one of them is passed as a const reference or is a temporary object.

Making Operators Feel Native

If we want our Matrix class to feel like a first-class citizen in C++, we shouldn't rely on clunky methods like matrix.getValue(1, 2). Instead, we overload operator(). While operator[] only takes one argument, operator() allows us to pass both the row and column index, making the syntax feel much more like MATLAB or Python's NumPy.

Here is how I usually structure a lean, functional Matrix class for practice exercises:

#include <iostream>
#include <vector>

class Matrix {
    int rows, cols;
    std::vector<std::vector<double>> data;

public:
    Matrix(int r, int c) : rows(r), cols(c), data(r, std::vector<double>(c, 0.0)) {}

    // Overloading () for easy access: mat(r, c)
    double& operator()(int r, int c) { return data[r][c]; }
    double operator()(int r, int c) const { return data[r][c]; }

    Matrix operator+(const Matrix& other) const {
        Matrix res(rows, cols);
        for(int i=0; i<rows; ++i)
            for(int j=0; j<cols; ++j)
                res(i, j) = (*this)(i, j) + other(i, j);
        return res;
    }

    // Friend function for stream output
    friend std::ostream& operator<<(std::ostream& os, const Matrix& m) {
        for(int i=0; i<m.rows; ++i) {
            for(int j=0; j<m.cols; ++j) os << m(i, j) << " ";
            os << "\n";
        }
        return os;
    }
};

Notice that I used std::vector here instead of raw pointers. In a real-world scenario, managing double** manually is a recipe for memory leaks. By using vectors, we let RAII handle the cleanup, allowing us to focus on the operator overloading logic itself.




📋 Practical Task

Exercise: Implementing Matrix-Scalar Multiplication and Chaining

Your task is to extend the Matrix class provided in the lesson to support scalar multiplication. This will test your ability to handle different operator signatures and ensure that operations can be chained together (e.g., Matrix result = (A + B) * 2.5;).

  • Requirement 1: Overload operator* as a member function to allow a Matrix to be multiplied by a double (e.g., mat * 2.0).
  • Requirement 2: Overload operator* as a non-member function to allow a double to be multiplied by a Matrix (e.g., 2.0 * mat). This is essential because the left-hand operand is not a Matrix object.
  • Requirement 3: Implement a main function that creates two 2x2 matrices, adds them together, multiplies the result by a scalar, and prints the final matrix using std::cout.

Verification: Ensure your scalar multiplication returns a new Matrix object by value to avoid the dangling reference issue discussed in the lesson.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.