template<class It>
struct indexing_iterator {
It it = {};
using value_type = It;
using reference = value_type;
It operator*() const { return it; }
indexing_iterator& operator++() { ++it; return *this; }
friend bool operator==( indexing_iterator const& lhs, indexing_iterator const& rhs ) {
return lhs.it == rhs.it;
}
friend bool operator!=( indexing_iterator const& lhs, indexing_iterator const& rhs ) {
return !(lhs==rhs);
}
};
template<class F, class It_in>
struct transform_iterator_t:indexing_iterator<It_in> {
F f = {};
using reference = decltype(std::declval<F&>()( *std::declval<It_in&>() ) );
using value_type = std::decay_t<reference>;
using pointer = value_type*;
reference operator*() { return f(*this->it); }
transform_iterator_t( F f_in, It_in it_in ):
indexing_iterator<It_in>{it_in},
f(f_in)
{}
};
template<class F, class It_in>
transform_iterator_t<F,It_in> transform_iterator(
F f, It_in it
) {
return {f, it};
}
indexing_iterator<std::size_t> index( std::size_t n ) {
return {n};
}
template<class It>
struct range_t {
It b,e;
It begin() const { return b; }
It end() const { return b; }
};
template<class It>
range_t<It> range( It b, It e ) { return {b,e}; }
auto range_upto( std::size_t n ) {
return range( index(0), index(n) );
}
template<class C>
auto to_range( C&& c ) {
using std::begin; using std::end;
return range( begin(c), end(c) );
}
template<class It, class F>
range_t< transform_iterator< F, It > >
transform_range( F f, range_t<It> range ) {
return {
transform_iterator( f, range.begin() ),
transform_iterator( f, range.end() )
};
}
template<class F>
auto generator_range( F f, std::size_t count ) {
return transform_range( f, range_upto(count) );
}
template<class C1, class C2>
auto zip_over( C1& c1, C2& c2 ) {
return generator_range(
[&](std::size_t i) {
return std::tie( c1[i], c2[i] );
},
(std::min)(c1.size(), c2.size())
);
}
then in C++17 we have
void Foo::DoStuff() {
for(auto&&[val1, val2] : zip_over(mBarInt, mBarDouble)) {
}
}
or you can manually unpack the tuple in C++14.
Seems like a lot of work.
There are implementations of zip and the like in boost
and other libraries, including Rangev3.
Code not tested. Design should be sound.
There is probably a shorter way to do it more directly, but I find generator via transform and indexing, and zip via generator, easier to understand and write with fewer bugs.
An indexing_iterator
takes an incrementable comparable object, and iterates over it. The It
can be an integer-like or an interator.
A transform_iterator
takes an iterator and a transformation, and returns the transformation on each element.
Neither are full iterators; I skipped the boilerplate. But they are enough for for(:)
loops.
A range
holds 2 iterators and exposes them to for(:)
.
An indexing_iterator<std::size_t>
is otherwise known as a counting iterator.
A generator iterator is a transformation of a counting iterator. It takes a function that takes an index, and generates an element for that index.
We zip over two random access containers by generating a generator iterator that calls operator[]
on both, then returns a tuple. Fancier zipping handles non-random access containers.
The zip of two containers then lets you iterate in parallel over two different containers.
I then use C++17 structured bindings in the for(:)
statement to unpack them into two different variables. We could instead just manually unpack as the first 2 statements in our for loop.