Цикл на основе диапазона С++ с особым случаем для первого элемента?

Я часто нахожусь с кодом, который выглядит так:

bool isFirst = true;
for(const auto &item: items)
{
    if(!isFirst) 
    { 
       // do something
    }
    // Normal processing
    isFirst = false;
}

Похоже, должен быть лучший способ выразить это, так как это общий шаблон в функциях, которые действуют как "соединение".

Ответы

Ответ 1

Может быть, for_first_then_each - это то, что вы ищете? Он принимает ваш диапазон в терминах итераторов и применяет первую функцию к первому элементу, а вторую функцию к остальным.

#include <iostream>
#include <vector>

template<typename BeginIt, typename EndIt, typename FirstFun, typename OthersFun>
void for_first_then_each(BeginIt begin, EndIt end, FirstFun firstFun, OthersFun othersFun) {
    if(begin == end) return;
    firstFun(*begin);
    for(auto it = std::next(begin); it != end; ++it) {
        othersFun(*it);
    };
} 

int main() {

    std::vector<int> v = {0, 1, 2, 3};

    for_first_then_each(v.begin(), v.end(),
        [](auto first) { std::cout << first + 42 << '\n'; },
        [](auto other) { std::cout << other - 42 << '\n'; }
    );

    // Outputs 42, -41, -40, -39

    return 0;
}

Ответ 2

Вы не можете знать, какой элемент вы посещаете в диапазоне, основанном на цикле, если вы не зацикливаетесь на контейнере, таком как array или vector где вы можете взять адрес объекта и сравнить его с адресом первого элемента, чтобы выяснить где в контейнере вы находитесь Вы также можете сделать это, если контейнер обеспечивает поиск по значению, вы можете увидеть, совпадает ли итератор, возвращаемый из операции поиска, с итератором begin.

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

for (auto it = std::begin(items), first = it, end = std::end(items); it != end; ++it)
{
    if (it == first)
    {
        // do something
    }
    // Normal processing
}

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

// do something
for(const auto &item: items)
{
    // Normal processing
}

Ответ 3

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

int main() {
  std::vector<int> v{1,2,3,4};

  for (const auto & [is_first,b] : wrap(v)) {
    if (is_first) {
      std::cout << "First: ";
    }
    std::cout << b << std::endl;
  }
}

Реализация игрушек может выглядеть так:

template<typename T>
struct collection_wrap {
  collection_wrap(T &c): c_(c) {}

  struct magic_iterator {
    bool is_first = false;
    typename T::iterator itr;

    auto operator*() {
      return std::make_tuple(is_first, *itr);
    }

    magic_iterator operator++() {
      magic_iterator self = *this;
      itr++;
      //only works for forward
      is_first = false;
      return self;
    }

    bool operator!=(const magic_iterator &o) {
      return itr != o.itr;
    }
  };

  magic_iterator begin() {
    magic_iterator itr;
    itr.is_first = true;
    itr.itr = c_.begin();

    return itr;
  }

  magic_iterator end() {
    magic_iterator itr;
    itr.is_first = false;
    itr.itr = c_.end();

    return itr;
  }


  T &c_;
};

template<typename Collection>
collection_wrap<Collection>
wrap(Collection &vec) {
  return collection_wrap(vec);
}

Ответ 4

С появлением Ranges в С++ 20 вы можете разделить это на два цикла:

for (auto const& item : items | view::take(1)) {
    // first element only (or never executed if items is empty)
}

for (auto const& item : items | view::drop(1)) {
    // all after the first (or never executed if items has 1 item or fewer)
}

Если вы не хотите ждать С++ 20, посмотрите range-v3, который поддерживает обе эти операции.

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


Более простой версией является использование enumerate (которое существует только в range-v3, а не в С++ 20):

for (auto const& [idx, item] : view::enumerate(items)) {
    if (idx == 0) {
         // first element only
    }
    // all elements
}

Ответ 5

Подход, все еще действующий в C++, заключается в использовании макроса:

#include <iostream>
#include <vector>

#define FOR(index, element, collection, body) { \
    auto &&col = collection; \
    typeof(col.size()) index = 0; \
    for(auto it=col.begin(); it!=col.end(); index++, it++) { \
        const auto &element = *it; \
        body; \
    } \
}

using namespace std;

int main() {
    vector<int> a{0, 1, 2, 3};
    FOR(i, e, a, {
        if(i) cout << ", ";
        cout << e;
    })
    cout << endl;

    FOR(i, e, vector<int>({0, 1, 2, 3}), {
        if(i) cout << ", ";
        cout << e;
    })
    cout << endl;

    return 0;
}

Печать:

0, 1, 2, 3
0, 1, 2, 3

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

Ответ 6

Я предполагаю, что вы хотите знать, как получить первый элемент, вы можете сделать это с array и vector.

Я собираюсь показать array здесь.

Сначала включите это в свой код:

#include <array>

Затем преобразуйте ваш массив соответственно:

    std::array<std::string, 4> items={"test1", "test2", "test3", "test4"};

    for(const auto &item: items)
    {
        if(item == items.front()){
           // do something     
           printf("\nFirst: %s\n", item.c_str()); //or simply printf("\nFirst:"); since you gonna output a double element
        }
        // Normal processing
           printf("Item: %s\n", item.c_str());
    }
    return 0;
}