Idiom(s) for “for each except the last” (or “betwe

2019-01-16 17:33发布

Everyone encounters this issue at some point:

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

... and you get an extra separator you don't want at the end. Sometime it's not printing, but, say, performing some other action, but such that consecutive actions of the same type require some separator action - but the last doesn't.

Now, if you work with old-school for loops and an array, you would do

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

(or you could special-case the last item out of the loop.) If you have anything that admits non-destructive iterators, even if you don't know its size, you can do:

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

I dislike the aesthetics of the last two, and like ranged for loops. Can I obtain the same effect as with the last two but using more spiffy C++11ish constructs?


To expand the question further (beyond, say, this one), I'll say I would also like not to expressly have special-case the first or the last element. That's an "implementation detail" which I don't want to be bothered with. So, in imaginary-future-C++, maybe something like:

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

12条回答
Anthone
2楼-- · 2019-01-16 17:56

I don't know about "idiomatic", but C++11 provides std::prev and std::next functions for bidirectional iterators.

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()));
    }
}
查看更多
时光不老,我们不散
3楼-- · 2019-01-16 17:57

I don't thing you can get around having a special case somewhere... For example, Boost's String Algorithms Library has a join algorithm. If you look at its implementation, you'll see a special case for the first item (no proceeding delimitier) and a then delimiter is added before each subsequent element.

查看更多
Viruses.
4楼-- · 2019-01-16 17:59

I like the boost::join function. So for more general behavior, you want a function that is called for each pair of items and can have a persistent state. You'd use it as a funcgion call with a lambda:

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

Now you can get back to a regular range-based for loop by using range filters!

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

In this idea, pair is set to a pair of adjecent elements of the original collection. If you have "abcde" then on the first iteration you are given first='a' and second='b'; next time through first='b' and second='c'; etc.

You can use a similar filter approach to prepare a tuple that tags each iteration item with an enumeration for /first/middle/last/ iteration and then do a switch inside the loop.

To simply leave off the last element, use a range filter for all-but-last. I don't know if that's already in Boost.Range or what might be supplied with Rangev3 in progress, but that's the general approach to making the regular loop do tricks and making it "neat".

查看更多
Deceive 欺骗
5楼-- · 2019-01-16 18:00

Excluding an end element from iteration is the sort of thing that Ranges proposal is designed to make easy. (Note that there are better ways to solve the specific task of string joining, breaking an element off from iteration just creates more special cases to worry about, such as when the collection was already empty.)

While we wait for a standardized Ranges paradigm, we can do it with the existing ranged-for with a little helper class.

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 }; }

and now you can write

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

Demo: http://rextester.com/MFH77611

For skip_last that works with ranged-for, a Bidirectional iterator is needed, for similar skip_first it is sufficient to have a Forward iterator.

查看更多
何必那么认真
6楼-- · 2019-01-16 18:08
int a[3] = {1,2,3};
int size = 3;
int i = 0;

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

Output:

1, 2, 3 

The goal is to use the way && is evaluated. If the first condition is true, it evaluates the second. If it is not true, then the second condition is skipped.

查看更多
再贱就再见
7楼-- · 2019-01-16 18:10

Typically I do it the opposite way:

bool first=true;
for(const auto& item : items) {
    if(!first) cout<<separator;
    first = false;
    cout << item;
}
查看更多
登录 后发表回答