Подтвердить что ты не робот

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

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

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.

4b9b3361

Ответ 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
$