Traversing a bidirectional iterator back to the first element

118 views Asked by At

There is something that I can't quite get my head around with bidirectional iterators.

Going forward through the collection is ok:

auto it = set.begin();
while (it != set.end())
{
  // Do something
  it++;
}

end() is the element after the last element. So let's say I want to go in reverse using the same iterator from the end:

if(it == set.end())
{
    it--; // Now points to last element
}
while(it != set.begin())
{
  // Do something
  it--;
}

But this last loop is flawed because begin() is the first element and we want to include it in processing. So how do we iterate back to (and including) the first element with a bidirectional iterator?


To provide context ...

The user iterates forward one by one. So they might be on the end() or any element before the end. Then, they decide they want to go backwards. Thus the sanity check since if we are at the end we must decrement by one. Now the user goes backwards one by one. Until they reach the first element.

I asked a similar question yesterday but deleted it since I did not grasp some of the concepts which I now have corrected for forward iteration.

3

There are 3 answers

5
Nicol Bolas On BEST ANSWER

Before starting this process, you need to be clear on what it is supposed to be. Your if statement suggests that you're not clear on that.

If it is supposed to point at the first element to be processed in the sequence, then the only way it will ever be the end element is if the list is empty (ie: end and begin are the same). In that case, decrementing the iterator is wrong.

To handle this case, your code should look like this:

if(it != set.end())
{
  for(; it != set.begin(); --it)
  {
    //Do stuff with *it.
  }
}

If it is supposed to point to the element after the first element to process, then your code would look like this:

while(it != set.begin())
{
  --it;
  //Do stuff with *it
}

The user iterates forward one by one. So they might be on the end() or any element before the end. Then, they decide they want to go backwards.

That user needs to figure out what they mean. When they provide the iterator to the code doing reverse processing, they are the ones who need to be clear on what the value of that iterator means. It either is always part of the range, or it never is part of the range.

It shouldn't be "it's part of the range, unless its the end iterator, then it isn't part of the range".

6
Vlad from Moscow On

It is easy to do, for example:

while(it != set.begin())
{
  // Do something with --it
}

Here is a demonstration program:

#include <iostream>
#include <set>
#include <iterator>

int main( 
{
    std::set<int> set = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

    auto it = std::cbegin( set );

    while ( it != std::cend( set ) )
    {
        std::cout << *it++ << ' ';
    }
    std::cout << '\n';

    while ( it != std::cbegin( set ) )
    {
        std::cout << *--it << ' ';
    }
    std::cout << '\n';
}

The program output is:

1 2 3 4 5 6 7 8 9 10 
10 9 8 7 6 5 4 3 2 1 

That is after the first while loop the iterator is equal to the iterator std::end( set ). It does not equal to std::begin( s ).

So in the second while loop you may write the condition:

while ( it != std::begin( set ) )

and within the loop the iterator is decremented to point to a valid element.

Just consider a set that contains only one element, for example:

    std::set<int> set = { 1 };

and investigate the program.

I is the same as for example to output a string in the reverse order using indices.

const char *s = "Hello World!";

for ( size_t n = std::strlen( s ); n != 0; )
{
    std::cout << s[--i];
}
std::cout << '\n';
1
Barry On

The correct way to efficiently traverse a range in reverse is:

auto it = v.end();
while (it != v.begin()) {
    --it;
    use(*it);
} 

If v is empty, then the loop is never entered, since v.end() == v.begin().

If v contains elements, then initially v.end() points one past the end, so we first decrement it (now pointing to the last element) before using it. The first call to use will be the last element.

When we decrement all the way to the first element, such that use(*it) is using the first element, that means that it == v.begin(), so we don't enter the loop again.

This is notably different from the structure of a for loop, since we decrement at the beginning of the body rather than at the end.

In C++20, you can simply avoid thinking about how to structure the loop entirely by writing:

for (auto elem : std::views::reverse(v)) {
    use(elem);
}

Which basically does:

auto it = std::make_reverse_iterator(v.end());
auto end = std::make_reverse_iterator(v.begin());
for (; it != end; ++it) {
    use(*it);
}

Which desugars roughly into:

auto it = v.end();
auto end = v.begin();
for (; it != end; --it) {
    auto this_elem = std::prev(it);
    use(*this_elem);
}

Note that this two decrements per element instead of one in order to give you the convenient syntax. This usually doesn't matter for most problems, and the convenient syntax is trivial to write correctly, whereas you (or, at least, I) really have to think a bit to write the correct loop reversal syntax correctly (i.e. not either run past or skip the first element).