3
\$\begingroup\$

I have been getting back into C++ today, after a few years of Python and R. I am completely rusty, but decided to create a matrix object to re-familiarise. In reality I wouldn't use this class, since I know Boost has matrix objects, but it's good practice!

It can be compiled with g++ -std=c++11 matrix.cpp -o m.

Any thoughts or comments are most welcome.

#include <iostream>
#include <vector>

class matrix {
    // Class: matrix
    //
    // Definition: 
    // creates a 2d - matrix object as a vector of vectors and 
    // populates with (int) zeros.
    //
    // Methods: get_value, assign_value, get_row.

    // create vector of vectors
    std::vector<std::vector<int> > m;
    public:
        // constructor for class, matrix dimension (rows=X, cols=Y).
        matrix( int X, int Y) {
        m.resize(X, std::vector<int>(Y, 0));
    }

    class row {
        //class for matrix row object. Pass in a 
        // vector and overload `[]`. 

        std::vector<int> _row;
        public:
            // constructor
            row(std::vector<int> r) : _row(r) {
            }

            // overload `[]` to return y element.
            // note `.at()` does a range check and will throw an error 
            // if out of range
            int operator[]( int y) {
                return _row.at(y);
            }
    };

    // overload [] to return x element
    row operator[]( int x) {
        return row(m.at(x));
    }

    int get_value ( int x, int y ) {
        //  Function: get_value
        //  Definition: returns value `v` of element 
        //  `xy` in matrix `M`. 
        return m[x][y];
    }

    void assign_value ( int x, int y, int v ) {
        //  Function: assign_value 
        //  Definition: Assigns value `v` to element 
        //  `xy` in matrix. 
        m[x][y] = v;
    }

    std::vector<int> get_row(int y, int X){
        // Function get_row
        // Definition: returns a vector object with row 
        // of x-values of length X at y.
        std::vector<int> ROW;
        for ( int i=y; i<=y;i++){
            for (int j=0; j<=X-1;j++){
                ROW.push_back(m[i][j]);
            }
        }
        return ROW;
    }


};



int main(){

    // specify matrix dimensions
    int N = 10; // number of rows
    int M = 10; // number of cols

    // create a matrix object 
    matrix mm(N,M);

    // print elements
    int i, j;
    for (i=0; i<=N-1;i++){
        for (j=0;j<=M-1; j++){
            std::cout << mm[i][j];
        }
        std::cout << std::endl;
    }

    // grab a value and print it to console
    std::cout << mm.get_value(0,0) << std::endl;

    // assign a new value (v = 1) to element (0,0)
    mm.assign_value(0,0,1); 

    // re-print the updated matrix
    for (i=0; i<=N-1;i++){
        for (j=0;j<=M-1; j++){
            std::cout << mm[i][j];
        }
        std::cout << std::endl;
    }


    // `get_row()` test
    std::vector<int> R = mm.get_row(0, M);
    for( int i: R){
        std::cout << i << ' ';
    }
}
\$\endgroup\$
  • \$\begingroup\$ As well as Boost, it may be instructive to look at OpenCV for inspiration. Although it's only a thin C++ wrapper over a C implementation, it does lean heavily on matrix operations, and could provide additional insight into which features are valuable to users. \$\endgroup\$ – Toby Speight Sep 18 '18 at 10:39
5
\$\begingroup\$

That's some nicely presented code. I found it very easy to read and understand.

A vector of rows isn't the best structure for a matrix. The reason is that each vector has its storage elsewhere, so you lose locality of access. A better structure is a flat array (or vector) of elements, and a knowledge of the stride from one row to the next. (we can make the stride be the same as the row length, for simplicity; separate members for width and stride can be useful in more advanced scenarios).

    std::vector<int> m;
    std::size_t width;

public:
    # Constraint: x * y must not overflow size_t
    matrix(std::size_t x, std::size_t y)
        : m(x*y, 0),
          width{x}
    {
    }

I've made the dimensions be size_t, as that's the natural type for a size or count in C++.

Now, when we need to index into the array, we need to multiply the y value by width and add x:

int get_value(std::size_t x, std::size_t y)
{
    return m[x + y*width];
}

We can improve on this, by returning a reference to the value. Instead of having a "get" and "set" method, we have a single method (for now), and we can give it a more convenient name:

int& operator()(std::size_t x, std::size_t y)
{
    return m[x + y*width];
}

This means that instead of having to write

mm.assign_value(0,0,1); 

we can instead use the more intuitive

mm(0,0) = 1;

Now it's time admit to a slight lie above. We actually need two methods, because if we have a const matrix, we should be allowed to read, but not write, its elements. So we also need:

const int& operator()(std::size_t x, std::size_t y) const
{
    return m[x + y*width];
}

For printing, it's helpful to provide an operator<<(). Mine would look like this:

friend auto& operator<<(std::ostream& os, const matrix& m)
{
    for (std::size_t row = 0;  row < m.height;  ++row) {
        for (std::size_t col = 0;  col < m.width;  ++col) {
            os << m.m[col + row*m.width] << ' ';
        }
        os << '\n';
    }
    return os;
}

I added a height member to make this easier.


With these changes, see how much easier it is to use:

#include <iostream>

int main()
{
    // create a matrix object
    matrix mm(4,6);

    // print elements
    std::cout << mm;

    // grab a value and print it to console
    std::cout << mm(0,0) << std::endl;

    // assign a new value (v = 1) to element (0,0)
    mm(0,0) = 1;

    // re-print the updated matrix
    std::cout << mm;
}

Here's the full version of matrix after my edits:

#include <ostream>
#include <vector>

class matrix {
    std::vector<int> m;
    std::size_t width;
    std::size_t height;

public:
    matrix(std::size_t x, std::size_t y)
        : m(x*y, 0),
          width{x},
          height{y}
    {
    }

    int& operator()(std::size_t x, std::size_t y)
    {
        return m[x + y*width];
    }

    const int& operator()(std::size_t x, std::size_t y) const
    {
        return m[x + y*width];
    }

    friend auto& operator<<(std::ostream& os, const matrix& m)
    {
        for (std::size_t row = 0;  row < m.height;  ++row) {
            for (std::size_t col = 0;  col < m.width;  ++col) {
                os << m.m[col + row*m.width] << ' ';
            }
            os << '\n';
        }
        return os;
    }
};

Further exercises

  • If you actually want a public get_row() (and get_column()), these will need new implementations, perhaps copying values.
  • Think about providing a get_subarray(x, y, width, height) to give a view of part of the matrix - you'll need new members for offset and stride. See how we can now more easily implement get_row() and get_column() using this new method.
  • Make the matrix a template, so we can have elements of whatever type we choose, rather than only int.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.