Reputation: 328
I'd like to choose the direction to iterate over a container at runtime, like in the following sample code:
#include <iostream>
#include <vector>
void iterate(bool forward, std::vector<int> v) {
auto start = v.begin();
auto end = v.end();
if (!forward) {
start = v.rbegin(); // this doesn't
end = v.rend(); // work
}
for (auto it = start; it != end; ++it) {
std::cout << *it << " -> " << *std::next(it) << std::endl;
}
std::cout << std::endl;
}
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
iterate(true, v);
iterate(false, v);
}
How can I change the iterator direction at runtime, without duplicating the for loop?
Imagine the loop being a rather complex algorithm that you don't want to duplicate to avoid future maintenance issues. Note that I need access to the std::next/std::previous element in the loop body.
Upvotes: 1
Views: 402
Reputation: 328
I'd like to show what I came up with after @Botje mentioned 'polymorphic lambdas'. I recently watched CppCon 2019: Arthur O'Dwyer “Back to Basics: Lambdas from Scratch” and I'm not quite sure if Arthur's 'Generic Lambdas' are what @Botje means, but the following code works just fine and is rather elegant IMHO:
#include <iostream>
#include <vector>
void iterate(bool forward, std::vector<int> v) {
auto l = [](auto start, auto end){
for (auto it = start; it != end; ++it) {
std::cout << *it << " -> " << *std::next(it) << std::endl;
}
};
if (forward) {
l(v.begin(), v.end());
} else {
l(v.rbegin(), v.rend());
}
std::cout << std::endl;
}
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
iterate(true, v);
iterate(false, v);
}
Upvotes: 0
Reputation: 275800
Some boilerplate. Some of the helper stuff has implementations in c++17/c++20 std library. Parts of this implementation requires c++17.
template<class It>
struct range_t {
It b, e;
It begin() const { return b; }
It end() const { return e; }
bool empty() const { return begin() == end(); }
// in a real implementation, this should check if the iterators are
// random access, and if so do the simple implementation. I don't
// want to use std::distance, because if n is small compared to the
// distance from begin to end, that could get expensive.
range_t except_last( std::size_t n = 1 ) const {
if (n==0 || empty()) return *this;
if (n==1) return { begin(), std::prev(end()) };
return except_last((n-1)/2).except_last(1+(n-1)/2);
}
};
template<class C>
auto range( C&& c ) {
using std::begin; using std::end;
return range_t{ begin(c), end(c) };
}
template<class C>
auto backwards( C&& c ) {
using std::rbegin; using std::rend;
return range_t{ rbegin(c), rend(c) };
}
template<class T>
struct index_iterator {
using difference_type = std::ptrdiff_t;
using value_type = T;
using reference = T;
T t;
T operator*() const { return t; }
T const* operator->() const { return std::addressof(t); }
index_iterator& operator++() { ++t; return *this; }
index_iterator operator++(int) { auto r = *this; ++t; r; }
friend bool operator==(index_iterator const& lhs, index_iterator const& rhs) {
return lhs.t==rhs.t;
}
friend bool operator!=(index_iterator const& lhs, index_iterator const& rhs) {
return lhs.t!=rhs.t;
}
};
template<class C>
auto iterators_into( C&& c ) {
using std::begin; using std::end;
return range_t{ index_iterator{begin(c)}, index_iterator{end(c)} };
}
template<class R, class Op>
void foreach_adjacent( R&& r, Op&& op ) {
for( auto it : iterators_into( r ).except_last() ) {
op( *it, *std::next(it) );
}
}
This is the result of having that boilerplate around:
void iterate(bool forward, std::vector<int> v) {
auto op = [](auto&& first, auto&& second) {
std::cout << first << " -> " << second << std::endl;
};
if (forward)
foreach_adjacent( v, op ):
else
foreach_adjacent( backwards(v), op );
}
what is going on:
range_t
is a wrapper for a non-owning range of elements using iterators.
backwards
and range
create forward and backwards range_t
s based off an input container or range.
index_iterator
is an input iterator over a range of iterators. With iterators_into
, it lets you simply loop over a range of a container with a for(:)
loop, but get the iterators not the elements at each step. (Note that index_iterator
can also accept std::size_t
instead of iterator
s; that is what I usually use it for. Both iterators and integers model the concepts it requires for its type argument.)
.except_last
is a helper method on range_t
that drops the last n
(default 1) elements from the range.
foreach_adjacent
iterates over the iterators of its input, dropping the last element, then calls op
on each element and the one after it (which is guaranteed to exist, because we dropped the last element).
In our iterate
function, we first write the body of the loop op
, then we either visit adjacent elements forwards or backwards, calling op
.
Upvotes: 2
Reputation: 180955
With a little bit of indirection, ie, using a helper function you can make a call to a template function that accepts the iterator types you want to loop with like
template<typename Iterator>
void iterate_helper(Iterator start, Iterator end)
{
for (auto it = start; it != end; ++it)
{
std::cout << *it << " -> " << *std::next(it) << std::endl;
}
std::cout << std::endl;
}
void iterate(bool forward, std::vector<int> v)
{
if (!forward)
iterate_helper(v.rbegin(), v.rend());
else
iterate_helper(v.begin(), v.end());
}
Just in case this example code is real code, do note that *std::next(it)
in the for loop is going to go one past the end of the container. Your end condition needs to stop one before end
to use it.
Upvotes: 7