0

I've created a simple circular buffer by inheriting std::vector and overloading its operator[] to modulo the desired index with the vector size:

template <typename T>
class circvector : public std::vector<T> {
public:
    T& operator[](size_t index) { return *(this->data() + index%this->size()); };       // modulo index by vector size when accessing with [] operator
};

int main()
{
    circvector<int> buffer;                         // create a circvector
    buffer.resize(10);                              // resize it
    std::iota(buffer.begin(), buffer.end(), 0);     // fill with monotonically increasing integers

    for (int i = 0; i < buffer.size() * 2; i++)
        std::cout << buffer[i] << " ";              // access elements with [] beyond the circvector size is safe
}

which correctly produces the output:

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

I would also like to create a "smart" forward input iterator which supports operator++ (increment) and operator* (dereference) and which automatically "wraps" when advancing past the last element in the vector back to the beginning of the underlying vector to which it's associated.

This hypothetical iterator would allow, for example, easier execution of some functions in the algorithm library (which often take iterators as arguments) on a subset of the circvector without adding logic to check for and then split into two separate calls when the desired subset spans the end->start wrap of the circvector.

I've been reading up on creating custom iterators but descriptions get pretty hairy with information that seemingly satisfies requirements I don't think I need. Add to that discussion of recent deprecation of std::iterator and I'm not sure where to even begin to create my own iterator, or associate it with my circvector class.

Can someone get me started with a minimally workable template to run with?

NKatUT
  • 429
  • 3
  • 10
  • 1
    I guess an iterator over a circular buffer is against the very idea of a circular buffer. Does `std::stack` have a `begin` and an `end`? Also, if it wraps, how will some algorithms know where the end is? – zdf Nov 16 '19 at 05:45
  • Don’t waste time, use the one already made e.g. https://www.boost.org/doc/libs/1_71_0/doc/html/circular_buffer.html – 4xy Nov 16 '19 at 09:34
  • @ZDF I don't see why it would be against the idea. What would be so wrong with iterating from element "begin()+8" to "begin()+12", and allow the iterator to automatically access the underlying vector elements at the start of the vector even if "+12" would otherwise increment past the last element in the vector? Furthermore, why not allow iterating from "begin()" to "begin()+100" if one wanted to iterate over the buffer 10 times? I agree an end() iterator wouldn't make much sense to have. – NKatUT Nov 16 '19 at 21:11
  • @4xy Thanks, I've run across the boost::circular_buffer before and figured I could just plug it in and try it, but I was also hoping to use this as an opportunity to learn more about custom smart iterators as well as have a solution which is only minimally modified from a std:vector for which I'm already familiar. – NKatUT Nov 16 '19 at 21:14
  • I do not see any practical use. A client needs `push_back` and `pop_front`. – zdf Nov 16 '19 at 21:17
  • @ZDF There is definitely a practical use... I allocate the entire buffer at the beginning with .resize() or .assign() and have no need to add or remove elements, only replace them or access them with [] operator or with my (hypothetical) iterator if I want to jump from Nth element to N+Mth element. The push_back() and pop_front() methods would just call the underlying default std::vector methods and actually resize the buffer instead adding or subtracting from it in the circular fashion that the overloaded [] operator provides. – NKatUT Nov 16 '19 at 21:31
  • @4xy Looks like the boost::circular_buffer iterators do not exhibit the behavior I require. If you advance the boost::circular_buffer<>::iterator past the size of the circular_buffer, it crashes with a read access violation. – NKatUT Nov 16 '19 at 21:46

1 Answers1

0

This class appears to achieve the desired behavior, though it inherits from the deprecated std::iterator class:


template <typename T>
class circvector: public std::vector<T> {
public:
    T& operator[](size_t index_) { return *(this->data() + index_%this->size()); };     // modulo index_ by vector size when accessing with [] operator

    class iterator; // forward declaration

    iterator begin() { return circvector<T>::iterator(*this, 0); }
    iterator end() { return circvector<T>::iterator(*this, this->size()); } // will be same as begin() due to modulo in iterator constructor initializer list

private:

    class iterator : public std::iterator<std::output_iterator_tag, T> {
    private:
        circvector<T>& container_;      // NOTE: ORDER MATTERS! // dependency injection
        size_t index_{ 0 };         // NOTE: ORDER MATTERS! // state of iterator

    public:
        T& operator*() const { return container_[index_]; }                                     // this uses the overloaded operator[] which includes modulo
        iterator& operator+(int N) { index_ = (index_ + N) % container_.size(); return *this; } // random increment by N
        iterator& operator++() { index_ = (index_ + 1) % container_.size(); return *this; }     // increment with modulo
        iterator operator++(int) { return ++(*this); }                                              // just calls prefix increment: operator++()
        bool operator!=(const iterator & right) const { return index_ != right.index_ % container_.size(); }
        bool operator==(const iterator & right) const { return index_ == right.index_ % container_.size(); }
        explicit iterator(circvector<T>& container, size_t index_ = 0) : container_(container), index_(index_ % container_.size()) {}       // constructor
    };
};

It was modified from https://lorenzotoso.wordpress.com/2016/01/13/defining-a-custom-iterator-in-c/

A test program is:

int main()
{
    circvector<int> buffer;
    buffer.assign({ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });

    auto start_offset{ 8 };
    auto end_offset{ start_offset + 5 };

    for (int i = 0; i < buffer.size(); i++) std::cout << buffer[i] << " ";
    std::cout << "\n";
    std::for_each(buffer.begin() + start_offset, buffer.begin() + end_offset, [](auto& i) { i = 42; });
    for (int i = 0; i < buffer.size(); i++) std::cout << buffer[i] << " ";
}

creating output:

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

The use of algorithms from begin() to end() of course no longer works, since begin()==end(). But you can operate on partial segments of the buffer using algorithms (like std::for_each as shown) as long as the length is 1 less than the full buffer size.

NKatUT
  • 429
  • 3
  • 10