Finding a Generic Way to Access Diverse Containers in C++

In modern C++, one common challenge developers face is iterating over different container types uniformly. Imagine you have several containers: the standard std::vector, std::list, and a custom list. Each container has its own unique iterator type, which can make writing clean and maintainable code difficult. In this blog post, we will explore this problem and present an elegant solution for accessing these containers generically.

The Problem

As you attempt to access items in these diverse containers, you quickly run into an issue. In your class Foo, you want a single iterator that can interact with std::vector<int>, std::list<int>, and your custom list. However, since all these containers return different iterator types, your attempts to assign one iterator would lead to type conflicts.

Here’s a simplified version of the challenge you’re facing:

class Foo {
public:
    Foo() {
        std::list<int> x;
        std::vector<int> y;
        custom_list<int> z;

        iter = x.begin(); // OR
        iter = y.begin(); // OR
        iter = z.begin(); // Problem: Different iterator types
    };
private:
    std::iterator<int> iter; // This won't work
};

The Solution

To resolve this issue while keeping your code elegant and manageable, we can utilize a concept known as type erasure. This technique allows us to create a generic interface for iterators, providing a uniform way to access items across different container types.

Step 1: Base Iterator Class

We’ll start by defining a base interface that all iterators will inherit from. This provides a common approach to interacting with the different iterator types.

class IIterator {
public:
    virtual ~IIterator() = default;
    virtual int& operator*() = 0; // Dereference operator
    virtual IIterator& operator++() = 0; // Increment operator
    virtual bool operator!=(const IIterator& other) const = 0; // Comparison operator
};

Step 2: Implementation for Each Container Type

Next, we will implement this interface for the different container types.

For std::vector

class VectorIterator : public IIterator {
    std::vector<int>::iterator it;
public:
    VectorIterator(std::vector<int>::iterator iterator) : it(iterator) {}
    int& operator*() override { return *it; }
    IIterator& operator++() override { ++it; return *this; }
    bool operator!=(const IIterator& other) const override {
        return this->it != dynamic_cast<const VectorIterator&>(other).it;
    }
};

For std::list

class ListIterator : public IIterator {
    std::list<int>::iterator it;
public:
    ListIterator(std::list<int>::iterator iterator) : it(iterator) {}
    int& operator*() override { return *it; }
    IIterator& operator++() override { ++it; return *this; }
    bool operator!=(const IIterator& other) const override {
        return this->it != dynamic_cast<const ListIterator&>(other).it;
    }
};

For Custom List

Assuming your custom list defining an iterator is available, you would similarly implement it as below:

class CustomListIterator : public IIterator {
    custom_list<int>::iterator it;
public:
    CustomListIterator(custom_list<int>::iterator iterator) : it(iterator) {}
    int& operator*() override { return *it; }
    IIterator& operator++() override { ++it; return *this; }
    bool operator!=(const IIterator& other) const override {
        return this->it != dynamic_cast<const CustomListIterator&>(other).it;
    }
};

Step 3: Using the Iterators in Foo

Now, in your class Foo, you can maintain a single type of pointer to the base IIterator class to handle any of the container types.

class Foo {
public:
    Foo() {
        std::list<int> x;
        std::vector<int> y;
        custom_list<int> z;

        IIterator* iter;

        // Let's assume you have instances of each container
        iter = new VectorIterator(y.begin());
        // OR
        iter = new ListIterator(x.begin());
        // OR
        iter = new CustomListIterator(z.begin());

        // You can now use iter as a uniform iterator
    };
};

Conclusion

By utilizing type erasure through a common iterator interface, we have provided an elegant way to manipulate disparate container types in C++. This allows for cleaner, more maintainable code while avoiding the headache of managing multiple iterator types.

For further exploration into this topic, consider perusing some insightful articles:

This solution not only enhances your C++ skills but also helps create versatile and reusable code structures that can handle various container types seamlessly.