C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
125: Custom Iterators for User-Defined Containers
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
usingaliases (traits) to make it a validstd::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 theend().
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.
There are no comments for now.