Итераторы C++ и наследование

У вас есть быстрый вопрос о том, как лучше всего реализовать итераторы в следующем:

Скажем, у меня есть шаблонный базовый класс «Список» и два подкласса «ListImpl1» и «ListImpl2». Основное требование базового класса - быть итерируемым, т.е. я могу сделать:

for(List<T>::iterator it = list->begin(); it != list->end(); it++){
   ...
}

Я также хочу разрешить добавление итератора, например:

for(List<T>::iterator it = list->begin()+5; it != list->end(); it++){
   ...
}

Итак, проблема в том, что реализация итератора для ListImpl1 будет отличаться от реализации для ListImpl2. Я обошел это, используя оболочку ListIterator, содержащую указатель на ListIteratorImpl с подклассами ListIteratorImpl2 и ListIteratorImpl2, но все это становится довольно запутанным, особенно когда вам нужно реализовать оператор + в ListIterator.

Любые мысли о лучшем дизайне, чтобы обойти эти проблемы?


person user360366    schedule 07.06.2010    source источник
comment
Почему бы не использовать std::advance?   -  person kennytm    schedule 07.06.2010
comment
Спасибо, но скажите, что мне нужно, я хочу предоставить более эффективный оператор +, чем просто повторное использование оператора ++. Наверно у меня все та же проблема?   -  person user360366    schedule 07.06.2010
comment
Различие между прямым итератором (для чего предназначен std::advance()) и итератором с произвольным доступом заключается в том, что прямой итератор не имеет эффективной реализации operator+ (вспомните связанный список). Этот вопрос подразумевает итератор, который имеет статический произвольный доступ, но имеет динамическое поведение для этого доступа.   -  person Simon Buchan    schedule 07.06.2010
comment
Правильно - я хотел бы динамическое поведение для произвольного доступа. Я не понимаю, почему это может быть проблемой?   -  person user360366    schedule 18.06.2010


Ответы (5)


Если вам сойдет с рук сделать List<T>::iterator невиртуальным, то делегирование виртуальности от добавления в список упрощает задачу:

template<typename T>
class List
{
    virtual void add_assign(iterator& left, int right) = 0;

public:
    class iterator
    {
        const List* list;
        const T* item;
    public:
        iterator(const List* list, const T* item) : list(list), item(item) {}

        iterator& operator +=(int right)
        {
            list->add_assign(*this, right);
            return *this;
        }
        static iterator operator +(iterator const& left, int right)
        {
            iterator result = left;
            result += right;
            return result;
        }
    };

    virtual iterator begin() const = 0;
    virtual iterator end() const = 0;
};

В противном случае (например, если итераторам нужно хранить значительно отличающиеся данные), вам придется делать обычную скучную реализацию указателя на реализацию, чтобы получить свою виртуальность:

template<typename T>
class List
{
    class ItImpl
    {
        virtual ItImpl* clone() = 0;
        virtual void increment() = 0;
        virtual void add(int right) = 0;
    };
public:
    class iterator
    {
        ItImpl* impl;
    public:
        // Boring memory management stuff.
        iterator() : impl() {}
        iterator(ItImpl* impl) : impl(impl) {}
        iterator(iterator const& right) : impl(right.impl->clone()) {}
        ~iterator() { delete impl; }
        iterator& operator=(iterator const& right)
        {
            delete impl;
            impl = right.impl->clone();
            return *this;
        }

        // forward operators to virtual calls through impl.
        iterator& operator+=(int right)
        {
            impl->add(right);
            return *this;
        }
        iterator& operator++()
        {
            impl->increment();
            return *this;
        }
    };
};

template<typename T>
static List<T>::iterator operator+(List<T>::iterator const& left, int right)
{
    List<T>::iterator result = left;
    result += right;
    return result;
}

template<typename T>
class MagicList : public List<T>
{
    class MagicItImpl : public ItImpl
    {
        const MagicList* list;
        const magic* the_magic;
        // implement ...
    };
public:
    iterator begin() const { return iterator(new MagicItImpl(this, begin_magic)); }
    iterator end() const { return iterator(new MagicItImpl(this, end_magic)); }
};
person Simon Buchan    schedule 07.06.2010

Среди итераторов есть кое-что очень важное, называемое Iterator Category:

  • Итератор ввода
  • Выходной итератор
  • Форвардитератор
  • Двунаправленный итератор
  • Итератор случайного доступа

Каждая категория определяет точный набор операций, которые эффективно поддерживаются итератором.

Здесь, кажется, вы хотите отказаться от этого мощного механизма идентификации, чтобы создать какую-то категорию ублюдков, в которой присутствуют все операции, но нет никаких гарантий их эффективности.

Я думаю, что ваш дизайн пахнет.

person Matthieu M.    schedule 07.06.2010
comment
Я действительно не понимаю, что вы имеете в виду. Это просто итератор произвольного доступа, гарантирующий постоянное время. - person user360366; 14.06.2010
comment
Тогда operator+ должен быть частью интерфейса (принимая целое число со знаком в качестве аргумента правой стороны). В чем твоя проблема? - person Matthieu M.; 14.06.2010
comment
Проблема в том, что оператор + возвращает итератор, но реализация итератора различна в двух подклассах, поэтому вам нужно скрыть эту реализацию внутри итератора-оболочки. Мой вопрос заключался в том, есть ли лучший способ сделать это. - person user360366; 18.06.2010
comment
Это можно сделать легко: класс pimpl iterator, содержащий указатель на динамически размещаемый объект, производный от класса iterator_interface (с методом clone). Это означает 2 класса + 1 на реализацию. - person Matthieu M.; 18.06.2010
comment
Это решение из исходного сообщения. Мне просто интересно, может ли кто-нибудь предложить что-то более чистое, но, похоже, все согласны с тем, что это нормальный способ получить результат. - person user360366; 21.06.2010

Итак, проблема в том, что реализация итератора для ListImpl1 будет отличаться от реализации для ListImpl2. Я обошел это, используя оболочку ListIterator, содержащую указатель на ListIteratorImpl с подклассами ListIteratorImpl2 и ListIteratorImpl2, но все это становится довольно запутанным, особенно когда вам нужно реализовать оператор + в ListIterator.

Этот дизайн прекрасен ИМХО, я не вижу в нем ничего грязного. За исключением равенства и вычитания, операции итератора могут быть довольно легко реализованы виртуальной функцией, поэтому у вас будет что-то вроде

class ListIteratorInterface // abstract
{
protected:
  virtual Data& operator*()=0;
  // and other operations
};
class ListIteratorA;
class ListIteratorB; // implementation of the above
class ListIterator
{
  ListIteratorInterface* impl_;
public:
  // when you create this, allocate impl_ on the heap
  // operations, forward anything to impl_
};
person jpalecek    schedule 07.06.2010
comment
Это примерно то, что у меня есть на данный момент. Что еще более запутанно, так это реализация оператора + в классе ListIterator. Поскольку мне нужно вернуть новый ListIterator и ListIteratorA/B, я думаю, мне нужно создать виртуальный метод clone() в ListIteratorInterface, чтобы создать новый импл правильного типа. - person user360366; 07.06.2010

Вы можете сохранить оператор + как частный виртуальный метод в базовом классе и вызвать его итератором.

В качестве альтернативы вы можете рассмотреть статически полиморфные классы списков, а не полиморфизм времени выполнения.

person Puppy    schedule 07.06.2010

Скажем, у меня есть шаблонный базовый класс «Список» и два подкласса «ListImpl1» и «ListImpl2».

Что именно вы получаете, используя здесь наследование?

person fredoverflow    schedule 07.06.2010
comment
@jom: существуют разные виды полиморфизма. Контейнеры STL основаны на полиморфизме времени компиляции/статическом связывании/универсальности/шаблонах, а не на том полиморфизме, который вы, кажется, имеете в виду. - person fredoverflow; 14.06.2010
comment
Я говорю о полиморфизме во время выполнения, т.е. использование List‹T› — это интерфейс с несколькими реализациями ListImpl1‹T› и ListImpl2‹T›, прозрачный для клиентского кода, хотя, очевидно, и здесь присутствует статический полиморфизм. - person user360366; 18.06.2010