Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the idiomatic way to iterate a container while incrementing an integer index?

Suppose you want to know the numerical index of an element when iterating a container that doesn't offer random access iterators. For example:

std::list<std::string> items;
int i = 0;
for (auto & item : items) item += std::to_string(i++);

Is there a more idiomatic or nicer way of doing this? I presume this pattern comes up in various situations. I don't like the integer index being available outside of the loop. Bracketing the loop and the index definition in a local block seems ugly, too.

Of course when the container offers random access iterators, one can leverage the iterator difference, but then you can't use the range-for:

std::vector<std::string> items;
for (auto it = items.begin(); it != items.end(); ++it)
  *it += std::to_string(it - items.begin());

Although I only show a C++11 example, I'm looking for hints for C++0x and C++98 as well.

like image 289
Kuba hasn't forgotten Monica Avatar asked Jul 31 '14 13:07

Kuba hasn't forgotten Monica


5 Answers

My personal preference: just keep the extra index. It's clear as it is, and in case you ever have an if() inside the loop, you can also easily skip the count:

std::list<std::string> items;
{
    int i = 0;
    for (auto & item : items) {
        if (some_condition(item)) {
            item += std::to_string(i); // mark item as the i-th match
            ++i;
        }
    }
}

Just make sure to keep the i counter close near the loop, using extra { } to create a nested scope. Also, the post-increment is borderline unclear.

Alternatives: I would like a ranged-based index_for language construct that would provide an automatic counter i, but alas, that's not the case currently.

However, if you absolutely, positively, definitively insist on some nice wrapper, it's actually instructive to look at the semantics of your loop, which is that of a std::transform with a pair of std::list iterators and a boost::counting_iterator.

std::transform(
    begin(items), end(items), 
    boost::counting_iterator<int>(0), 
    begin(items), 
    [](auto const& elem, auto i) {
    return elem + std::to_string(i);    
});

This 4-legged overload of std::transform is sometimes called zip_with, which is why there are some comments to use a boost::zip_iterator with the list and counting_iterator.

You can make some nice range-based wrapper to be a lot more concise:

template<class Range, class T, class BinaryOp>
void self_transform(Range& rng, T value, BinaryOp op)
{
    auto i = value;
    for (auto& elem : rng) {
        elem = op(elem, i);        
        ++i;
    }
}

that can be more compactly called like:

self_transform(items, 0, [](auto const& elem, auto i) {
    return elem + std::to_string(i);    
});

Live Example.

like image 136
TemplateRex Avatar answered Oct 07 '22 05:10

TemplateRex


Some compilers already offer expressions with lambda captures that'll be in C++1y standard. So you could do this:

#include <string>
#include <list>
#include <iostream>

int main()
{
    std::list<std::string> items {"a","b","c","d","e"};

    //                 interesting part here, initializes member i with 0, 
    //          ˇˇˇˇˇˇ type being deduced from initializer expression            
    auto func = [i = 0](auto& s) mutable { s+= std::to_string(i++); };
    for (auto& item : items) func(item);

    for (const auto& item : items) std::cout << item << ' ';
}

Output: a0 b1 c2 d3 e4

EDIT: For the record, I do think that having a little index variable outside of the scope of the loop is the best here (see other answers). But for fun, I wrote an iterator adaptor (with the help of Boost Iterator Adaptor) that you can use to tie a member function index to any iterator:

#include <boost/iterator/iterator_adaptor.hpp>
#include <list>
#include <string>
#include <iostream>
#include <algorithm>

// boiler-plate

template<typename Iterator>
class indexed_iterator
: public boost::iterator_adaptor<indexed_iterator<Iterator>, Iterator>
{
public:
    indexed_iterator(Iterator it, int index_value = 0)
    : indexed_iterator::iterator_adaptor_(it)
    , i_(index_value)
    { }

private:
    int i_;

    friend class boost::iterator_core_access;
    void increment(){ ++i_; ++this->base_reference(); }

    /* TODO: decrement, etc. */

public:
    int index() const { return i_; }
};

template<typename Iterator>
indexed_iterator<Iterator> make_indexed_iterator(Iterator it, int index = 0)
{
    return indexed_iterator<Iterator>(it, index);
}

// usuable code

int main()
{
    std::list<std::string> items(10);

    auto first = make_indexed_iterator(items.begin());
    auto last  = make_indexed_iterator(items.end());
    while (first != last) {
        std::cout << first.index() << ' ';
        ++first;
    }
}

Output: 0 1 2 3 4 5 6 7 8 9

like image 37
jrok Avatar answered Oct 07 '22 05:10

jrok


I would probably end up with something like this:

std::list<std::string> items = ...;

{
    int index = 0;
    auto it = items.begin();
    for (; it != items.end(); ++index, ++it)
    {
        *it += std::to_string(index);
    }
}

I've seen way more uses of for loops with two loop variables than I have seen uses of zipped iterators or lambda capture counted variables. "Idiomatic" is a subjective term, but I would call this idiomatic.

Having an explicit extra variable makes it obvious that we are just counting upward. This is important if you decide to do anything non-trivial in the loop. For example, you can insert or remove an item in the list and adjust the index accordingly—if you were using an iterator adapter, it might not even be obvious that the index it provides might not actually be the index of the item in the container.


Alternately, you could write a variant of std::for_each:

template <typename InputIt, typename BinaryFn>
BinaryFn for_each_index(InputIt first, InputIt last, BinaryFn fn)
{
    for (int i = 0; first != last; ++i, ++first)
    {
        fn(i, *first);
    }
    return fn;
}

which at least isn't obfuscated. Then you can do this:

std::list<std::string> items = ...;
for_each_index(items.begin(), items.end(), [](int i, std::string& s)
{
    s += std::to_string(i);
});
like image 20
John Calsbeek Avatar answered Oct 07 '22 04:10

John Calsbeek


Well using Boost.Range you can do this:

std::list<std::string> items;
for (const auto & t : boost::combine(items, boost::irange(0, items.size()))) 
{
    std::string& item = boost::get<0>(t);
    int i = boost::get<1>(t);
    item += std::to_string(i);
}
like image 20
Paul Fultz II Avatar answered Oct 07 '22 05:10

Paul Fultz II


There is a little library called pythonic, which gives you the enumerate() function, you might know from python, in C++. It creates a list of pairs with index and value. You then iterate over this list instead. It let you do the following (from their documentation):

#include <vector>
#include <iostream>
#include "pythonic/enumerate.h"
using namespace pythonic;

// ...

typedef std::vector<int> vec;

for (auto v : enumerate(vec {0, -1337, 42}))
{
    std::cout << v.first << " " << v.second << std::endl;
}

// ...

Which gives you as output

$ ./enumerate
0 0
1 -1337
2 42
$
like image 3
hildensia Avatar answered Oct 07 '22 05:10

hildensia