Q:

Best way to access elements of custom 2-dim C++ container

I have a very basic 2D C++ container that relies on a single dimensional container (std::array or std::vector) and "advanced" indexing. This container is not expected to store many elements, not even 500. What would be the best approach in accessing its elements?
At first I was using int for indexing the elements directly while iterating the container. However, after reading a bit, posts such as this one made me switch to using std::size_t instead. (The switch was primarily about picking up good habits and not due to the requirements of my container. But other sources made me question whether it would be actually a good habit.)
As a result I had to readjust several for() loops to avoid underflow realated errors, e.g. when checking the values of elements next to one another, or when iteratin the elements backward in a row or a column. These changes in turn decreased the readability and made the indexing more error prone. (MyIiexperience is definitely a factor in this matter.)
What should I do?

stick with std::size_t
keep using int(while also restricting the size of my 2D storage to avoid unlikely overflows)
stick with range or iterator based loops by creating them for columns and rows (suggestions and links are welcome)
something else

Thank you in advance!
P.S.: Even C++20 functionality is welcomed!

A:

std::ptrdiff_t is a signed std::size_t.
I use that.  I have heard arguments that is what the standard should have used, at the cost of a single bit of maximum size.
Writing a stride-friendly iterator is a bit of a pain.  Here is a sketch:
template<class T, class Stride=std::integral_constant<std::size_t, 1>>
struct stride_iterator {
  using difference_type = std::ptrdiff_t;
  using value_type = T;
  using reference= T&;
private:
  T* dataptr = nullptr;
  Stride datastride = {};
  T* data() const { return dataptr; }
  T*& data() { return dataptr; }
  Stride stride() const { return datastride; }
public:
  explicit stride_iterator( T* ptr, Stride s ):
    dataptr(ptr),
    datastride{ std::move(s) }
  {}
  explicit stride_iterator( T* ptr ):
    dataptr(ptr)
  {}
  stride_iterator():stride_iterator(nullptr) {}
  stride_iterator(stride_iterator const&)=default;
  stride_iterator& operator=(stride_iterator const&)& =default;
  stride_iterator(stride_iterator &&)=default;
  stride_iterator& operator=(stride_iterator &&)& =default;
  
  T& operator*() const { return *data(); }
  T* operator->() const { return data(); }
  T& operator[](std::ptrdiff_t n) const {
    return *(*this+n);
  }
  stride_iterator& operator+=( std::ptrdiff_t n )& {
    data() += (n*stride());
    return *this;
  }
  stride_iterator& operator-=( std::ptrdiff_t n )& {
    data() -= (n*stride());
    return *this;
  }
  friend stride_iterator operator+( std::ptrdiff_t rhs, stride_iterator lhs ) {
    return std::move(lhs)+rhs;
  }
  friend stride_iterator operator+( stride_iterator lhs, std::ptrdiff_t rhs ) {
    lhs += rhs; 
    return lhs;
  }
  friend stride_iterator operator-( stride_iterator lhs, std::ptrdiff_t rhs ) {
    lhs += rhs; 
    return lhs;
  }
  stride_iterator& operator++() {
    *this += 1;
    return *this;
  }
  stride_iterator& operator--() {
    *this -= 1;
    return *this;
  }
  stride_iterator operator++(int) {
    auto r = *this;
    ++*this;
    return r;
  }
  stride_iterator operator--(int) {
    auto r = *this;
    --*this;
    return r;
  }
  friend std::ptrdiff_t operator-( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return (lhs.data()-rhs.data())/stride();
  }
  friend bool operator<( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() < rhs.data();
  }
  friend bool operator<=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() <= rhs.data();
  }
  friend bool operator>( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() > rhs.data();
  }
  friend bool operator>=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() >= rhs.data();
  }
  friend bool operator==( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() == rhs.data();
  }
  friend bool operator!=( stride_iterator const& lhs, stride_iterator const& rhs ) {
    return lhs.data() != rhs.data();
  }
};

template<class It>
struct range {
    It b, e;
    It begin() const { return b; }
    It end() const { return e; }
    decltype(auto) operator[]( std::ptrdiff_t i ) const {
        return b[i];
    }
    std::size_t size() const { return end()-begin(); }
    bool empty() const { return end()==begin(); }
};

struct toy_matrix {
    std::ptrdiff_t width = 1;
    std::ptrdiff_t height = 1;
    std::vector<int> data = std::vector<int>( 1 );
    
    toy_matrix() = default;
    toy_matrix( std::size_t w, std::size_t h ):width(w),height(h), data(w*h) {}
    toy_matrix( std::size_t w, std::size_t h, std::initializer_list<int> il ):width(w),height(h), data(il) {
        data.resize(w*h);
    }
    
    range<stride_iterator<int>> row( std::ptrdiff_t i ) {
        int* ptr = data.data();
        ptr += height * i;
        return { stride_iterator<int>{ ptr }, stride_iterator<int>{ ptr + width } };
    }
    range<stride_iterator<int, std::ptrdiff_t>> column( std::ptrdiff_t i ) {
        int* ptr = data.data();
        ptr += i;
        return { stride_iterator<int, std::ptrdiff_t>{ ptr, width }, stride_iterator<int, std::ptrdiff_t>{ ptr + height * width, width } };
    }
    range<stride_iterator<int>> operator[]( std::ptrdiff_t i ) {
        return row(i);
    }
    int& operator()( std::ptrdiff_t x, std::ptrdiff_t y ) {
        return (*this)[x][y];
    }
};

Test code:
toy_matrix m{ 5, 4, {
    1,2,3,4,5,
    10,20,30,40,50,
    100,200,300,400,500,
    1000,2000,3000,4000,5000,
}};

for (auto x : m.column(0)) {
    std::cout << x << "\n";
}
for (auto x : m.column(1)) {
    std::cout << x << "\n";
}
for (auto x : m.column(2)) {
    std::cout << x << "\n";
}

output:

1
10
100
1000
2
20
200
2000
3
30
300
3000

Live example.

