1

Consider part of the following class dynamic_matrix which is a container encapsulating a std::vector<std::vector<T>> with an invariant which states that every row shall have an equal number of elements, and every column shall have an equal number of elements. The majority of the class has been omitted as most parts are irrelevant for this question.

dynamic_matrix

template<typename _Ty>
class dynamic_matrix {
public:
    // public type defns
    typedef _Ty value_type;
    typedef _Ty& reference;
    typedef const _Ty& const_reference;
    typedef _Ty* pointer;
    typedef const _Ty* const_pointer;
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;
    typedef dynamic_matrix_iterator<value_type> iterator; // defined below
private:
    typdef std::vector<std::vector<value_type>> vector_2d;
    // enables use of operator[][] on dynamic_matrix
    class proxy_row_vector {
    public:
        proxy_row_vector(const std::vector<value_type>& _vec) : vec(_vec) {}
        const_reference operator[](size_type _index) const {
            return vec[_index];
        }
        reference operator[](size_type _index) {
            return vec[_index];
        }
    private:
        std::vector<value_type>& vec;
    };
public:
    explicit dynamic_matrix() : mtx() {}
    template<class _Uty = _Ty,
        class = std::enable_if_t<std::is_default_constructible<_Uty>::value>
    > explicit dynamic_matrix(size_type _rows, size_type _cols) 
       : mtx(_row, std::vector<value_type>(_cols)) {}
   // ... a few other constructors, not important here...

   // Capacity

   bool empty() const noexcept {
       return mtx.empty();
   }
   size_type rows() const noexcept {
       return mtx.size();
   }
   size_type columns() const noexcept {
       if(empty()) return static_cast<size_type>(0);
       return mtx[0].size();
   }

   // Element access       

   proxy_row_vector operator[](size_type _row_index) const {
       return proxy_row_vector(mtx[_row_index]);
   }
   proxy_row_vector operator[](size_type _row_index) {
       return proxy_row_vector(mtx[_row_index]);
   }
   const value_type* inner_data(size_type _row_index) const noexcept {
       return mtx[_row_index).data();
   }
   value_type* inner_data(size_type _row_index) noexcept {
       return mtx[_row_index].data();
   }
   std::ostream& write(std::ostream& _os, char _delim = ' ') const noexcept {
       for (const auto& outer : mtx) {
           for (const auto& inner : outer) 
               _os << inner << _delim;
           _os << '\n';
       }
       return _os;
   }

   // Iterators

   iterator begin() {
       return iterator(inner_data(0)); // points to first element of matrix
   }
   iterator end() {
       // points to element past end of matrix
       return iterator(inner_data(rows()-1) + columns());
   }
private:
    vector_2d mtx;
};

The custom-iterator, dynamic_matrix_iterator, which uses a std::bidirectional_iterator_tag is defined below.

dynamic_matrix_iterator

template<typename _Ty>
class dynamic_matrix_iterator : public std::iterator<std::bidirectional_iterator_tag,
    _Ty, std::ptrdiff_t, _Ty*, _Ty&> {
public:
    dynamic_matrix_iterator(_Ty* _ptr) : ptr(_ptr) {}
    dynamic_matrix_iterator(const dynamic_matrix_iterator& _other) = default;
    dynamic_matrix_iterator& operator++() {
        ptr++;
        return *this;
    }
    dynamic_matrix_iterator operator++(int) {
        dynamic_matrix_iterator<_Ty> tmp(*this);
        operator++();
        return tmp;
    }
    dynamic_matrix_iterator& operator--() {
        ptr--;
        return *this;
    }
    dynamic_matrix_iterator operator--(int) {
        dynamic_matrix_iterator<_Ty> tmp(*this);
        operator--();
        return tmp;
    }
    _Ty& operator*() {
        return *ptr;
    }
    _Ty* operator->() {
        return ptr;
    }
    bool operator==(const dynamic_matrix_iterator& _other) {
        return ptr == _other.ptr;
    }
    bool operator!=(const dynamic_matrix_iterator& _other) {
        return ptr != _other.ptr;
    }
private:
    _Ty* ptr;
};

Here is a test-case in which I use a range based for loop to print the elements in the matrix, as well as using the dynamic_matrix::write() method to compare:

int main(void) {
    std::size_t rows = 3;
    std::size_t cols = 3;
    dynamic_matrix<int> dm(rows,cols);
    int count = 0;
    // assign increasing natural numbers to each element
    for (std::size_t i = 0; i < rows; ++i) {
        for (std::size_t j = 0; j < cols; ++j) 
            dm[i][j] = ++count;
    }
    int range_count = 0;
    // print using iterators
    for (auto x : dm) {
        std::cout << x << ' ';
        ++range_count;
        if (!(range_count % cols))
            std::cout << std::endl;
    }
    std::cout << std::endl;
    // print using inbuilt method
    dm.write(std::cout);
}

Now, the iterator-based ranged for loop prints the following:

1 2 3
0 0 0
35 0 4
5 6 0
0 0 35
0 7 8 
9

whereas the correct output as given by dynamic_matrix::write is, of course,

1 2 3
4 5 6
7 8 9

In the incorrect output using iterators, we see some garbage elements between the actual matrix elements which I can only assume are brought about by undefined behaviour when the pointer of the dynamic_matrix_iterator accesses "random" memory in-between each row-vector of the matrix - running this on a different machine would likely yield different values here or something else unexpected if this is undefined behaviour as I suspect it is.

Question

So given this behaviour, is there a more elegant way to implement iterators for this container? Also, given that a std::vector uses contiguous storage why is the above actually happening - are the "garbage" values part of the vectors' internal memory used for allowing expansion of the vector?

sjrowlinson
  • 3,297
  • 1
  • 18
  • 35
  • I did think about posting it to codereview, but whilst the code compiles and runs the output is incorrect for my specific problem so I thought it would be better to post it here. – sjrowlinson Jul 02 '16 at 17:07
  • You are right about that. – R Sahu Jul 02 '16 at 17:08
  • "given that a std::vector uses contiguous storage why is the above actually happening " - because though a vector stores in contiguous memory, you have far more than one vector here. You have a vector of vectors, the prime vector's memory is indeed contiguous, storing the rows. Any given row vector is indeed contiguous, storing its elements. But the *collective* of row's elements are not contiguous *with each other*. – WhozCraig Jul 02 '16 at 17:09
  • @WhozCraig I see, that makes sense. – sjrowlinson Jul 02 '16 at 17:13
  • Iterators are subtle. If you read the requirements carefully, you will find that you don't actually satisfy the BidirectionalIterator requirements (but only InputIterator). – Kerrek SB Jul 02 '16 at 17:28
  • 1
    If you modify your structure from `vector>` to just a `vector` and manage a `row` and `column` count you can get by with a single `vector` and things will be more clear. Inserting and removing will be less straight forward but since you can easily iterate a single vector it's not hard. – Matt Jul 02 '16 at 17:42
  • @Matt If I just have a single `std::vector` then I won't have a matrix object at all - it would simply be a row/column vector in that case. – sjrowlinson Jul 02 '16 at 17:50
  • @ArchbishopOfBanterbury you can save a matrix as a vector and index into it as if it was a matrix. For example `m = [1 2; 3 4]` row(1) = [1 2] can be saved as `v = [1 2 3 4]` with `row = 2` `column = 2`. In this way it is saved a row major matrix. If you prefer column major then `v = [1 3 2 4]`. – Matt Jul 02 '16 at 18:00
  • Hmmm, I see, not sure if I like that solution though tbh. Whilst it would make iterator support much easier, the rest of my class would need to be altered significantly. – sjrowlinson Jul 02 '16 at 18:04
  • @ArchbishopOfBanterbury There are all kinds of reasons that you definitely want to handle the underlying storage as a single vector for this kind of object. Less overhead, it ends up being simpler, compatibility with matrix libraries (which require a matrix to be in contiguous block of memory). – Nir Friedman Jul 02 '16 at 23:24

1 Answers1

2

Vectors of vectors are not contiguous. So your approach is broken.

Either use a flat vector and maintain the dimensions separately, or write a generic iterator for ranges of ranges.

The second is best done with range abstractions. Store two ranges (which are pairs of iterators). == compares outer range begin and inner range (with all empty ranges compare equal). Advance is:

shrink inner range
while inner range is empty
  shrink outer range, get new inner range from front of outer
repeat

Initialization is with outer range:

Init outer range
While outer is non-empty And (inner range=front of outer) is empty 
  shrink outer range

Where shrinking is from the front.

Dereference is "get from front of inner range".

Here is an earlier post where I implemented this strategy.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Cheers for this, I decided to change my structure to using a single `std::vector` in row-major format from the other feedback but I'm sure your answer will come in handy for if I ever need a similar data structure. – sjrowlinson Jul 03 '16 at 16:17