C ++: использование разных типов итераторов в подклассах без нарушения механизма наследования - PullRequest
2 голосов
/ 11 февраля 2012

Я пытаюсь добиться следующего: учитывая абстрактный класс MemoryObject , который может наследовать каждый класс, у меня есть два подкласса: A Buffer и BigBuffer

template <typename T>
class MemoryObject
{
public:

    typedef typename std::vector<T>::iterator iterator;
    typedef typename std::vector<T>::const_iterator const_iterator;

    [...] //Lot of stuff

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

Буфер:

template <typename T>
class Buffer: public MemoryObject<T>
{
public:
    typedef typename std::vector<T>::iterator iterator;
    iterator begin() { return buffer_.begin(); }
    iterator end() { return buffer_.end(); };

    [...] //Lot of stuff

private:
    std::vector<T> buffer_;
};

И наконец:

template <typename T>
class BigBuffer: public MemoryObject<T>
{
public:
    [...] //Omitted, for now

private:
    std::vector<Buffer<T>*> chunks_;
};

Как видите, BigBuffer содержит std :: vector из Буфер *, поэтому вы можете просматривать BigBuffer в виде агрегации буфера (ов). Кроме того, у меня есть несколько функций, которые должны работать с каждым объектом MemoryObject, так что это настоящая подпись:

template <class KernelType, typename T>
void fill(CommandQueue<KernelType>& queue, MemoryObject<T>& obj, const T& value)
{
   //Do something with obj
}

Какой смысл? - спросите вы. Дело в том, что я должен реализовать итераторы над этими классами. Я уже реализовал их для Buffer , и это именно то, что мне нужно: возможность перебирать Buffer и доступ к диапазонам (например, b.begin (), b .begin () + 50). Очевидно, я не могу сделать то же самое для BigBuffer , потому что реальные данные (которые находятся внутри каждой Buffer 'закрытой переменной buffer_ ) разбросаны по всей памяти. Поэтому мне нужен новый класс, давайте назовем его BigBufferIterator , который может перегрузить оператор, такой как * или +, что позволяет мне «перепрыгивать» из куска памяти в другой, не вызывая ошибки сегментации.

Проблемы две:

  1. Тип итератора MemoryObject отличается от итератора. тип BigBuffer : первым является std :: vector :: iterator, последний BigBufferIterator . Мой компилятор явно жалуется
  2. Я хочу быть в состоянии сохранить универсальность сигнатур моих функций передавая им только MemoryObject &, не специализируя их на каждый тип класса.

Я попытался решить первую проблему, добавив классный параметр класса Iterator и присвоив ему аргумент по умолчанию для каждого класса, с моделью, слабо основанной на модели Александреску, основанной на политике. Это решение решило первую проблему, но не вторую: мой скомпилированный все еще жалуется, говоря мне: «Неизвестное преобразование из BigBuffer в MemoryObject», когда я пытаюсь передать BigBuffer в функцию (например, fill ()). Это потому, что типы итераторов различны ..

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

Заранее спасибо, только что прочитали до этого момента.

Смиренно, Alfredo

Ответы [ 2 ]

2 голосов
/ 11 февраля 2012

Их путь состоит в том, чтобы использовать наиболее общее определение в качестве типа итератора базы.То есть вы хотите обрабатывать данные в Buffer как один сегмент, тогда как BigBuffer - это последовательность соответствующих сегментов.Это немного прискорбно, потому что это означает, что вы рассматриваете свой итератор для единственного буфера в Buffer, как если бы он мог быть несколькими буферами, т.е. у вас есть сегментированная структура с одним сегментом.Однако, по сравнению с альтернативой (то есть иерархией итераторов с виртуальными функциями, обернутыми дескриптором, дающим семантику значения этому беспорядку), вы фактически не платите слишком много.

0 голосов
/ 31 августа 2016

Я столкнулся с той же проблемой в другом контексте;позвольте мне повторить это.

  1. У вас есть класс Base (который может быть абстрактным), который можно повторять через его BaseIterator.
  2. У вас есть подкласс Child, который немного отличается в реализации и для которого у вас есть различная специализированная ChildIterator.
  3. У вас есть customфункции, которые работают с Base и полагаются на его итеративность.
  4. невозможно создать шаблонную специализацию пользовательских функций для каждого подкласса Base.Возможные причины:
    • огромное дублирование кода;
    • вы распространяете этот код как библиотеку, а другие разработчики собираются создать подкласс Base;
    • другие классы займут некоторыессылка или указатель на Base и применение этих пользовательских функций, или более обобщенно:
    • Base реализует некоторую логику, которая будет использоваться в контекстах, где не известен ни один из подклассов (и запись полностью шаблоннаякод слишком громоздкий).

Редактировать: Другой возможностью было бы использование стирание типа .Вы бы скрыли реальный итератор, который вы используете, за классом фиксированного типа.Вы должны были бы оплатить стоимость вызова виртуальных функций.Вот реализация any_iterator класса, который реализует именно стирание типа итератора, и еще немного фона для него.


Единственное эффективное решение, которое я мог бы написать многоцелевой итератор , который может перебирать все возможные контейнеры, возможно, используя их внутренние компоненты (чтобы избежать копирования кода итератора для каждого подкласса Base):

// A bigger unit of memory
struct Buffer;

class Iterator {
    // This allows you to know which set of methods you need to call
    enum {
        small_chunks,
        big_chunks
    } const _granularity;

    // Merge the data into a union to save memory
    union {
        // Data you need to know to iterate over ints
        struct {
            std::vector<int> const *v;
            std::vector<int>::const_iterator it;
        } _small_chunks;

        // Data you need to know to iterate over buffer chunks
        struct {
            std::vector<Buffer *> const *v;
            std::vector<Buffer *>::const_iterator it;
        } _big_chunks;
    };

    // Every method will have to choose what to do
    void increment() {
        switch (_granularity) {
            case small_chunks:
                _small_chunks.it++;
                break;
            case big_chunks:
                _big_chunks.it++;
                break;
        }
    }

public:
    // Cctors are almost identical, but different overloads choose
    // different granularity
    Iterator(std::vector<int> const &container)
        : _granularity(small_chunks) // SMALL
    {
        _small_chunks.v = &container;
        _small_chunks.it = container.begin();
    }
    Iterator(std::vector<Buffer *> const &container)
        : _granularity(big_chunks) // BIG
    {
        _big_chunks.v = &container;
        _big_chunks.it = container.begin();
    }

    // ... Implement all your methods
};

Вы можете использовать один и тот же класс для Base и Child, но вам нужно инициализировать его по-разному.На этом этапе вы можете сделать begin и end виртуальными и вернуть Iterator, построенный по-разному в каждом подклассе:

class Base {
public:
    virtual Iterator begin() const = 0;
};

class IntChild : public Base {
    std::vector<int> _small_mem;
public:
    virtual Iterator begin() const {
        // Created with granularity 'small_chunks'
        return Iterator(_small_mem);
    }
    // ...
};

class BufferChild : public Base {
    std::vector<Buffer *> _big_mem;
public:
    virtual Iterator begin() const {
        // Created with granularity 'big_chunks'
        return Iterator(_big_mem);
    }
    // ...
};

Вы заплатите небольшую цену за производительность (из-за switchоператоры) и в дублировании кода, но вы сможете использовать любой универсальный алгоритм из <algorithm>, использовать диапазонный цикл, использовать Base только в каждой функции, и это не растягивает механизм наследования.

// Anywhere, just by knowing Base:
void count_free_chunks(Base &mem) {
    // Thanks to polymorphism, this code will always work
    // with the right chunk size
    for (auto const &item : mem) {
        // ...this works
    }
    // This also works:
    return std::count(mem.begin(), mem.end(), 0);
}

Обратите внимание, что ключевым моментом здесь является то, что begin() и end() должны возвращать один и тот же тип.Единственное исключение будет, если методы Child будут затенять метод Base (что в целом не является хорошей практикой ).

Одной из возможных идей было бы объявитьабстрактный итератор, но это не так хорошо.Вам придется все время использовать ссылку на итератор.Итератор, тем не менее, должен переноситься как легковесные типы, быть назначаемыми и легко конструируемыми, так что это сделает кодирование минным полем.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...