Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >.
Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==():
В этом коде используется костыль в виде дополнительной переменной isEnd_, которая устанавливается в true для конца первого контейнера (когда значения it2_ и it2end_ не определены). Это увеличивает размер данных под итератор и количество операций сравнения в шаге цикла for(;it!=itEnd; ). Можно ли данную задачу решить изящнее, без дополнительной переменной и дополнительного сравнения?
Составляя пример кода выше, я пропустил ещё одну переменную класса it1end_, без которой нельзя в operator++() определить наступление условия isEnd_. Хотя тогда можно попробовать обойтись без isEnd_ и применять такой оператор сравнения:
[ccode]
bool operator=(iterator it) const {
return it1_==it.it1_ && (it1_==it1end_ || it2_==it.it2_);
}
Тем не менее, при этом в итераторе по-прежнему нужно хранить четыре переменные и большинство сравнений в цикле for() для объединённого контейнера будет состоять из трёх сравнений. Что намного хуже с вычислительной точки зрения, чем реализация в виде вложенных циклов, где в таком случае нужно всего одно сравнение.
Здравствуйте, LightGreen, Вы писали:
LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >. LG>Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==():
просто не продвигайте итератор внутреннего вектора (it2_).
тогда он будет либо default-constructed для пустого контейнера, либо contents_.back().end()
Здравствуйте, Abyx, Вы писали:
A>Здравствуйте, LightGreen, Вы писали:
LG>>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >. LG>>Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==():
A>просто не продвигайте итератор внутреннего вектора (it2_). A>тогда он будет либо default-constructed для пустого контейнера, либо contents_.back().end()
хотя не, вру. default-constructed итераторы нельзя сравнивать, по кр.мере в MSVC.
тогда нужна статическая или глобальная переменная-вектор, чтобы у нее брать end() итератор, вместо default-constructed.
Здравствуйте, LightGreen, Вы писали:
LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >. LG>Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==():
LG>template <class T>
LG> class iterator
LG> {
LG> Outer::iterator it1_;
LG> Inner::iterator it2_, it2end_;
делай по-другому, храни два внешних итератора (текущий и конечный) и один внутренний -- текущий.
LG> iterator& operator++() {
LG> if(++it2_==it2end_) {
LG> ++it1_;
LG> it2_ = it1_->begin();
LG> it2end_ = it1_->end();
LG> }
здесь проблема. мало того что при it1 == end получишь неприятности, но и при пустом внутреннем контейнере разименуешь it2end_.
LG> }
LG> bool operator==(iterator it) const {
LG> return it1_==it.it1_ &&
LG> (isEnd_==it.isEnd_ || it2_==it.it2_);
здесь можно обойтись сравнением it2_==it.it2_
LG> }
LG> };
Здравствуйте, LightGreen, Вы писали:
LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >.
<>
Попробуем сделать наивную и ошибкоопасную реализацию такого итератора.
(Чтоб не путаться в терминах коллекций и элементов, представим себе, что имеем дело с текстом, строками и буквами).
template<class I, class J> struct jagged_iterator
{
I i; J j;
jagged_iterator(I i0, J j0) // при условии, что j0 относится к строке *i
{
i = i0; j = j0;
skip_empty_lines(); // возможно, что мы встали в конец первой строки, - нужно прыгнуть вперёд.
}
jagged_iterator operator++()
{
// если это инкремент, то мы заведомо не в конце текста и не в конце строки (стоим на какой-то букве)
++j; // прыгнем на одну букву
skip_empty_lines(); // и перепрыгнем, при необходимости, через конец строки и следующие пустые строки
}
void skip_empty_lines()
{
if(j==end(*i)) // если встали на конец строки,
{
do { ++i; } while(begin(*i)==end(*i)); // начнём прыгать по строкам, пока не найдём непустую
j = begin(*i); // и встанем на её начало
}
}
bool operator==(jagged_iterator const& other) const
{
return i==other.i && j==other.j;
}
decltype(*j) operator* () const { return *j; }
};
Очевидно, что он будет работать на непустых текстах, до последней буквы.
Инкремент с последней буквы приведёт нас к аварии.
Как на старте, так и на каждом инкременте нам нужно знать, где останавливаться.
Поэтому добавим итератор концевой строки, чтобы заведомо не вылететь за конец текста.
template<class I, class J> struct jagged_iterator
{
I i;
I ie; J j;
jagged_iterator(I i0, I iend, J j0) // при условии, что если i!=iend, j0 относится к строке *i
{
i = i0; ie = iend; j = j0;
skip_empty_lines();
}
jagged_iterator operator++()
{
++j;
skip_empty_lines();
}
void skip_empty_lines()
{
if(i!=ie && j==end(*i)) // обязательно нужны проверки на валидность i: здесь...
{
do { ++i; } while(i!=ie && begin(*i)==end(*i)); //... здесь, ...if(i!=ie) j = begin(*i); //... и здесь.
}
}
bool operator==(jagged_iterator const& other) const
{
// равенство тоже более хитроеreturn i==other.i && (i==ie ? other.i==other.ie || j==other.j);
}
decltype(*j) operator* () const { return *j; }
};
Наш итератор удовлетворяет разумному требованию: он либо указывает на букву, либо стоит на конце текста.
Встать куда-то между буквами он не может, хотя аргументы его конструктора вполне могут указать на такую позицию: начало любой пустой строки, или конец какой-нибудь непустой.
То есть, мысленные переводы строк мы выкинули.
Сделать это как-либо проще — не получится. Ну, разве что заменить итератор конца текста iend на количество строк до конца.
К этому же решению можно было подойти, если взять двухэтажный цикл и развернуть его
А теперь замечаем, что инициализация итератора — это код, выполняемый от Li0 до Lp0, разыменование — Lp0..Lp1, и инкремент — Lp1..Lp0.
Разыменуемый итератор пребывает в метке Lp0; итератор конца — в метке Li3.
То есть, наш итератор — это кортеж <i,j,is_Lp0_or_Li3,iend>.
Как ни крути, значение iend мы должны запомнить, потому что используем его в процедуре инкремента.
Флаг is_Lp0_or_Li3 принимает значение Lp0 при i!=iend и Li3 при i==iend, поэтому мы можем его не хранить, а подразумевать.
К сожалению, этот итератор неполноценен: он всегда стартует с начала текста и всегда завершается на конце текста.
Добавление возможности стартовать и завершаться в произвольной позиции — приведёт нас к тому, что я написал выше.
Всегда считал что концепция итераторов в namespace std вещь весьма убогая. Во всяком случае ограниченная сугубо векторами и непрерывными ranges.
Одна из проблем как раз и озвучена в данном топике — не всегда существует понятие end() как comparable сущности.
Вот пример генератора для данной задачи как альтернативы:
Здравствуйте, c-smile, Вы писали:
CS>Всегда считал что концепция итераторов в namespace std вещь весьма убогая. Во всяком случае ограниченная сугубо векторами и непрерывными ranges. CS>Одна из проблем как раз и озвучена в данном топике — не всегда существует понятие end() как comparable сущности.
Ну уж прямо не существует end. Он даже для istream_iterator существует, — как проективная точка +оо.
Если мы, инкрементируя итератор, диагностировали, что дальше некуда, — переводим его в сигнальное состояние.
Все сигнальные состояния между собой считаем неразличимыми.
Сложность только в том, что итератор получается однонаправленный, нельзя из конца отмотать. Ну так и генераторы — тоже однонаправленные.
Здравствуйте, LightGreen, Вы писали: LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу.
А какой итератор нужен? Input/Forward/Bidirectional?
Варианты:
1. Делать "в лоб". Тут придётся вручную кодировать автомат.
2. Сделать не InputIterator, а что-то range'е подобное — TrivialInputIterator:
// Refer http://ericniebler.com/2013/11/07/input-iterators-vs-input-ranges/ ,
// http://boost.2283326.n4.nabble.com/range-fat-iterators-td4654575.html#include <boost/iterator/iterator_facade.hpp>
#include <type_traits>
#include <iterator>
#include <iostream>
#include <cassert>
using namespace boost;
using namespace std;
template<typename TrivialInputIterator>
class InputRangeAdaptor
{
mutable TrivialInputIterator it;
// Cache during increment?
// Or maybe underlying iterator/range should do it by himself.
// iterator_traits?using reference = decltype(*it);
using value_type = typename std::remove_reference<reference>::type;
public:
class iterator: public iterator_facade
<
iterator, value_type, input_iterator_tag, reference
>
{
const InputRangeAdaptor *range = nullptr;
public:
iterator() = default;
explicit iterator(const InputRangeAdaptor &range)
: range{empty(range.it) ? nullptr : &range}
{}
private:
friend class boost::iterator_core_access;
void increment()
{
++range->it;
if(empty(range->it))
range = nullptr;
}
bool equal(iterator x) const
{
return range == x.range;
// Another option: move checks for emptiness here from increment.
// That would be faster for cases when we do not check for equality,
// i.e. std::copy_n.
// But such equality check will be more complicated and slower
}
reference dereference() const
{
return *range->it;
}
};
explicit InputRangeAdaptor(TrivialInputIterator it)
: it(it)
{}
iterator begin() const
{
return iterator{*this};
}
iterator end() const
{
return iterator{};
}
};
template<typename TrivialInputIterator>
InputRangeAdaptor<TrivialInputIterator> make_range(TrivialInputIterator x)
{
return InputRangeAdaptor<TrivialInputIterator>{x};
}
/*****************************************************************************/class NullTerminated // Models TrivialInputIterator
{
const char *position;
public:
explicit NullTerminated(const char *position)
: position{position}
{}
NullTerminated &operator++()
{
++position;
return *this;
}
const char &operator*() const
{
return *position;
}
friend bool empty(const NullTerminated &x)
{
return *x == 0;
}
};
/*****************************************************************************/#include <initializer_list>
#include <iterator>
#include <cassert>
#include <vector>
// NOT TESTED!template<typename Outer>
class JaggedIterator // Models TrivialInputIterator
{
Outer first, last;
using Inner = decltype(begin(*first)); // TODO: use iterator_traits
Inner inner_first;
using value_type = decltype(*inner_first);
void skip_empty()
{
assert(first != last);
while(inner_first == end(*first))
{
++first;
if(first == last) break;
inner_first = begin(*first);
}
}
public:
JaggedIterator(Outer first, Outer last)
: first(first), last(last)
{
if(first != last)
{
inner_first = begin(*first);
skip_empty();
}
}
JaggedIterator &operator++()
{
assert(inner_first != end(*first));
++inner_first;
if(first != last)
skip_empty();
return *this;
}
const int &operator*() const
{
return *inner_first;
}
friend bool empty(const JaggedIterator &x)
{
return x.first == x.last;
}
};
template<typename Outer>
auto make_jagged(Outer first, Outer last)
{
return JaggedIterator<Outer>{first, last};
}
int main()
{
using namespace std;
vector<vector<int>> x = {{}, {}, {1,2,3}, {}, {4, 5, 6}, {7,8}, {9}};
for(auto x : make_range(make_jagged(begin(x), end(x))))
cout << x << " ";
cout << endl;
}
3. Использовать stackful coroutines — получается проще всего, но дорого в runtime:
#include <boost/coroutine/all.hpp>
#include <initializer_list>
#include <iostream>
#include <vector>
int main()
{
using namespace std;
using namespace boost;
using namespace coroutines;
typedef coroutine<int> Coro;
vector<vector<int>> v = {{}, {}, {1,2,3}, {}, {4, 5, 6}, {7,8}, {9}};
Coro::pull_type coro([&](Coro::push_type &yield)
{
for(auto &inner : v)
for(auto &x : inner)
yield(x);
});
for(auto x : coro) // InputRange
cout << x << " ";
}
4. Использовать stackless coroutine. Выше уже показали один вариант, другой есть в Boost.Asio.
5. Банальный continuation passing style. Подойдёт если сами итераторы не нужны(они могут быть нужны для zip-а и т.п.), а требуется просто сделать for_each:
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <vector>
template<typename Range, typename F>
F for_each_jagged(Range &r, F yield)
{
for(auto &inner : r)
for(auto &x : inner)
yield(x);
return yield;
}
int main()
{
using namespace std;
vector<vector<int>> v = {{}, {}, {1,2,3}, {}, {4, 5, 6}, {7,8}, {9}};
for_each_jagged(v, [](auto x)
{
cout << x << " ";
});
}
6. Реализовать генератор на базе рекурсивных продолжений — самый дорогой и неуклюжий вариант.
Нужно скрестить монады List и Generator, добавить DO-сахар и получится:
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, c-smile, Вы писали:
CS>>Всегда считал что концепция итераторов в namespace std вещь весьма убогая. Во всяком случае ограниченная сугубо векторами и непрерывными ranges. CS>>Одна из проблем как раз и озвучена в данном топике — не всегда существует понятие end() как comparable сущности.
К>Ну уж прямо не существует end. Он даже для istream_iterator существует, — как проективная точка +оо. К>Если мы, инкрементируя итератор, диагностировали, что дальше некуда, — переводим его в сигнальное состояние. К>Все сигнальные состояния между собой считаем неразличимыми.
Извернуться это мы "завсегда могём". Только сколько оно того изврата требует. А инвалидация тех итераторов если контейнер изменился? И т.д.
Мой генератор из примера например stable к этому делу.
К>Сложность только в том, что итератор получается однонаправленный, нельзя из конца отмотать. Ну так и генераторы — тоже однонаправленные.
A>хотя не, вру. default-constructed итераторы нельзя сравнивать, по кр.мере в MSVC. A>тогда нужна статическая или глобальная переменная-вектор, чтобы у нее брать end() итератор, вместо default-constructed.
А разве сравнивать итераторы разных контейнеров можно?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
A>>хотя не, вру. default-constructed итераторы нельзя сравнивать, по кр.мере в MSVC. A>>тогда нужна статическая или глобальная переменная-вектор, чтобы у нее брать end() итератор, вместо default-constructed.
E>А разве сравнивать итераторы разных контейнеров можно?
сложный вопрос
вроде нельзя.
было предложение на запрет сравнения, но в стандарт это предложение вошло с невнятной формулировкой
Здравствуйте, LightGreen, Вы писали:
LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >. LG>Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==():
По хорошему мне кажется надо для этого ссылку на контейнер в итераторе держать. Как то так:
Здравствуйте, c-smile, Вы писали:
CS>Извернуться это мы "завсегда могём". Только сколько оно того изврата требует. А инвалидация тех итераторов если контейнер изменился? И т.д. CS>Мой генератор из примера например stable к этому делу.
А смотря как базовые итераторы устроены. Они тоже могут быть устойчивы к изменениям (использовать внутри ссылку на контейнер и смещение от начала).
Правда, для списков это дороговатое удовольствие, не?
К>>Сложность только в том, что итератор получается однонаправленный, нельзя из конца отмотать. Ну так и генераторы — тоже однонаправленные.
CS>Да фигня.
Да не фигня. Дело не в том, чтобы одним кодом покрыть оба порядка обхода, а в том, чтобы реализовать operator-- в дополнение к operator++ .
Здравствуйте, night beast, Вы писали:
NB>вроде нельзя. NB>было предложение на запрет сравнения, но в стандарт это предложение вошло с невнятной формулировкой
Если говорить о чистых указателях, то, вроде как, результат сравнения двух указателей внутрь разных блоков памяти не определён, хотя поведение только не специфицированное, то есть упасть не может, но вернуть может что угодно.
В частности, при сегментной модели памяти, вроде бы можно сравнивать только смещения внутри сегментов...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Если говорить о чистых указателях, то, вроде как, результат сравнения двух указателей внутрь разных блоков памяти не определён, хотя поведение только не специфицированное, то есть упасть не может, но вернуть может что угодно. E>В частности, при сегментной модели памяти, вроде бы можно сравнивать только смещения внутри сегментов...
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, Abyx, Вы писали:
A>>хотя не, вру. default-constructed итераторы нельзя сравнивать, по кр.мере в MSVC. A>>тогда нужна статическая или глобальная переменная-вектор, чтобы у нее брать end() итератор, вместо default-constructed.
E>А разве сравнивать итераторы разных контейнеров можно?
Здравствуйте, LightGreen, Вы писали:
LG>Подскажите, люди знающие, как грамотно сделать итератор для vector<vector<T> > так, чтобы внешняя семантика оставалась, как у итератора одного контейнера? Нужно обойти все элементы по одному разу. Типы контейнеров могут быть другими, это не принципиально, например vector<list<T> > или list<list<T> >. LG>Я пробовал делать по-разному, но всё время возникает одна и та же проблема: непонятно, как сравнивать итераторы в конце — что считать за общий end(). Если внешний it1_ упирается в свой end(), то значение внутреннего it2_ оказывается неопределённым. Поэтому появляются трудности с реализацией функции operator==(): LG>
LG>В этом коде используется костыль в виде дополнительной переменной isEnd_, которая устанавливается в true для конца первого контейнера (когда значения it2_ и it2end_ не определены). Это увеличивает размер данных под итератор и количество операций сравнения в шаге цикла for(;it!=itEnd; ). Можно ли данную задачу решить изящнее, без дополнительной переменной и дополнительного сравнения?
А вообще зачем в операторе сравнения сравнивать два итератора и it1_ и it2_?
По идее достаточно сравнить только it2_ == it.it2_? Не?
Судя по коду в operator++() у тебя отсутствует промежуточная ситуация, когда it1_ валидный, а it2_ == it2end_. Всё потому, что it2_ сразу перепрыгивает на начало следующего диапазона, когда достигается it2end_. То есть it2_ равен неоднозначному it2end_ только в одном состоянии — когда достигается конец контейнера. Неоднозначный он потому, что может содержать один и тот же указатель (в общем зависит от реализации STL, например NULL).
Поскольку, итератор — это указатель, то валидный итератор из одного диапазона не может оказаться равным валидного итератору из другого диапазона.
Получается что твой сборный итератор окажется равным end() тогда, когда it2_ == Inner->back().end() — это и есть end() твоего контейнера.
Остается поиграться c инициализацией этого итератора. Тут и возникает проблема с инициализацией end(), когда контейнер пустой.
НО, проблема ли это? Ты при пустой инициализации, можешь в Inner контейнер сувать пустой Outer. Таким образом наш Inner всегда содержит данные, даже в пустом состоянии, то есть его begin() всегда валидный, а значит всегда есть Inner->back().end(), который и является end()-итератором сборного контейнера.
Хотя, конечно, STL не оговаривает ситуацию сравнения итераторов из разных контейнеров. Но это совсем какой то экзотический STL.
Здравствуйте, c-smile, Вы писали:
CS>Всегда считал что концепция итераторов в namespace std вещь весьма убогая. Во всяком случае ограниченная сугубо векторами и непрерывными ranges.
А мне нравятся STL итераторы.
Кроме InputIterator, все остальные имеют хорошие примеры моделей, для которых итераторы являются эффективной абстракцией:
ForwardIterator — односвязный список
BidirectionalIterator — двусвязный список, UTF-8 строка
RandomAccessIterator — массивы и т.п.
OutputIterator — да хоть ostream_iterator<int>(cout)
InputIterator часто не имеет выраженного last, да и вообще last тут не особо уместен. InputIterator Это скорее адаптация InputRange к существующим алгоритмам, причём адаптация с небольшим overhead'ом.
Для InputRange нужно что-то типа:
с отдельными алгоритмами.
Но тем не менее, такой TrivialInputIterator всё же частично совместим с STL итераторами/алгоритмами. Например в случае Counted Range [first, n):
TrivialInputIterator first;
std::copy_n(first, n, out);