Search code examples
c++iteratorpolymorphism

C++: "Iterable<T>" interface


What I want to achieve is probably easily explained: Consider I have an abstract class that I know will contain multiple objects of known type. However the actual container holding these objects will be implemented in sub-classes.
In my abstract base class I now want to provide an interface to iterate over these objects. Given that I don't know (or rather don't want to fix) the type of container, I thought that iterators would probably be my best bet.

A conceptual declaration of this class might look like this:

class MyClass {
public:
    // Other interface methods, e.g. size()

    virtual Iterable<MyObject> objects() = 0;
};

The intention here is that I'll be able to iterate over the nested objects of my class like this:

MyClass *class = new ImplementationOfClass();
for (const MyObject &obj : class->objects()) {
    // Do stuff with obj
}

The issue I am facing however is that I can't seem to figure out how Iterable<MyObject> should be defined. The key property of this object is that at the time of defining this class I can only specify that the returned value will be iterable (using STL-style iterators) and will yield objects of type MyObject when the used iterator is dereferenced.

Normally I would use an abstract class on its own for this but it seems that this is very tricky (impossible?) since iterators are always passed by value and thus to my knowledge no Polymorphism is possible.

Questions dealing with how to pass arbitrary iterator types as arguments into a function always come up with the "use templates" answer. However I think in my case I can't use templates for that. This assumption might be wrong though, so feel free to correct me.

Essentially the barrier I always run into is that at some point I have to write down the iterator type explicitly which in my case I can't. I thought about using a template for that but this would then inhibit proper Polymorphism (I think?) because the user of that abstract interface seems to have the burden of explicitly initializing the correct template. The whole point of all of this however is that the caller does not have to care about the underlying structure.


TL;DR: Is there a way to create an interface class that only promises to be iterable and that dereferencing an iterator will yield an object of type T?


Solution

  • With the help of @FrançoisAndrieux and a hint from https://stackoverflow.com/a/4247445/3907364, I was able to come up with an approach to my problem.

    Essentially the idea is to create an iterator-wrapper that stores a function to obtain an object of the given type if given an index. That index is then what is iterated on.

    The nice thing about this is that the iterator interface is fixed by specifying the type of object that dereferencing it should return. The polymorphism comes into play by making the member function objects() virtual so that each sub-class can construct the iterator itself, providing a custom function pointer. Thus as long as there is a way to map an index to the respective element in the container (whichever is used), this trick is usable.

    Note that you can either directly use pointers to e.g.std::vector<T>::at or create a custom function that will return the respective element.

    Here's the implementation for the iterator (The implementation could probably be improved upon but it seems to get the job done):

    template< typename T > struct iterator_impl {
        using iterator_category = std::forward_iterator_tag;
        using difference_type   = std::ptrdiff_t;
        using value_type        = T;
        using pointer           = T *;
        using reference         = T &;
    
        using access_function_t = std::function< T &(std::size_t) >;
    
        // regular Ctor
        iterator_impl(std::size_t start, access_function_t &func, const void *id)
            : m_index(start), m_func(func), m_id(id) {}
    
        // function-move Ctor
        iterator_impl(std::size_t start, access_function_t &&func, const void *id)
            : m_index(start), m_func(func), m_id(id) {}
    
        // copy Ctor
        iterator_impl(const iterator_impl &) = default;
    
        // move ctor
        iterator_impl(iterator_impl &&other) {
            std::swap(m_index, other.m_index);
            m_func = std::move(other.m_func);
            std::swap(m_id, other.m_id);
        }
    
        // copy-assignment
        iterator_impl &operator=(const iterator_impl &other) = default;
    
        // prefix-increment
        iterator_impl &operator++() {
            ++m_index;
            return *this;
        }
    
        // postfix-increment
        iterator_impl operator++(int) {
            iterator_impl old = *this;
            ++(*this);
            return old;
        }
    
        bool operator==(const iterator_impl &other) { return m_index == other.m_index && m_id == other.m_id; }
    
        bool operator!=(const iterator_impl &other) { return !(*this == other); }
    
        T &operator*() { return m_func(m_index); }
    
        T *operator->() { return &m_func(m_index); };
    
    protected:
        std::size_t m_index = 0;
        access_function_t m_func;
        const void *m_id = nullptr;
    };
    

    Note that I had to introduce the m_id member variable as a means to properly compare iterators (std::function can't be compared using ==). it is meant to be e.g. the address of the container the elements are contained in. Its sole purpose is to make sure that 2 iterators that happen to have the same index but are iterating over completely different sets are not considered equal.

    And based on that here's an implementation of an Iterable<T>:

    template< typename T > struct Iterable {
        using iterator       = iterator_impl< T >;
        using const_iterator = iterator_impl< const std::remove_const_t< T > >;
    
        Iterable(std::size_t start, std::size_t end, typename iterator_impl< T >::access_function_t &func, const void *id)
            : m_begin(start, func, id), m_end(end, func, id) {}
    
        iterator begin() { return m_begin; }
        iterator end() { return m_end; }
    
        const_iterator begin() const { return m_begin; }
        const_iterator end() const { return m_end; }
    
        const_iterator cbegin() const { return m_begin; }
        const_iterator cend() const { return m_end; }
    
    protected:
        iterator m_begin;
        iterator m_end;
    };