Петля итератора против контура индекса

Возможный дубликат:
Зачем использовать итераторы вместо индексов массива?

Я просматриваю свои знания на С++, и я наткнулся на итераторы. Одна вещь, которую я хочу знать, - это то, что делает их такими особенными, и я хочу знать, почему это:

using namespace std;

vector<int> myIntVector;
vector<int>::iterator myIntVectorIterator;

// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(myIntVectorIterator = myIntVector.begin(); 
        myIntVectorIterator != myIntVector.end();
        myIntVectorIterator++)
{
    cout<<*myIntVectorIterator<<" ";
    //Should output 1 4 8
}

лучше, чем это:

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

for(int y=0; y<myIntVector.size(); y++)
{
    cout<<myIntVector[y]<<" ";
    //Should output 1 4 8
}

И да, я знаю, что я не должен использовать пространство имен std. Я просто взял этот пример с сайта cprogramming. Так что, пожалуйста, скажите мне, почему последнее хуже? Какая большая разница?

Ответ 1

Особенность итераторов заключается в том, что они обеспечивают клей между алгоритмами и контейнерами. Для общего кода рекомендуется использовать комбинацию алгоритмов STL (например, find, sort, remove, copy) и т.д., Которая выполняет расчеты, которые вы имеете в виду в своей структуре данных (vector, list, map и т.д.) и предоставить этот алгоритм итераторам в ваш контейнер.

Ваш конкретный пример может быть записан как комбинация алгоритма for_each и контейнера vector (см. вариант 3) ниже), но только один из четырех различных способов перебора по std::vector:

1) Итерация на основе индексов

for (std::size_t i = 0; i != v.size(); ++i) {
    // access element as v[i]

    // any code including continue, break, return
}

Преимущества: знакомые всем, знакомым с кодом стиля C, могут циклически использовать разные шаги (например, i += 2).

Недостатки: только для последовательных контейнеров произвольного доступа (vector, array, deque) не работает для list, forward_list или ассоциативных контейнеров. Кроме того, управление контуром немного подробное (init, check, increment). Люди должны знать о индексировании на основе 0 в С++.

2) итерация на основе итератора

for (auto it = v.begin(); it != v.end(); ++it) {
    // if the current index is needed:
    auto i = std::distance(v.begin(), it); 

    // access element as *it

    // any code including continue, break, return
}

Преимущества: более общие, работает для всех контейнеров (даже новые неупорядоченные ассоциативные контейнеры, также могут использовать разные шаги (например, std::advance(it, 2));

Недостатки: нужна дополнительная работа, чтобы получить индекс текущего элемента (может быть O (N) для списка или forward_list). Опять же, управление контуром немного подробное (init, check, increment).

3) STL for_each алгоритм + lambda

std::for_each(v.begin(), v.end(), [](T const& elem) {
     // if the current index is needed:
     auto i = &elem - &v[0];

     // cannot continue, break or return out of the loop
});

Преимущества: то же, что и 2) плюс небольшое сокращение управления контуром (без проверки и приращения), это может значительно снизить вашу частоту ошибок (неправильная инициализация, проверка или приращение, ошибки "один за другим" ).

Недостатки: такие же, как явный цикл итератора плюс ограниченные возможности для управления потоком в цикле (не могут использовать continue, break или return) и никакой опции для разных шагов (если вы не используете адаптер итератора, который перегружает operator++).

4) цикл диапазона для

for (auto& elem: v) {
     // if the current index is needed:
     auto i = &elem - &v[0];

    // any code including continue, break, return
}

Преимущества: очень компактное управление контуром, прямой доступ к текущему элементу.

Недостатки: дополнительная инструкция для получения индекса. Нельзя использовать разные шаги.

Что использовать?

Для вашего конкретного примера итерации по std::vector: если вам действительно нужен индекс (например, доступ к предыдущему или следующему элементу, печать/запись индекса внутри цикла и т.д.), или вам нужен шаг, отличный от 1, тогда Я бы пошел за явным индексированным циклом, иначе я бы пошел на цикл range-for.

Для общих алгоритмов в общих контейнерах я бы пошел за явным циклом итератора, если только код не содержал никакого управления потоком внутри цикла и не нуждался в шаге 1, и в этом случае я бы пошел на STL for_each + лямбда.

Ответ 2

Итераторы делают ваш код более общим.
Каждый стандартный контейнер библиотеки предоставляет итератор, поэтому, если вы измените свой класс контейнера в будущем, этот цикл не будет затронут.

Ответ 3

Итераторы являются первым выбором по сравнению с operator[]. С++ 11 предоставляет функции std::begin(), std::end().

Поскольку ваш код использует только std::vector, я не могу сказать, что в обоих кодах есть большая разница, однако operator [] может работать не так, как вы намереваетесь. Например, если вы используете карту, operator[] будет вставлять элемент, если не найден.

Кроме того, используя iterator, ваш код становится более переносимым между контейнерами. Вы можете легко переключаться между контейнерами от std::vector до std::list или другим контейнером без значительных изменений, если вы используете итератор. Это правило не применяется к operator[].

Ответ 4

Это всегда зависит от того, что вам нужно.

Вы должны использовать operator[], когда нужен прямой доступ к элементам в векторе (когда вам нужно индексировать определенный элемент в векторе). Нет ничего плохого в использовании его над итераторами. Тем не менее, вы должны сами решить, что (operator[] или итераторы) наилучшим образом соответствуют вашим потребностям.

Использование итераторов позволит вам переключиться на другие типы контейнеров без значительных изменений в вашем коде. Другими словами, использование итераторов сделает ваш код более общим и не зависит от конкретного типа контейнера.

Ответ 5

С помощью векторных итераторов нет никаких реальных преимуществ. Синтаксис является более уродливым, более длинным для ввода и более трудным для чтения.

Итерация по вектору с использованием итераторов выполняется не быстрее и не является более безопасным (на самом деле, если вектор, возможно, изменился в течение итерации с использованием итераторов, вы столкнетесь с большими проблемами).

Идея иметь общий цикл, который работает, когда вы позже измените тип контейнера, также в большинстве случаев бессмыслица в реальных случаях. К сожалению, темная сторона строго типизированного языка без серьезного ввода текста (немного лучше теперь с С++ 11) заключается в том, что вам нужно сказать, что является типом всего на каждом шаге. Если вы передумаете, вам все равно придется ходить и менять все. Кроме того, разные контейнеры имеют очень разные компромиссы, и изменение типа контейнера часто не так часто происходит.

Единственный случай, когда итерация должна поддерживаться, если это возможно, является общим при написании кода шаблона, но это (я надеюсь, для вас) не является самым частым случаем.

Единственная проблема, присутствующая в вашем явном цикле индексов, заключается в том, что size возвращает значение unsigned (ошибка дизайна С++), а сравнение между подписанным и unsigned опасно и удивительно, поэтому лучше избегать. Если вы используете достойный компилятор с включенными предупреждениями, там должна быть диагностика.

Обратите внимание, что решение не должно использовать unsiged как индекс, потому что арифметика между неподписанными значениями также, по-видимому, нелогична (она по модулю арифметики и x-1 может быть больше, чем x). Вместо этого вы должны использовать размер до целого числа, прежде чем использовать его. Он может иметь смысл использовать беззнаковые размеры и индексы (уделяя много внимания каждому выражению, которое вы пишете), только если вы работаете над 16-битной реализацией на С++ (16 бит был причиной наличия неподписанных значений в размерах).

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

void drawPolyline(const std::vector<P2d>& points)
{
    for (int i=0; i<points.size()-1; i++)
        drawLine(points[i], points[i+1]);
}

Здесь присутствует ошибка, потому что, если вы пройдете пустой вектор points, значение points.size()-1 будет огромным положительным числом, заставив вас зациклиться на segfault. Рабочее решение может быть

for (int i=1; i<points.size(); i++)
    drawLine(points[i - 1], points[i]);

но я лично предпочитаю всегда удалять unsinged -ness с помощью int(v.size()).

Уродство использования итераторов в этом случае остается в качестве упражнения для читателя.

Ответ 6

Записывая код клиента в терминах итераторов, вы полностью абстрагируете контейнер.

Рассмотрим этот код:

class ExpressionParser // some generic arbitrary expression parser
{
public:
    template<typename It>
    void parse(It begin, const It end)
    {
        using namespace std;
        using namespace std::placeholders;
        for_each(begin, end, 
            bind(&ExpressionParser::process_next, this, _1);
    }
    // process next char in a stream (defined elsewhere)
    void process_next(char c);
};

клиентский код:

ExpressionParser p;

std::string expression("SUM(A) FOR A in [1, 2, 3, 4]");
p.parse(expression.begin(), expression.end());

std::istringstream file("expression.txt");
p.parse(std::istringstream<char>(file), std::istringstream<char>());

char expr[] = "[12a^2 + 13a - 5] with a=108";
p.parse(std::begin(expr), std::end(expr));

Изменить: рассмотрите пример исходного кода, реализованный с помощью

using namespace std;

vector<int> myIntVector;
// Add some elements to myIntVector
myIntVector.push_back(1);
myIntVector.push_back(4);
myIntVector.push_back(8);

copy(myIntVector.begin(), myIntVector.end(), 
    std::ostream_iterator<int>(cout, " "));

Ответ 7

Хорошая вещь об итераторе заключается в том, что позже, если вы захотите переключить свой вектор на другой контейнер STD. Тогда форпоп по-прежнему будет работать.