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
Снова, некоторые вопросы:
- Я совершенно не согласен с этим?
- Я предполагаю, что мне нужно дублировать код
CollectionFilter::iterator
, чтобы реализовать CollectionFilter::const_iterator
только с небольшими изменениями. Есть ли способ избежать дублирования этого кода (написано 8 раз, если я считаю дублированный класс CollectionFilterConst
и обратные итераторы)?
- Я не чувствую себя комфортно с константой моего кода. Вы видите некоторые проблемы?
Спасибо заранее!
Ответы
Ответ 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
, это сэкономит вам некоторое время и некоторые головные боли.