Идиома для "для каждого, кроме последнего" (или "между каждой последовательной парой элементов" )

Каждый сталкивается с этой проблемой в какой-то момент:

for(const auto& item : items) {
    cout << item << separator;
}

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

Теперь, если вы работаете со старой школой для циклов и массива, вы бы сделали

for(int i = 0; i < num_items; i++)
    cout << items[i];
    if (i < num_items - 1) { cout << separator; }
}

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

for(auto it = items.cbegin(); it != items.cend(); it++) {
    cout << *it;
    if (std::next(it) != items.cend()) { cout << separator; }
}

Мне не нравится эстетика последних двух, и, как правило, для петель. Могу ли я получить тот же эффект, что и с двумя последними, но используя более сильные конструкции С++ 11ish?


Чтобы расширить вопрос дальше (за пределами, скажем, этого), я скажу, что также хотел бы, чтобы явным образом не имел особого случая первый или последний элемент. Это "деталь реализации", с которой я не хочу беспокоиться. Итак, в imaginary-future-С++ возможно что-то вроде:
for(const auto& item : items) {
    cout << item;
} and_between {
    cout << separator;
}

Ответ 1

Мой путь (без дополнительной ветки):

const auto separator = "WhatYouWantHere";
const auto* sep = "";
for(const auto& item : items) {
    std::cout << sep << item;
    sep = separator;
}

Ответ 2

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

Пока мы ожидаем стандартизованной парадигмы Ranges, мы можем сделать это с существующим диапазоном с небольшим вспомогательным классом.

template<typename T> struct trim_last
{
    T& inner;

    friend auto begin( const trim_last& outer )
    { using std::begin;
      return begin(outer.inner); }

    friend auto end( const trim_last& outer )
    { using std::end;
      auto e = end(outer.inner); if(e != begin(outer)) --e; return e; }
};

template<typename T> trim_last<T> skip_last( T& inner ) { return { inner }; }

и теперь вы можете написать

for(const auto& item : skip_last(items)) {
    cout << item << separator;
}

Демо: http://rextester.com/MFH77611

Для skip_last, который работает с ranged-for, необходим двунаправленный итератор, для аналогичного skip_first достаточно иметь итератор Forward.

Ответ 3

Знаете ли вы устройство Duff?

int main() {
  int const items[] = {21, 42, 63};
  int const * item = items;
  int const * const end = items + sizeof(items) / sizeof(items[0]);
  // the device:
  switch (1) {
    case 0: do { cout << ", ";
    default: cout << *item; ++item; } while (item != end);
  }

  cout << endl << "I'm so sorry" << endl;
  return 0;
}

(Live)

Надеюсь, я не испортил весь день. Если вы не хотите или никогда не использовать это.

(бормотать) Мне очень жаль...


Устройство, обрабатывающее пустые контейнеры (диапазоны):

template<typename Iterator, typename Fn1, typename Fn2>
void for_the_device(Iterator from, Iterator to, Fn1 always, Fn2 butFirst) {
  switch ((from == to) ? 1 : 2) {
    case 0:
      do {
        butFirst(*from);
    case 2:
        always(*from); ++from;
      } while (from != to);
    default: // reached directly when from == to
      break;
  }
}

Live test:

int main() {
  int const items[] = {21, 42, 63};
  int const * const end = items + sizeof(items) / sizeof(items[0]);
  for_the_device(items, end,
    [](auto const & i) { cout << i;},
    [](auto const & i) { cout << ", ";});
  cout << endl << "I'm (still) so sorry" << endl;
  // Now on an empty range
  for_the_device(end, end,
    [](auto const & i) { cout << i;},
    [](auto const & i) { cout << ", ";});
  cout << "Incredibly sorry." << endl;
  return 0;
}

Ответ 4

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

#include <iostream>
#include <vector>

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

    std::cout << "\"";
    if (!values.empty())
    {
        std::cout << values[0];

        for (size_t i = 1; i < values.size(); ++i)
        {
            std::cout << ", " << values[i];
        }
    }
    std::cout << "\"\n";

    return 0;
}

Выход: "1, 2, 3, 4, 5"

Ответ 5

Обычно я делаю это наоборот:

bool first=true;
for(const auto& item : items) {
    if(!first) cout<<separator;
    first = false;
    cout << item;
}

Ответ 6

Мне нравятся простые структуры управления.

if (first == last) return;

while (true) {
  std::cout << *first;
  ++first;
  if (first == last) break;
  std::cout << separator;
}

В зависимости от вашего вкуса вы можете поместить инкремент и тест в одну строку:

...
while (true) {
  std::cout << *first;
  if (++first == last) break;
  std::cout << separator;
}

Ответ 7

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

Ответ 8

Вы можете определить функцию for_each_and_join, которая в качестве аргумента принимает два функтора. Первый функтор работает с каждым элементом, второй работает с каждой парой смежных элементов:

#include <iostream>
#include <vector>

template <typename Iter, typename FEach, typename FJoin>
void for_each_and_join(Iter iter, Iter end, FEach&& feach, FJoin&& fjoin)
{
    if (iter == end)
        return;

    while (true) {
        feach(*iter);
        Iter curr = iter;
        if (++iter == end)
            return;
        fjoin(*curr, *iter);
    }
}

int main() {
    std::vector<int> values = { 1, 2, 3, 4, 5 };
    for_each_and_join(values.begin(), values.end()
    ,  [](auto v) { std::cout << v; }
    ,  [](auto, auto) { std::cout << ","; }
    );
}

Пример в реальном времени: http://ideone.com/fR5S9H

Ответ 9

int a[3] = {1,2,3};
int size = 3;
int i = 0;

do {
    std::cout << a[i];
} while (++i < size && std::cout << ", ");

Вывод:

1, 2, 3 

Цель состоит в том, чтобы использовать способ &&. Если первое условие истинно, оно оценивает второе. Если это неверно, то второе условие пропускается.

Ответ 10

Я не знаю о "идиоматическом", но С++ 11 предоставляет функции std::prev и std::next для двунаправленных итераторов.

int main() {
    vector<int> items = {0, 1, 2, 3, 4};
    string separator(",");

    // Guard to prevent possible segfault on prev(items.cend())
    if(items.size() > 0) {
        for(auto it = items.cbegin(); it != prev(items.cend()); it++) {
            cout << (*it) << separator;
        }
        cout << (*prev(items.cend()));
    }
}

Ответ 11

Мне нравится функция boost::join. Поэтому для более общего поведения вам нужна функция, которая вызывается для каждой пары элементов и может иметь постоянное состояние. Вы использовали бы это как вызов funcgion с лямбдой:

foreachpair (range, [](auto left, auto right){ whatever });

Теперь вы можете вернуться к регулярному циклу for на основе диапазона, используя фильтры диапазона!

for (auto pair : collection|aspairs) {
    Do-something_with (pair.first);
}

В этой идее pair задается пара смежных элементов исходного набора. Если у вас есть "abcde", то на первой итерации вам присваиваются первые = "a", а второй = "b"; в следующий раз через first = 'b' и second = 'c'; и др.

Вы можете использовать подобный подход фильтра для подготовки кортежа, который отмечает каждый элемент итерации перечислением для /first/middle/last/iteration, а затем выполняет переключатель внутри цикла.

Чтобы просто оставить последний элемент, используйте фильтр диапазона для всех-но-последних. Я не знаю, уже ли это в Boost.Range или что может быть поставлено с Rangev3 в процессе, но что общий подход к созданию регулярного цикла делает трюки и делает его "опрятным".

Ответ 12

Вот небольшой трюк, который мне нравится:

Для двунаправленных итерационных объектов: for ( auto it = items.begin(); it != items.end(); it++ ) { std::cout << *it << (it == items.end()-1 ? "" : sep); };

Используя тернарный оператор ?, я сравниваю текущую позицию итератора с вызовом item.end()-1. Поскольку итератор, возвращаемый item.end(), ссылается на позицию после последнего элемента, мы уменьшаем его один раз, чтобы получить наш фактический последний элемент.

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

Для одноименных итераций (проверено с помощью std:: forward_list): for ( auto it = items.begin(); it != items.end(); it++ ) { std::cout << *it << (std::distance( it, items.end() ) == 1 ? "" : sep); };

Здесь мы заменяем предыдущее тернарное условие вызовом std:: distance, используя текущее местоположение итератора, и конец итерабельного.

Примечание. Эта версия работает как с двунаправленными итерами, так и с итераторами с одним направлением.

EDIT: Я понимаю, что вам не нравятся итерации типа .begin() и .end(), но если вы хотите, чтобы счетчик LOC уменьшался, в этом случае вам, вероятно, придется избегать итерации на основе диапазона.

"Trick" просто обертывает логику сравнения в одном тройном выражении, если ваша логика сравнения относительно проста.