Как я могу пропустить элементы в цикле for на основе диапазона на основе «индекса»?

Есть ли способ получить доступ к итератору (я полагаю, нет индекса цикла?) в цикле for на основе диапазона С++ 11?

Часто нам нужно сделать что-то особенное с первым элементом контейнера и перебрать оставшиеся элементы. Итак, я ищу что-то вроде оператора c++11_get_index_of в этом псевдокоде:

for (auto& elem: container) 
{
  if (c++11_get_index_of(elem) == 0)
     continue;

  // do something with remaining elements
}

Мне бы очень хотелось не возвращаться к старому стилю ручной обработки итераторов код в этом сценарии.


person Jay    schedule 19.01.2014    source источник
comment
Используйте конструкцию, которая обеспечивает необходимую вам функциональность. То есть используйте обычный цикл for (если это легко решает вашу проблему). Не заставляйте себя делать сложные вещи, которые не обязательно должны быть сложными.   -  person Nawaz    schedule 19.01.2014
comment
используйте алгоритм STL с предпочтительным диапазоном. вернуться к циклу for только в том случае, если ни один из алгоритмов не подходит.   -  person Karoly Horvath    schedule 19.01.2014
comment
@Nawaz Верно, но как насчет использования if (elem == container.front()) continue; в качестве обходного пути? Не слишком сложно, не так ли? И мы все еще можем использовать краткий синтаксис. Все же лучше, чем 3 строки ручной обработки итератора, как в ИМХО до С++ 11.   -  person Jay    schedule 19.01.2014
comment
@Jay: не надо, если только вы не хотите каждый раз молиться о том, чтобы контейнер не содержал равных (==) элементов.   -  person Karoly Horvath    schedule 19.01.2014
comment
@Jay: Правильный способ сделать это - использовать итераторы, а не индексы (они не являются универсальными) или алгоритмы C ++ (я не думаю, что какой-либо из них здесь подходит). Смотрите ответ Даниэля.   -  person user541686    schedule 19.01.2014
comment
@Jay В зависимости от контейнера и условий пропуска это может быть излишне дорого. Это также может привести к сбою, если в контейнере есть повторяющиеся элементы.   -  person Joseph Mansfield    schedule 19.01.2014
comment
@KarolyHorvath Верно .. Я думал о контейнерах, содержащих указатели (99% случаев использования в нашей кодовой базе)   -  person Jay    schedule 19.01.2014
comment
@Jay: Контейнеры не должны содержать необработанные указатели, если они ими владеют, потому что это небезопасно для исключений и не передает семантику, что контейнеры владеют объектами. Вместо этого вы должны использовать контейнер-указатель, например ptr_vector в Boost. Кроме того, если есть одна вещь, которую вы должны изучить с помощью C++, это: не ленитесь. Я знаю, что итераторы болезненны, но это правильный способ сделать это, и он поможет вам научиться писать надежный универсальный код. Индексы не всегда имеют смысл, но итераторы всегда имеют смысл.   -  person user541686    schedule 19.01.2014
comment
@all Хорошая дискуссия, спасибо за ваш вклад и обсуждение различных аспектов!   -  person Jay    schedule 19.01.2014


Ответы (8)


Часто нам нужно сделать что-то особенное с первым элементом контейнера и перебрать оставшиеся элементы.

Я удивлен, увидев, что до сих пор никто не предложил это решение:

  auto it = std::begin(container);

  // do your special stuff here with the first element

  ++it;

  for (auto end=std::end(container); it!=end; ++it) {

      // Note that there is no branch inside the loop!

      // iterate over the rest of the container
  }

Большое преимущество заключается в том, что ветвь перемещается из цикла. Это делает цикл намного проще, и, возможно, компилятор также может лучше оптимизировать его.

Если вы настаиваете на цикле for на основе диапазона, возможно, самый простой способ сделать это (есть и другие, более уродливые способы):

std::size_t index = 0;

for (auto& elem : container) {

  // skip the first element
  if (index++ == 0) {
     continue;
  }

  // iterate over the rest of the container
}

Однако я бы серьезно переместил ветвь из цикла, если все, что вам нужно, это пропустить первый элемент.

person Ali    schedule 19.01.2014
comment
Определенно предпочитаю ветвление вне цикла - person Jay; 19.01.2014
comment
Одним из недостатков этого является повышенная многословность, когда вы получаете контейнер из функции, которая может возвращать пустой список, потому что тогда вы получаете по существу две проверки размера контейнера. - person John Neuhaus; 25.02.2019

Boost предоставляет хороший лаконичный способ сделать это:

std::vector<int> xs{ 1, 2, 3, 4, 5 };
for (const auto &x : boost::make_iterator_range(xs.begin() + 1, xs.end())) {
  std::cout << x << " ";
}
// Prints: 2 3 4 5

Вы можете найти make_iterator_range в заголовке boost/range/iterator_range.hpp.

person Sam Kellett    schedule 16.01.2015

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

С auto и std::begin и std::end ваш цикл for должен быть очень простым:

for (auto it = std::begin(container); it != std::end(container); it++)
person Joseph Mansfield    schedule 19.01.2014
comment
Использование постинкремента здесь создает бессмысленный потенциал накладных расходов, особенно для контейнеров, чьи итераторы являются типами классов, а не указателями. Я бы хотел, чтобы все избавились от привычки по умолчанию использовать postinc/dec. - person underscore_d; 19.07.2017
comment
@underscore_d Это называется C ++ по правильной причине: P Разве компилятор не может просто заметить, что значение не используется? - person GeeTransit; 01.03.2020
comment
@GeeTransit Да, но это не обязательно, особенно со сложными итераторами, которые могут иметь побочные эффекты, которые он не анализирует как отсутствие операций. И да, то, что он называется C++, немного прискорбно, но я сомневаюсь, что ++C имел бы такое же звучание, так что вот мы здесь. ;-) - person underscore_d; 24.04.2020

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

Выбор правильного алгоритма зависит от того, что вы хотите делать с элементами... о чем вы нам не сказали.

Если вы хотите пропустить первый элемент, пример дампа:

if (!container.empty()) {
   for_each(++container.begin(), container.end(), [](int val) { cout << val; });
}
person Karoly Horvath    schedule 19.01.2014
comment
Я понимаю вашу точку зрения, хотя обычно это множество вещей, которые мы делаем с нашими бизнес-объектами, поэтому даже лямбда-выражения могут стать большими и выйти из-под контроля. Все еще проблема с for_each, когда нам нужно пропустить обработку одного из элементов, не так ли? - person Jay; 19.01.2014
comment
Если пропуск основан на индексе, вы, вероятно, делаете что-то не так. Реструктурируйте свои данные. - person Karoly Horvath; 19.01.2014

Как насчет использования простого цикла for с iteratos:

for(auto it = container.begin(); it != container.end(); it++)
{
    if(it == container.begin())
    {
        //do stuff for first
    }
    else
    {
        //do default stuff
    }
}

Это не основано на диапазоне, но оно функционально. Если вы все еще хотите использовать цикл диапазона:

int counter = 0;
for(auto &data: container)
{
    if(counter == 0)
    {
        //do stuff for first
    }
    else
    {
        //do default stuff
    }
    counter++;
}
person Netwave    schedule 19.01.2014

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

int index= 0;
for (auto& elem: container) 
{
  if (index++ == something)
     continue;

  // do something with remaining elements
}

Если вы хотите пропустить первый элемент, другой способ — использовать std::deque и pop_front для первого элемента. Затем вы можете выполнить цикл range for с контейнером, как обычно.

person nurettin    schedule 19.01.2014

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

for( std::size_t i : indexes( container ) ) {
  if (i==0) continue;
  auto&& e = container[i];
  // code
}

единственная сложная часть — написать indexes, которая возвращает диапазон итераторов, которые boost вызывает counting. Создать базовый итерируемый диапазон из итераторов легко: либо используйте концепцию диапазона boost, либо сверните свою собственную.

Базовый диапазон для произвольного типа итератора:

template<typename Iterator>
struct Range {
  Iterator b; Iterator e;
  Range( Iterator b_, Iterator e_ ):b(b_), e(e_) {};
  Iterator begin() const { return b; }
  Iterator end() const { return e; }
};

что вы можете придумать кучу, но это ядро.

person Yakk - Adam Nevraumont    schedule 19.01.2014

Я бы старался избегать использования итераторов, потому что идея цикла for на основе for чтобы избавиться от них. Начиная с C++20, чтобы пропустить первый элемент в вашем container, Я бы выбрал один из следующих подходов. Я также включаю, для полноты картины, как обрабатывать первый элемент отдельно:

  1. Обработка первого элемента вне цикла

    Вы можете использовать container.front(), который существует для всех контейнеров последовательности, чтобы получить доступ к первому элементу. Однако вы должны убедиться, что контейнер не пуст, чтобы избежать ошибки сегментации. Затем, чтобы пропустить первый элемент (или более) в цикле, вы можете использовать адаптер диапазона std::views::drop из библиотеки диапазонов. Все вместе это выглядит следующим образом:

    std::vector<int> container { 1, 2, 3 };
    
    if(!container.empty()) {
        // do something with first element
        std::cout << "First element: " << container.front() << std::endl;
    }
    
    for (auto& elem : container | std::views::drop(1)) {
        // do something with remaining elements
        std::cout << "Remaining element: " << elem << std::endl;
    }
    

    Вместо container.front() вы также можете использовать другой цикл for на основе диапазона вместе с адаптером диапазона std::views::take(1)< /а>. Преимущество take() и drop() в том, что они работают безопасно, даже если их аргументы превышают количество элементов в вашем container.

  2. Обработка первого элемента внутри цикла

    Вы можете использовать инит-оператор в цикле for на основе диапазона для определения логического флага (или даже счетчика). Таким образом, флаг виден только в рамках цикла. Вы можете использовать флаг внутри цикла следующим образом:

    std::vector<int> container { 1, 2, 3 };
    
    for(bool isFirst(true); auto& elem : container) {
        if(isFirst) {
            // do something with first element
            std::cout << "First element: " << elem << std::endl;
            isFirst = false;
            continue;
        }
    
        // do something with remaining elements
        std::cout << "Remaining element: " << elem << std::endl;
    }
    

Вывод для обоих показанных подходов:

Первый элемент: 1
Оставшийся элемент: 2
Оставшийся элемент: 3

Код на Wandbox

person honk    schedule 19.04.2021