Jonathan Boccara's blog

How to Access the Index of the Current Element in a Modern For Loop

Published October 26, 2018 - 15 Comments

Daily C++

 

For loops have evolved over the years, starting from the C-style iterations to reach the range-based for loops introduced in C++11.

But the later, modern, versions of the for loop have lost a feature along the way: the possibility to access the index of the current element in the loop.

Indeed, consider this rather old-style loop:

std::vector<X> collection = //...

for (size_t i = 0; i < collection.size(); ++i)
{
    // accessing an element with the syntax: collection[i]
    // ...
}

The iteration mechanism doesn’t look very modern because it doesn’t use iterators nor ranges, and i is kind of an awkward name, but it has an advantage: you always know the position of the current element: it’s i.

With C++98, iterators came along, and allowed to write this (if we simplify it with auto, that only came in C++11):

std::vector<X> collection = //...

for (auto element = collection.begin(); element != collection.end(): ++element))
{
    // accessing an element with the syntax: *element
}

It is more complicated, but has the advantage of working for containers that don’t have an operator[], such as std::map and std::set for example.

Nevertheless for an std::vector, it’s not such a good deal because the convoluted syntax doesn’t bring anything and loses the direct access the current position.

And in C++11 came range-based for loops, with their expressive syntax:

std::vector<X> collection = //...

for (auto const& element : collection)
{
    // accessing an element with the direct syntax: element
}

It’s much simpler than anything before. But it still doesn’t give access to the current index.

How can we use a modern for loop and get access to the index of the current element?

Do you really need the current position?

Before seeing how to retrieve it, it’s worth making sure that we do need the current position. Indeed, an iteration that manipulates both the contents and the structure of a collection is a relatively complex one. And making complex iterations expressive is difficult.

Complex for loops are hard to read, and therefore can hide bugs pretty easily. And if they don’t have a bug, they’re only waiting for one to happen when somebody tries to modify it.

According to Steve McConnell reference book Code Complete, software development is all about managing complexity and change. So there are ways to work around that complexity. One of them is to break it down into manageable pieces that encapsulate the complexity.

This is exactly what STL algorithms are meant to do, for operations on collections. They encapsulate the complexity of iterations behind a simple iterface. So maybe what you’re trying to achieve that needs the position could be done better with one or a combination of STL algorithms.

That said, there are cases where you do need the current position. Let’s take the simple example of a program that should read through a collection of strings and print out each, preceded by its index (starting at 1) in the collection. So for the following input:

std::vector<std::string> words = {"Bella", "ciao", "Bella", "ciao", "Bella", "ciao", "ciao", "ciao"};

We want to output this:

1 - Bella
2 - ciao
3 - Bella
4 - ciao
5 - Bella
6 - ciao
7 - ciao
8 - ciao

A pretty simple code to write would be:

for (size_t i = 0; i < words.size(); ++i)
{
    std::cout << (i + 1) << " - " << words[i] << '\n';
}

But does this code work all the time? Are there other alternatives?

Boost indexed

Boost indexed is part of the Boost Ranges library. Boost Ranges are the precursor of ranges in C++, which the STL is steering towards.

Assuming you’re familiar with what a range adaptor is, consider the indexed range adaptor:

myRange | boost::adaptors::indexed(0)

It takes an initial value (here, 0), and plugs itself onto a range to produce a new range. The new range contains the values of the initial one, plus an index for each position. Those indexes are equal to the position in the range + an offset equal to the initial value (here, 0).

Let’s adapt our code with it:

using namespace boost::adaptors;

for (auto const& word : words | indexed(0))
{
    std::cout << (word.index() + 1) << " - " << word.value() << '\n';
}

No trace of the old awkwardly-named i. We can now explicitly access the value or the index of the element.

And since we want to produce the values starting with an index at 1, let’s take advantage of the offset that the adaptor offers:

using namespace boost::adaptors;

for (auto const& word : words | indexed(1))
{
    std::cout << word.index() << " - " << word.value() << '\n';
}

Here is an example of a runnable program using indexed.

Boost Ranges came out a while ago, and a more modern ranges library today is range-v3. range-v3 has most of Boost Ranges’ features, plus a lot more. So I’m assuming that range-v3 must have an equivalent of Boost indexed, but I wasn’t able to find it. If anyone knows, please tell me in a comment!

Working with iterators

Not everyone gets to use Boost in their project, for various reasons. If you can’t use Boost, one solution is to revert to the old style with our friend i.

But in the cases where you can’t access the ith element of a container, this old technique won’t work. Consider the example of a std::set:

std::set<std::string> words = {"Bella", "ciao", "Bella", "ciao", "Bella", "ciao", "ciao", "ciao"};

for (size_t i = 0; i < words.size(); ++i)
{
    std::cout << (i + 1) << " - " << words[i] << '\n';
}

The above code doesn’t compile, because there is no operator[] on an std::set. What to do to access the position of the current element in the iteration?

One solution could be to maintain an index incremented at each cycle of the loop. But I find this awkward and risky because that’s a technical variable sticking out in the business logic inside of the loop:

int i = 0;
for (auto const& word : words)
{
    std::cout << (i + 1) << " - " << word << '\n';
    ++i;
}

But this produces the correct output:

1 - Bella
2 - ciao

(it’s a set, so elements are unique.)

In terms of algorithmic complexity, it has O(n) increments of the index.

There is another way, that leads to code that looks more like Boost indexed, but at the expense of more increments of the index: O(n2) increments. You may find this acceptable (for small collections maybe) or not.

It consists in creating a function that deduces the position based on the distance from the beginning of the collection:

std::set<std::string> words = {"Bella", "ciao"};

auto index = [&words](auto const& iterator)
             {
                 return std::distance(begin(words), iterator);
             };
                   
for (auto word = begin(words); word!= end(words); ++word)
{
    std::cout << (index(word) + 1) << " - " << *word << '\n';
}

This looks like a lot of code, so let’s encapsulate it. We can use the technique of encapsulating the lambda to make code more expressive, and making it a template to make it more generic:

template<typename Collection>
auto getIndex(Collection const& collection)
{
    return [&collection](auto const& iterator)
    {
        return std::distance(begin(collection), iterator);
    };
}

Which makes our calling code look like this:

std::set<std::string> words = {"Bella", "ciao"};

const auto index = getIndex(words);
                   
for (auto word = begin(words); word!= end(words); ++word)
{
    std::cout << (index(word) + 1) << " - " << *word << '\n';
}

Now that we have a getIndex interface, let’s add to it the possibility to specify an offset, like with Boost indexed:

template<typename Collection>
auto getIndex(Collection const& collection, size_t offset = 0)
{
    return [&collection, offset](auto const& iterator)
    {
        return offset + std::distance(begin(collection), iterator);
    };
}

This simplifies the calling site:

std::set<std::string> words = {"Bella", "ciao"};

const auto index = getIndex(words, 1);
                   
for (auto word = begin(words); word!= end(words); ++word)
{
    std::cout << index(word) << " - " << *word << '\n';
}

It looks a little more like indexed, but there can be many iterator increments. Indeed, for containers that don’t have random iterators, such as std::map and std::set for example, std::distance is linear. And as it’s called for every element of the collection, overall this makes a quadratic number of iterator increments.

You will find a runnable version of the above examples here. All your feedback is welcome.

EDIT: as pointed out in the comment section, a great alternative is to use a custom iterator that provides the current index. You can read about this on Therocode’s blog.

You may also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed