Каков идиоматический способ итерации контейнера при приращении целочисленного индекса?

Предположим, вы хотите знать числовой индекс элемента при итерации контейнера, который не предлагает итераторы произвольного доступа. Например:

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

Есть ли более идиоматический или более хороший способ сделать это? Я предполагаю, что эта модель возникает в разных ситуациях. Мне не нравится, что индекс integer доступен вне цикла. Брекетинг цикла и определение индекса в локальном блоке тоже кажутся уродливыми.

Конечно, когда контейнер предлагает итераторы произвольного доступа, можно использовать разницу итераторов, но тогда вы не можете использовать диапазон для:

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

Хотя я показываю только пример С++ 11, я ищу подсказки для С++ 0x и С++ 98.

Ответ 1

Мои личные предпочтения: просто держите дополнительный индекс. Ясно, что это, и если у вас когда-либо есть 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 и 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);    
});

Эта 4-ночная перегрузка std::transform иногда называется zip_with, поэтому есть некоторые комментарии использовать boost::zip_iterator с list и 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.

Ответ 2

Некоторые компиляторы уже предлагают выражения с лямбда-захватами, которые будут в стандарте С++ 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

EDIT:. Для записи я считаю, что здесь есть небольшая переменная индекса за пределами области цикла (см. другие ответы). Но для удовольствия я написал адаптер итератора (с помощью Boost Iterator Adapter), который можно использовать для привязки функции-члена 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

Ответ 3

Я бы, скорее всего, получил бы что-то вроде этого:

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

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

Я видел больше возможностей для циклов с двумя переменными цикла, чем я видел использование затушеванных итераторов или подсчитанных переменных лямбда-захвата. "Идиоматика" - субъективный термин, но я бы назвал это идиоматическим.

Наличие явной дополнительной переменной делает очевидным, что мы просто подсчитываем вверх. Это важно, если вы решите сделать что-то нетривиальное в цикле. Например, вы можете вставить или удалить элемент в списке и соответственно отрегулировать индекс - если вы используете адаптер итератора, может быть даже не очевидно, что указанный им индекс может фактически не быть индексом элемента в контейнере.


В качестве альтернативы вы можете написать вариант 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);
});

Ответ 4

Хорошо используя 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);
}

Ответ 5

Существует небольшая библиотека под названием pythonic, которая дает вам функцию enumerate(), вы можете знать из python, в С++. Он создает список пар с индексом и значением. Затем вы перебираете этот список. Это позволяет вам сделать следующее (из их документации):

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