在递增整数索引的同时迭代容器的惯用方法是什么?

时间:2014-07-31 13:31:26

标签: c++ c++11 stl iteration

假设您想要在迭代不提供随机访问迭代器的容器时知道元素的数字索引。例如:

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

是否有更惯用或更好的方法?我认为这种模式会出现在各种情况下。我不喜欢在循环之外可用的整数索引。包围循环和本地块中的索引定义似乎也很难看。

当然,当容器提供随机访问迭代器时,可以利用迭代器差异,但是你不能使用range-for:

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

虽然我只展示了一个C ++ 11示例,但我也在寻找C ++ 0x和C ++ 98的提示。

5 个答案:

答案 0 :(得分:30)

我的个人偏好:只需保留额外的索引。 很清楚,因为它是,如果你在循环中有if(),你也可以轻松跳过计数:

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;
        }
    }
}

只需确保i计数器靠近循环,使用额外的{ }来创建嵌套范围。此外,后增量是边界不清楚。

替代方案:我想要一个基于范围的index_for语言构造,它将提供一个自动计数器i,但是,目前情况并非如此。

但是,如果你绝对,积极地,明确地坚持一些不错的包装器,那么查看循环的语义实际上是有益的,这是具有一对std::transform迭代器的std::list的语义。和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);    
});

std::transform的4段重叠有时称为zip_with,这就是为什么有boost::zip_iteratorlist和{{1}一起使用counting_iterator的注释的原因}。

你可以使一些不错的基于范围的包装器更加简洁:

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;
    }
}

可以更紧凑地称为:

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

Live Example

答案 1 :(得分:10)

有些编译器已经提供了带有lambda捕获的表达式,这些表达式将采用C ++ 1y标准。所以你可以这样做:

#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 << ' ';
}

输出:a0 b1 c2 d3 e4

编辑:对于记录,我确实认为在循环范围之外有一个小索引变量是最好的(参见其他答案)。但为了好玩,我编写了一个迭代器适配器(在Boost Iterator Adaptor的帮助下),您可以使用它将成员函数index绑定到任何迭代器:

#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;
    }
}

输出:0 1 2 3 4 5 6 7 8 9

答案 2 :(得分:6)

我可能最终得到这样的东西:

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

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

我已经看到更多使用带有两个循环变量的for循环,而不是使用zipped迭代器或lambda捕获计数变量。 “惯用语”是一个主观术语,但我称之为惯用语。

有一个明确的额外变量使它明显我们只是向上计数。如果您决定在循环中执行任何非平凡的操作,这一点很重要。例如,您可以在列表中插入或删除项目并相应地调整索引 - 如果您使用的是迭代器适配器,则它提供的索引实际上可能实际上并不是容器中项目的索引


或者,您可以编写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;
}

至少不会混淆。然后你可以这样做:

std::list<std::string> items = ...;
for_each_index(items.begin(), items.end(), [](int i, std::string& s)
{
    s += std::to_string(i);
});

答案 3 :(得分:5)

使用Boost.Range,你可以这样做:

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);
}

答案 4 :(得分:2)

有一个名为pythonic的小库,它为你提供了enumerate()函数,你可能从python中知道,在C ++中。它创建了一个包含索引和值的对列表。然后,您将遍历此列表。它允许您执行以下操作(来自他们的文档):

#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;
}

// ...

它为您提供输出

$ ./enumerate
0 0
1 -1337
2 42
$