迭代容器并增加整数索引的惯用方法是什么?

31

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

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

有没有更符合习惯或更好的方法来做这件事?我认为在各种情况下都会出现这种模式。我不喜欢整数索引在循环外可用。将循环和索引定义括在局部块中似乎也很丑。

当容器提供随机访问迭代器时,可以利用迭代器差异,但然后就不能使用范围-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的提示。


4
对于“insanity”,boost库提供了计数迭代器和zip迭代器:使用zip函数将元素的数量与元素范围进行打包,然后迭代该序列,并提取每个元素及其索引。遗憾的是,结果不太美观。 - Yakk - Adam Nevraumont
2
我不知道这个类似问题的答案有多么“惯用”,但它肯定非常聪明。 - Sergey Kalinichenko
15
请模仿 Dr. Evil 的语气,翻译以下代码:for(auto z = std::make_pair(items.begin(), 0); z.first != items.end(); ++z.first, ++z.second) {}像邪恶博士一样,用这段代码:for(auto z = std::make_pair(items.begin(), 0); z.first != items.end(); ++z.first, ++z.second) {} - Captain Obvlious
@CaptainObvlious 这应该是一个答案。 - Slava
一则小提示,特别重要,因为您关心习惯用法代码:请使用std::size_t代替int--它更能表达意图(非负大小-与任意/可能为负的整数相反),更加普及和习惯化(C和C++ std库都将其用于计数、索引和大小),并且保证具有足够大的存储空间(标准仅保证int可以表示16位宽度的数字,对于有符号类型而言,即值高达32767:en.cppreference.com/w/cpp/language/types#Integer_types)。 - Matt
显示剩余5条评论
5个回答

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::list 迭代器和一个 boost::counting_iteratorstd::transform

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

这个有4个参数的std::transform有时被称为zip_with,因此有一些评论建议使用boost::zip_iteratorlistcounting_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);    
});

实时示例


这并不糟糕,只是有点让人恼火 :) - Kuba hasn't forgotten Monica
13
随便你怎么踩,但是循环中的任何 "i" 对于任何读者都非常清晰明了,而一些 "std::distance" 或 zip 迭代器则是没有必要的混淆。 - TemplateRex
1
@jrok 我不知道,我看不出那个无辜的小 i 的问题。会不惜一切代价维护它(如果没有其他办法,我会得到一个同行压力徽章,耶!) - TemplateRex
3
如果你非常想要那个徽章,我可以取消点赞。 :P - jrok
我也更喜欢那个版本,即使有一个额外的块作用域,如果在野外使用 i 是一个问题,也比一些完全混淆你正在做什么的复杂代码更好(特别是 zip 迭代器,它们带回了完全失去读者并增加了更多输入以获得...什么?内部化索引?通过在循环+索引周围添加一个 {},您可以获得相同的结果)。 - JBL
@KubaOber 加入了一些替代方案,希望你会觉得不那么烦人;-) - TemplateRex

10

一些编译器已经提供了在C++1y标准中将包含lambda捕获的表达式。所以你可以这样做:

#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 迭代器适配器 的帮助),您可以使用它将成员函数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


4
啊,那个看似无害的小小的 func(item) 调用。然而它却改变了 funcitem 两者的状态。我希望这种做法不会变成惯用写法... - John Calsbeek

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

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

3

有一个小型库叫做pythonic,它在C++中提供了你在Python中熟知的enumerate()函数。它创建了一组索引和值的配对列表。之后,您可以遍历这个列表。根据文档,它使您能够执行以下操作:

#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
$

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接