Skip to Content
Course content

125: Custom Iterators for User-Defined Containers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when developers first try to make their own containers: they treat iterators as a "suggestion" rather than a strict contract. They think that as long as they overload operator++ and operator*, they've built an iterator. It feels intuitive, right? If it looks like a pointer and moves like a pointer, it's an iterator.

The Myth that a Pointer is Enough

Here is where that logic falls apart. Imagine you've built a RingBuffer—a container where the end of the array wraps back around to the beginning. If you just hand out a raw pointer to the internal array, your iterator will walk right off the edge of your memory allocation the moment it hits the end of the buffer, completely ignoring your wrap-around logic.

// The "naive" approach that crashes your program
struct RingBufferIterator {
    int* ptr;
    int& operator*() { return *ptr; }
    RingBufferIterator& operator++() { 
        ptr++; // This is the disaster. It doesn't know about the buffer's size!
        return *this; 
    }
    bool operator!=(const RingBufferIterator& other) { return ptr != other.ptr; }
};

Even if you fix the increment logic, you'll find that std::sort or std::find might refuse to compile or behave erratically. Why? Because the C++ Standard Library doesn't just look at the operators; it looks for specific traits. The STL needs to know if your iterator is "Random Access," "Bidirectional," or just "Forward." Without these declarations, your iterator is a stranger to the rest of the ecosystem.

Fulfilling the STL Iterator Contract

To make your iterator truly "first-class," you need to provide a few type aliases. This tells the compiler and the STL algorithms exactly what they are dealing with. I usually recommend starting with a forward_iterator_tag unless you absolutely need the complexity of bidirectional movement.

Let's look at how we actually implement a wrapping iterator for that RingBuffer. The trick is to stop storing a raw pointer and instead store a pointer back to the container and the current index.

#include <iterator>
#include <vector>

class RingBuffer {
    std::vector<int> data;
    size_t head;
    size_t count;

public:
    // ... constructor and other methods ...

    struct Iterator {
        // These traits are the "secret sauce" that make STL algorithms work
        using iterator_category = std::forward_iterator_tag;
        using value_type        = int;
        using difference_type   = std::ptrdiff_t;
        using pointer           = int*;
        using reference         = int&;

        RingBuffer* buffer;
        size_t index;

        reference operator*() const { 
            return buffer->data[(buffer->head + index) % buffer->data.size()]; 
        }

        Iterator& operator++() {
            index++;
            return *this;
        }

        bool operator!=(const Iterator& other) const {
            return index != other.index;
        }
    };

    Iterator begin() { return Iterator{this, 0}; }
    Iterator end()   { return Iterator{this, count}; }
};

Notice the operator*. Instead of dereferencing a raw pointer, we're calculating the actual position based on the buffer's head and the current index. This encapsulates the wrap-around logic inside the iterator, meaning the user of your container can just use a standard for (auto x : myBuffer) loop without ever knowing the memory is circular.

One small tip: always mark your operator* and operator!= as const. It seems like a pedantic detail, but you'll run into frustrating compiler errors the moment you try to pass your container into a function that takes a const RingBuffer&.




📋 Practical Task

Implementing a Wrapping Iterator for a Ring Buffer

You have been provided with a skeleton of a RingBuffer class. The internal storage is a std::vector, and the buffer tracks a head position to allow for efficient insertions and deletions at the front.

Your Task: Complete the Iterator nested struct. You must:

  • Define the five required using aliases (traits) to make it a valid std::forward_iterator.
  • Implement operator* so that it correctly handles the wrap-around logic using the formula: (head + index) % vector_size.
  • Implement operator++ (prefix) to advance the index.
  • Implement operator!= to allow the loop to know when it has reached the end().

Once implemented, verify your work by using a range-based for loop to print the elements of a RingBuffer where the head is positioned in the middle of the underlying vector.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.