Каков идиоматический способ итерации контейнера при приращении целочисленного индекса?
Предположим, вы хотите знать числовой индекс элемента при итерации контейнера, который не предлагает итераторы произвольного доступа. Например:
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
$