Подтвердить что ты не робот

Const-correctness самоидентизированных итераторов

Общая цель

Я управляю коллекцией объектов (Collection of Real как простой пример). Затем я определил итераторы в своей коллекции. Это означает: iterator, const_iterator, reverse_iterator и const_reverse_iterator. В этом примере я буду обращать внимание только на iterator и const_iterator, два других очень похожи.

После этого я хотел бы определить фильтр в моей коллекции, который хранит или не содержит элементы по отношению к определенному условию. Например, сохраняйте только экземпляры Real с положительным значением. Я также хотел бы перебирать свою коллекцию только на сохраненных элементах.

Как я реализовал коллекцию

В этом примере мой объект в коллекции очень прост. Цель состоит в том, чтобы иметь объект вместо собственного типа:

struct Real
{
    public:
      double r;
};

Затем я определяю свою коллекцию, не задумываясь о реальном контейнере внутри:

class Collection
{
  public:
    typedef std::vector<Real>::iterator iterator;
    typedef std::vector<Real>::const_iterator const_iterator;
  private:
    std::vector<Real> data;
  public:
    Collection() : data() {}
    Collection(unsigned long int n) : data(n) {}
    Collection(unsigned long int n, const Real& x) : data(n,x) {}
    Collection::iterator       begin()       { return this->data.begin(); }
    Collection::iterator       end()         { return this->data.end(); }
    Collection::const_iterator begin() const { return this->data.begin(); }
    Collection::const_iterator end() const   { return this->data.end(); }
};

Это очень хорошо работает в этом простом примере:

int main()
{
  Collection c(5);
  double k = 1.0;
  for(Collection::iterator it = c.begin(); it != c.end(); ++it)
  {
    it->r = k;
    k *= -2.0;
  }

  std::cout << "print c with Collection::iterator" << std::endl;
  for(Collection::iterator it = c.begin(); it != c.end(); ++it)
    std::cout << it->r << std::endl;

  std::cout << "print c with Collection::const_iterator" << std::endl;
  for(Collection::const_iterator it = c.begin(); it != c.end(); ++it)
    std::cout << it->r << std::endl;

  return 0;
}

И эта программа записывает ожидаемый результат:

print with Collection::iterator
1
-2
4
-8
16
print with Collection::const_iterator
1
-2
4
-8
16

Как я реализовал фильтр

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

class CollectionFilter
{
  private:
    Collection& col;
  public:
    CollectionFilter(Collection& c) : col(c) {}
    virtual ~CollectionFilter() {}
    Collection& collection() { return this->col; }
    iterator begin() { /* todo */ }
    iterator end() { /* todo */ }
    const_iterator begin() const { /* todo */ }
    const_iterator end() const { /* todo */ }
    virtual bool accept(const Real& x) const = 0;
};

Затем довольно легко создать новый фильтр, реализующий конкретное условие:

class CollectionFilterPositive : public CollectionFilter
{
  public:
    CollectionFilterPositive(Collection& c) : CollectionFilter(c) {}
    virtual ~CollectionFilterPositive() {}
    virtual bool accept(const Real& x) const { return x.r >= 0.0; }
};

Перед реализацией итераторов в фильтре у меня есть некоторые замечания/вопросы.

  • Этот фильтр работает на не-const Collection&, то действительно ли нужны функции begin() const и end() const? И если да, то почему?
  • Я не могу применить фильтр на const Collection&, но это явно необходимо для моей цели. Что может быть хорошим способом сделать это? Нужно ли дублировать класс CollectionFilter в классе CollectionFilterConst с очень похожим кодом? Более того, это решение довольно запутанно для пользователя, которому нужно наследовать два подобных класса.

Затем переходим к реализации итераторов. В этом примере я написал только iterator, а не const_iterator. Я добавляю это в свой класс:

class CollectionFilter
{
  public:
    class iterator
    {
      private:
        CollectionFilter*    filter;
        Collection::iterator iter;
      public:
                  iterator(CollectionFilter* f, Collection::iterator i) : filter(f), iter(i) {}
                  iterator(const iterator& i) : filter(i.filter), iter(i.iter) {}
        iterator& operator = (const iterator& i) { this->filter = i.filter; this->iter = i.iter; return *this; }
        iterator& operator ++ ()
        {
          if(this->iter != this->filter->collection().end())
          {
            do
            {
              ++this->iter;
            } while(this->iter != this->filter->collection().end() && !this->filter->accept(*this->iter));
          }
        }
        iterator operator ++ (int) { /* similar */ }
        Real& operator * () { return *this->iter; }
        Collection::iterator operator -> () { return this->iter; }
        bool operator == (const iterator& i) const { return this->iter == i.iter; }
        bool operator != (const iterator& i) const { return this->iter != i.iter; }
    };
  public:
    iterator begin()
    {
      Collection::iterator it = this->col.begin();
      if(!this->accept(*it)) ++it;
      return CollectionFilter::iterator(this,it);
    }
    iterator end()
    {
      Collection::iterator it = this->col.end();
      return CollectionFilter::iterator(this,it);
    }
};

Это также хорошо работает на этом простом примере

int main()
{
  Collection c(5);
  double k = 1.0;
  for(Collection::iterator it = c.begin(); it != c.end(); ++it)
  {
    it->r = k;
    k *= -2.0;
  }

  std::cout << "print c with CollectionFilterPositive::iterator" << std::endl;  
  CollectionFilterPositive fc(c);
  for(CollectionFilterPositive::iterator it = fc.begin(); it != fc.end(); ++it)
    std::cout << it->r << std::endl;

  return 0;
}

дает ожидаемый результат:

print with CollectionFilterPositive::iterator
1
4
16

Снова, некоторые вопросы:

  1. Я совершенно не согласен с этим?
  2. Я предполагаю, что мне нужно дублировать код CollectionFilter::iterator, чтобы реализовать CollectionFilter::const_iterator только с небольшими изменениями. Есть ли способ избежать дублирования этого кода (написано 8 раз, если я считаю дублированный класс CollectionFilterConst и обратные итераторы)?
  3. Я не чувствую себя комфортно с константой моего кода. Вы видите некоторые проблемы?

Спасибо заранее!

4b9b3361

Ответ 1

Я бы предложил отказаться от класса CollectionFilter и вместо этого иметь класс шаблона Collection::filter_iterator_tmpl с двумя экземплярами Collection::filter_iterator и Collection::const_filter_iterator.

Collection::filter_iterator_tmpl можно реализовать следующим образом:

class Collection {         
    template<typename Iterator, typename Predicate>
    class filter_iterator_tmpl :
    public std::iterator<std::input_iterator_tag, typename Iterator::value_type, typename Iterator::difference_type, typename Iterator::pointer, typename Iterator::reference> {
    private:
        Iterator underlying_iterator_;
        Predicate predicate_;

    public:
        filter_iterator_tmpl& operator++() {
            do {
                ++ underlying_iterator_;
            } while(! predicate_(*underlying_iterator_));
            return *this;
        }

        typename Iterator::reference operator*() const {
            return *underlying_iterator_;
        }

        ....
    }

};

Полиморфизм можно добавить, позволяя Predicate быть полиморфным функционалом с функцией virtual bool PolymorphicPredicate::operator(Real) const.

Collection будет определять фактические итераторы фильтра:

class Collection {
private:
    template<typename Iterator, typename Predicate>
    class filter_iterator_tmpl;
public:
    template<typename Predicate>
    using filter_iterator = filter_iterator_tmpl<Collection::iterator, Predicate>;

    template<typename Predicate>
    using const_filter_iterator = filter_iterator_tmpl<Collection::const_iterator, Predicate>;

    template<typename Predicate>
    filter_iterator<Predicate> begin_filter(const Predicate& pred);

    template<typename Predicate>
    const_filter_iterator<Predicate> begin_filter(const Predicate& pred) const;
}

Boost реализует общий "Итератор фильтра" аналогичным образом: http://www.boost.org/doc/libs/1_46_1/libs/iterator/doc/filter_iterator.html Как отдельный класс, а не как часть класса контейнера.

О const-correctness: Контейнеры в С++ (std::vector, std::map, std::string и т.д.) Владеют своими объектами контента: они создают и удаляют их, и им необходимо убедиться, что при доступе const к контейнеру вы также получаете доступ только к const объекты содержимого. Они должны быть реализованы таким образом, чтобы обеспечить соблюдение этого, поскольку базовые указатели, по которым они обращаются к выделенному хранилищу, не имеют этого понятия собственности. Вот почему они должны иметь две версии итераторов (iterator и const_iterator). Итераторы сами не владеют объектом: при доступе const к iterator вы не можете продвигать итератор, но вы все равно получаете неконстантный доступ к объекту.

Вопросы 1/2: CollectionFilter является проблематичным, поскольку он не владеет объектами, к которым он предоставляет доступ, но const/non-const доступ к фильтру должен давать только const/non-const доступ к объекту. Поскольку он содержит ссылку на Collection, и он должен работать как для const, так и для неконстантного доступа к Collection, тогда с этим подходом потребуется две версии CollectionFilter и ConstCollectionFilter.

Вопрос 4: После того, как есть расщепление от const-correct контейнерного объекта на два класса для доступа const и non-const, обязательно будет дублирование кода. Шаблоны не позволяют выполнять обе версии вручную. Есть также некоторые дополнительные осложнения, такие как сравнение iterator с const_iterator и построение a const_iterator из iterator, но не наоборот...

Вопросы 3/5: См. Выше.

Ответ 2

  • Этот фильтр работает не с const Collection&, то действительно ли нужны функции begin() const и end() const? И если да, то почему?
  • Я не могу применить фильтр на const Collection&, но это явно необходимо для моей цели. Что может быть хорошим способом сделать это? Нужно ли дублировать class CollectionFilter на class CollectionFilterConst с очень похожим кодом? Более того, это решение довольно запутанно для пользователя, которому нужно наследовать два подобных класса.

Эти вопросы очень взаимосвязаны. В принципе, имеет смысл ограничить фильтрацию неконстантным Collection? Для меня это не очень много. Мы вообще не изменяем объект CollectionFilter, а только базовые Collection объекты (потенциально), а функциональность Filter не зависит от того, является ли Collection const. Поместите это все вместе, и он вызывает шаблон:

template <typename C>
class Filter {
    static_assert(std::is_same<
                      std::decay_t<C>,
                      Collection
                  >::value, "Can only filter a Collection.");

    using collection_iterator = decltype(std::declval<C&>().begin());

    C& collection_;
public:
    Filter(C& collection) : collection_(collection) { }

    struct iterator { 
        /* TODO, use collection_iterator */
    };

    iterator begin() const { /* TODO */ };
    iterator end() const   { /* TODO */ };
};

Таким образом, Filter<Collection>::collection_iterator - Collection::iterator, а Filter<const Collection>::collection_iterator - Collection::const_iterator. И вы не можете сделать Filter<std::vector<int>>.

Такой ответ отвечает и на остальные ваши вопросы - это const -корректный, не дублированный подход к фильтрации любой коллекции.

Чтобы избежать дополнительной набрав, вы также можете создать функцию построения:

template <typename <typename> class F, typename C>
F<C> makeFilter(C& collection) {
    return F<C>(collection);
}

auto filter = makeFilter<CollectionFilterPositive>(some_collection);

const -ность итераторов Filter будет зависеть от const -ness some_collection.

Я также рассмотрел Boost.IteratorFacade для написания Filter::iterator, это сэкономит вам некоторое время и некоторые головные боли.