How can I skip elements in a range-based for loop based on 'index'?
Asked Answered
A

9

14

Is there a way to access the iterator (I suppose there's no loop index?) in a C++11 range-based for loop?

Often we need to do something special with the first element of a container and iterate over the remaining elements. So I'm looking for something like the c++11_get_index_of statement in this pseudo-code:

for (auto& elem: container) 
{
  if (c++11_get_index_of(elem) == 0)
     continue;

  // do something with remaining elements
}

I'd really like to avoid going back to old-style manual iterator handling code in that scenario.

Ashlynashman answered 19/1, 2014 at 11:9 Comment(9)
Use the construct which provides the functionality you need. That is, use normal for loop (if that solves your problem easily). Don't force yourself doing complex things which need not to be complex.Anthology
use an STL algorithm with the preferred range. only fall back on a for loop if none of the algorithms fit.Elegant
@Nawaz Right, but how about using if (elem == container.front()) continue; as a workaround? Not overly complex, is it? And we can still use the concise syntax. Still better than 3 lines of manual iterator handling as in pre-C++11 IMHO..Ashlynashman
@Jay: don't, unless you want to pray each time that the container doesn't contain equal(==) elements.Elegant
@Jay: The correct way to do this is with iterators, not with indices (they aren't generic) or C++ algorithms (I don't believe any of them fit here). See Daniel's answer.Stoecker
@Ashlynashman Depending on the container and the condition for skipping, this could be unnecessarily expensive. It might also fail if there are duplicate elements in the container.Greenburg
@KarolyHorvath True.. I was thinking about containers containing pointers (99% use case in our code base)Ashlynashman
@Jay: Containers shouldn't contain raw pointers if they own them, because this isn't exception-safe and doesn't convey the semantics that the containers own the objects. Instead you should be using a pointer-container, like Boost's ptr_vector. Also if there's one thing you should learn with C++ is this: don't be lazy. I know iterators are painful, but they are the correct way to do this and will help you learn how to write robust generic code. Indices don't always make sense but iterators always do.Stoecker
@all Good discussion, thanks for all input and discussing various aspects!Ashlynashman
R
34

Often we need to do something special with the first element of a container and iterate over the remaining elements.

I am surprised to see that nobody has proposed this solution so far:

  auto it = std::begin(container);

  // do your special stuff here with the first element

  ++it;

  for (auto end=std::end(container); it!=end; ++it) {

      // Note that there is no branch inside the loop!

      // iterate over the rest of the container
  }

It has the big advantage that the branch is moved out of the loop. It makes the loop much simpler and perhaps the compiler can also optimize it better.

If you insist on the range-based for loop, maybe the simplest way to do it is this (there are other, uglier ways):

std::size_t index = 0;

for (auto& elem : container) {

  // skip the first element
  if (index++ == 0) {
     continue;
  }

  // iterate over the rest of the container
}

However, I would seriously move the branch out of the loop if all you need is to skip the first element.

Rook answered 19/1, 2014 at 11:59 Comment(2)
Definitely prefer the branching outside the loopAshlynashman
One drawback to this is increased verbosity when you're getting the container from a function that may return an empty list, because then you end up with essentially two checks on container sizeSquaw
V
6

Boost provides a nice succinct way to do this:

std::vector<int> xs{ 1, 2, 3, 4, 5 };
for (const auto &x : boost::make_iterator_range(xs.begin() + 1, xs.end())) {
  std::cout << x << " ";
}
// Prints: 2 3 4 5

You can find make_iterator_range in the boost/range/iterator_range.hpp header.

Vassallo answered 16/1, 2015 at 10:48 Comment(1)
Boost offers boost/range/adaptor/sliced.hpp which is even more convenient. Also, it is more descriptive than your solution and can be combined with other range adaptors. However, it works with indices so one often needs to query the size of the range which I don't like that much. See my answer for details.Telium
F
3

How about using a simple for loop with iteratos:

for(auto it = container.begin(); it != container.end(); it++)
{
    if(it == container.begin())
    {
        //do stuff for first
    }
    else
    {
        //do default stuff
    }
}

It's not range based, but it's functional. In case you may still want to use the range loop:

int counter = 0;
for(auto &data: container)
{
    if(counter == 0)
    {
        //do stuff for first
    }
    else
    {
        //do default stuff
    }
    counter++;
}
Flyover answered 19/1, 2014 at 11:20 Comment(1)
Those should be pre-incrementsWiry
G
2

No, you can't get the iterator in a range-based for loop (without looking up the element in the container, of course). The iterator is defined by the standard as being named __begin but this is for exposition only. If you need the iterator, it is intended that you use the normal for loop. The reason range-based for loop exists is for those cases where you do not need to care about handling the iteration yourself.

With auto and std::begin and std::end, your for loop should still be very simple:

for (auto it = std::begin(container); it != std::end(container); it++)
Greenburg answered 19/1, 2014 at 11:18 Comment(3)
Using post-increment here introduces pointless potential for overhead, especially for containers whose iterators are class types instead of pointers. I wish everyone could get out of the habit of defaulting to postinc/dec.Wiry
@Wiry It's called C++ for a reason right :P Can't the compiler just notice that the value isn't being used though?Doha
@Doha Yes, but it doesn't have to, especially with complex iterators that might have side-effects it doesn't analyse as being no-ops. And yes, it being called C++ is a tad unfortunate, but I doubt "++C" would've had the same ring to it, so here we are. ;-)Wiry
E
2

When iterating over elements, always prefer to use an algorithm, and use a plain for loop only if none of the algorithms fit.

Picking the right algorithm depends on what you want to do with the elements... which you haven't told us.

If you want to skip the first element, dump example:

if (!container.empty()) {
   for_each(++container.begin(), container.end(), [](int val) { cout << val; });
}
Elegant answered 19/1, 2014 at 11:35 Comment(2)
I get your point, though it's usually a bunch of things we do with our business objects so even lambdas could get biggish and out of hand. Still a problem with for_each when we need to skip handling for one of the elements, isn't it?Ashlynashman
If the skipping is based on the index, you're probably doing something wrong. Restructure your data.Elegant
E
1

I would try to avoid using iterators, because the idea of a range-based for loop is to get rid of them. As of C++20, to skip the first element in your container, I would take one of the following approaches. I also include, for the sake of completeness, how to handle the first element separately:

  1. Handling the first element outside the loop

    You can use container.front() which exists for all sequence containers to access the first element. However, you must make sure that the container is not empty to avoid a segmentation fault. Then, to skip the first element (or more) in the loop, you can use the range adapter std::views::drop from the Ranges library. All together it looks as follows:

    std::vector<int> container { 1, 2, 3 };
    
    if(!container.empty()) {
        // do something with first element
        std::cout << "First element: " << container.front() << std::endl;
    }
    
    for (auto& elem : container | std::views::drop(1)) {
        // do something with remaining elements
        std::cout << "Remaining element: " << elem << std::endl;
    }
    

    Instead of container.front() you can also use another range-based for loop together with the range adapter std::views::take(1). The advantage of take() and drop() is that they work safely even if their arguments exceed the count of elements in your container.

  2. Handling the first element inside the loop

    You can use an init-statement in a range-based for loop to define a Boolean flag (or even a counter). This way, the flag is visible only within the scope of the loop. You can use the flag inside the loop as follows:

    std::vector<int> container { 1, 2, 3 };
    
    for(bool isFirst(true); auto& elem : container) {
        if(isFirst) {
            // do something with first element
            std::cout << "First element: " << elem << std::endl;
            isFirst = false;
            continue;
        }
    
        // do something with remaining elements
        std::cout << "Remaining element: " << elem << std::endl;
    }
    

Output for both approaches shown:

First element: 1
Remaining element: 2
Remaining element: 3

Code on Wandbox

Eligibility answered 19/4, 2021 at 13:42 Comment(1)
If you can't use C++20 (yet) you may resort to Boost to achieve proposal 1. See my answer for more on that.Telium
C
0

There is no way of knowing how far an element is within the container without having an iterator, pointer or an intrusive index. Here's a simple way of doing it:

int index= 0;
for (auto& elem: container) 
{
  if (index++ == something)
     continue;

  // do something with remaining elements
}

If you want to skip the first element, another way is to use a std::deque and pop_front the first element. Then you can do your ranged for loop with the container as usual.

Chifley answered 19/1, 2014 at 11:24 Comment(0)
L
0

When I need to do something like this on a random access container, my habit is to iterate over the indexes.

for( std::size_t i : indexes( container ) ) {
  if (i==0) continue;
  auto&& e = container[i];
  // code
}

the only tricky part is writing indexes, which returns a range of what boost calls counting iterators. Creating a basic iterable range from iterators is easy: either use boost's range concept, or roll your own.

A basic range for an arbitrary iterator type is:

template<typename Iterator>
struct Range {
  Iterator b; Iterator e;
  Range( Iterator b_, Iterator e_ ):b(b_), e(e_) {};
  Iterator begin() const { return b; }
  Iterator end() const { return e; }
};

which you can gussy up a bunch, but that is the core.

Landowska answered 19/1, 2014 at 12:41 Comment(0)
T
0

Your question actually has several possible answers, and most of them can be answered with Boost.Range. These really come in handy if C++20's ranges are not (yet) an option, although they are a bit more clumsy in my opinion.

Adding an index to your range

To answer your direct question Boost offers the range adaptor boost::adaptors::indexed which you can use like so: (see it live on Coliru)

#include <iostream>
#include <vector>

#include <boost/range/adaptor/indexed.hpp>

int main() 
{ 
    std::vector const values{ 0, 1, 1 }; 

    for (auto const indexedValue : values | boost::adaptors::indexed{ 1 })
    {
        std::cout << indexedValue.index() << ": " << indexedValue.value() << ", ";
    }
}

Output is this:

1: 0, 2: 1, 3: 1, 4: 2, 5: 3, 6: 5, 7: 8, 8: 13, 9: 21, 10: 34, 

Dropping elements from a range

As others noted handling the special case outside the loop is a good idea. To drop leading or trailing elements Boost offers boost::adaptors::sliced. Again, I'll demonstrate it with an example: (see it live on Coliru)

#include <iostream>
#include <vector>

#include <boost/range/adaptor/sliced.hpp>

int main() 
{ 
    std::vector const values{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

    for (auto const value : values | boost::adaptors::sliced{ 2, values.size() - 1 })
    {
        std::cout << value << ", ";
    }
}

Here, I dropped the first two and the last elements, the output is this:

2, 3, 4, 5, 6, 7, 8, 

Combining range adaptors

The pipe syntax already suggests it, a range adaptor yields a range, so you can adapt it again: (see it live on Coliru)

#include <iostream>
#include <vector>

#include <boost/range/adaptor/indexed.hpp>
#include <boost/range/adaptor/sliced.hpp>

int main() 
{ 
    std::vector const values{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }; 

    for (auto const indexedValue : values
        | boost::adaptors::indexed{ 1 }
        | boost::adaptors::sliced{ 2, values.size() - 1 })
    {
        std::cout << indexedValue.index() << ": " << indexedValue.value() << ", ";
    }
}

yields the output

3: 1, 4: 2, 5: 3, 6: 5, 7: 8, 8: 13, 9: 21, 

Boost provides several other adaptors like reversed, indirected, transformed or fitlered to name just the really useful ones. They are worth a read.

Accessing the iterator

Lastly, your question starts with "Is there a way to access the iterator", and it turns out, there is. All we need to do™ is write our own range adaptor for this. This seems not really useful but it has helped me to refactor a very nasty raw loop from using iterator in the for statement to a range-based for loop. This made the code easier, you can immediately see that the loop actually visits each element in the range once but inspects the neighbors of the current element.

A simple solution can be achieved in 4 easy steps:

  1. Write a class template that wraps an iterator with a member function value() to dereference the wrapped iterator and a member function iterator() to retrieve a copy of the wrapped iterator
  2. Write an iterator template which contains the class from the previous step and yields a reference to it when dereferenced.
  3. Define a range type with boost::iterator_range using the iterator of the previous step.
  4. Write an operator|() factory and a tag class which selects the factory to convert a regular range into the adapted range type defined in the previous step.

The following solution works but may need some tweaking for edge cases. Also, it uses return type deduction of C++14 for the factory for simplicity, in C++11 a bit more work is needed. (see it live on Coliru)

#include <iostream>
#include <vector>

#include <boost/range/iterator_range.hpp>

template <typename Iterator>
class ValueAndIterator
{
public:
    using iterator_type = Iterator;
    using value_type = typename std::iterator_traits<iterator_type>::value_type;
    using reference = typename std::iterator_traits<iterator_type>::reference;

public:
    explicit ValueAndIterator(Iterator const iter)
        : m_iter{ iter }
    {}

    [[nodiscard]] reference value() const
    {
        return *m_iter;
    }

    [[nodiscard]] iterator_type iterator() const
    {
        return m_iter;
    }

private:
    Iterator m_iter{};
};

template <typename Iterator>
class IteratoredIterator
{
public:
    using base_iterator = Iterator;
    using difference_type = typename std::iterator_traits<base_iterator>::difference_type;
    using value_type = ValueAndIterator<base_iterator> const;
    using pointer = value_type *;
    using reference = value_type &;
    using iterator_category = std::forward_iterator_tag; // This is sufficient for the range-based for loop, this could be improved with some effort

public:
    IteratoredIterator() = default;

    explicit IteratoredIterator(base_iterator const iter)
        : m_valueIter{ iter }
    {}

    IteratoredIterator & operator++()
    {
        m_valueIter = ValueAndIterator<Iterator>{ ++m_valueIter.iterator() };
        return *this;
    }

    IteratoredIterator operator++(int)
    {
        IteratoredIterator result{ *this };
        ++*this;
        return result;
    }

    [[nodiscard]] reference operator*() const
    {
        return m_valueIter;
    }
    
    [[nodiscard]] pointer operator->() const
    {
        return &m_valueIter;
    }
    
    [[nodiscard]] base_iterator base() const
    {
        return m_valueIter.iterator();
    }

    [[nodiscard]] bool operator==(IteratoredIterator const other) const
    {
        return m_valueIter.iterator() == other.m_valueIter.iterator();
    }

    [[nodiscard]] bool operator!=(IteratoredIterator const other) const
    {
        return !operator==(other);
    }

private:
    ValueAndIterator<Iterator> m_valueIter;
};

constexpr class IteratoredTag {} Iteratored;

template <typename Iterator>
using IteratoredRange = boost::iterator_range<IteratoredIterator<Iterator>>;

template <typename Range>
[[nodiscard]] auto operator|(Range const & range, IteratoredTag)
{
    using iterator = decltype(std::begin(range));
    return IteratoredRange<iterator>{ IteratoredIterator<iterator>{ std::begin(range) },
        IteratoredIterator<iterator>{ std::end(range) } };
}

template <typename Range>
[[nodiscard]] auto operator|(Range & range, IteratoredTag)
{
    using iterator = decltype(std::begin(range));
    return IteratoredRange<iterator>{ IteratoredIterator<iterator>{ std::begin(range) },
        IteratoredIterator<iterator>{ std::end(range) } };
}

int main() 
{ 
    std::vector values{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

    for (auto const & value : values | Iteratored)
    {
        *value.iterator() *= 2;
        std::cout << value.value() << ", ";
    }
}

It uses the iterator to double each entry in values before printing the value. Consequently, the output it as follows:

0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 
Telium answered 5/6 at 20:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.