1

I'm creating a class called "matrix" that has a class member of std::vector<std::vector<T>>, what is the best optimized way to set its size in the class constructor? I'm using matrix_.resize(m) in the constructor for m rows and then using a for loop to set its row values(matrix m x n). The question I have is:

  1. Is it a good approach to use .resize()? I heard it is not efficient but I can't think of anything else

  2. When I use std::vector<std::vector<T>> = matrix_;, for example and I use matrix_.resize(m), the matrix_ now will have "m" spaces for "m" std::vector<T> objects, but how matrix_ will manage the memory if it doesn't know the size of its objects(std::vector<T>)? What if each objects are size(1000) or larger?

#include <iostream>
#include <vector>

// Matrix of shape m x n, 'm' lines and 'n' columns
template <typename T>
class matrix {
private:
    typedef T value_type;
    typedef std::vector<value_type> row;
    std::vector<row> matrix_;
    
public:
    // Default constructor, initializes a matrix of size 1x1, with element 0.
    matrix() {
        matrix_.resize(1);
        row temp = {0};
        matrix_[0] = temp;
    };
    matrix(size_t m, size_t n) {
        matrix_.resize(m);
        // Appends 'm' rows to the matrix_
        for (size_t i = 0; i < m; i++) {
            row temp(n);    // Creates a row of 'n' columns
            matrix_[i] = temp;
        }
    }
    value_type& at (size_t m, size_t n) {
        return matrix_[m][n];
    }
};

int main() {
    matrix<double> first(2, 2);
    first.at(0, 0) = 3.3;
    first.at(0, 1) = 5.2;
    first.at(1, 0) = 8.9;
    first.at(1, 1) = 1.4;

    return 0;
}
  • 4
    Don't use a 2d vector. Use a 1d vector of size N x M and then use math to translate `m` and `n` into a single index to it like done here: https://stackoverflow.com/questions/2151084/map-a-2d-array-onto-a-1d-array – NathanOliver Apr 01 '21 at 16:40
  • 1
    1. you don't need `resize` as you can do it in constructor of `vector`, but it do no much harm. – apple apple Apr 01 '21 at 16:43
  • 2. simply as a 1D `vector`? I don't really get your concern here. – apple apple Apr 01 '21 at 16:43
  • Yeah, the top-level vector is responsible for managing `m` other vectors. Those `m` vectors are each perfectly capable of taking care of their own `n` scalar elements. But you should use a flat n*m vector anyway. – Useless Apr 01 '21 at 16:53
  • fwiw, `sizeof( std::vector)` is a constant, no matter how many elements the vector contains. The elements are not stored within the vector, but they are dynamically allocated – 463035818_is_not_an_ai Apr 01 '21 at 17:20
  • `value_type& at (size_t m, size_t n)` -- The `at()` suggests a runtime check of the boundaries, similar to `vector::at()`. But your function does no runtime check. Maybe you should simply have an overloaded `operator()` that doesn't do the check, and let `at()` do bounds checking. – PaulMcKenzie Apr 01 '21 at 17:52
  • Could you please explain what is a "runtime check"? I'm not sure what it means – Lucas Saito Apr 01 '21 at 18:20

1 Answers1

3

The best way to do this is to not. vectors of vectors are a bad idea, especially at small sizes, because you want your memory to be contiguous.

template <typename T>
class matrix {
public:
  typedef T value_type; // expose this
private:
  std::size_t m_size = 0;
  std::size_t n_size = 0
  std::vector<T> data;
public:
  // Default constructor, initializes a matrix of size 0
  matrix():matrix(0,0) {};
  // square matrix
  explicit matrix(std::size_t n):matrix(n,n) {};
  matrix(std::size_t m, std::size_t n):
    m_size(m), n_size(n),
    data(m*n, 0)
  {}
  value_type& operator()(size_t m, size_t n) {
    return data[m*n_size + n];
  }
  value_type const& operator()(size_t m, size_t n) const {
    return data[m*n_size + n];
  }
};

add in array_view or span:

template<class It>
struct range {
  It b, e;
  It begin()const{return b;}
  It end()const{return e;}
  std::size_t size()const{ return end()-begin(); }
  bool empty()const{ return begin()==end(); }
};
template<class T>
struct array_view:range<T*> {
  T& operator[](std::size_t i)const{
    return this->begin()[i];
  }
  array_view(T* start, T* finish):range<T*>{ start, finish }{}
  array_view():array_view(nullptr, nullptr){}
  array_view(T* start, std::size_t length):array_view( start, start+length ) {}
};

with that we can add

   array_view<T> operator[](std::size_t m) {
     return {std::addressof(data[m*n_size]), n_size};
   }
   array_view<T const> operator[](std::size_t m) const {
     return {std::addressof(data[m*n_size]), n_size};
   }

and you now get matrix[a][b] syntax that is highly efficient. (addressof is just me being paranoid).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I've learned something today with you, thanks! I didn't know that I could create a default constructor with a non-default constructor. But why the `:` on `matrix():matrix(0,0) {};`? Is just the syntax or would it be possible to use `=` ? And what does `std::addressof` actually do? Thanks! – Lucas Saito Apr 01 '21 at 21:01
  • @oucal those are member initislize lists (not at all the same as a std initializer list). `=` is not allowed there. I'd start https://en.cppreference.com/w/cpp/language/constructor -- also google std addressof; it is `&` that covers paranoid issues in generic code. – Yakk - Adam Nevraumont Apr 02 '21 at 14:18