How C++20 improved the for-loop syntax
22 points by raymii
22 points by raymii
Yet you still need the ++i bit that none of the other languages require…
if using C++23 you can do this:
for (auto [idx, val] = std::views::enumerate(iter) {
do_whatever(0);
}
Relevant source: https://en.cppreference.com/cpp/ranges/enumerate_view
I was also about to comment that it's still wild that you have to rely on a side-effect of the print statement to advance the loop, but it seems that the example was just really confusing.
It works perfectly well without incrementing an index:
it.cpp:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<string> vec = {
"the", "quick", "brown", "fox",
"jumped", "over", "the", "lazy", "dog"
};
for (auto&& it: vec) {
cout << it << endl;
}
}
$ clang++ it.cpp --std=c++20 -o it && ./it
the
quick
brown
fox
jumped
over
the
lazy
dog
If the author wanted a counter, it should probably just be created before the for loop to avoid confusing people with shaky C++ knowledge such as myself. :)
The article is a bit confusing. There are two things in the new syntax:
The problem that the article is discussing is that you may be iterating over something that isn't indexed by an integer (e.g. a map, which gives key-value pairs in iteration), but you want to know in each loop iteration how many key-value pairs you've seen. That's a simple thing to want, but with C++17 and earlier you needed to write it as:
int counter = 0;
for (auto &thing : collection)
{
/* Do stuff with thing and counter here */
counter++;
}
But now counter is initialised outside of the loop and, most importantly, its scope doesn't end until the end of the block that encloses the loop. With C++20, you can rewrite it as:
for (int counter = 0; auto &thing : collection)
{
/* Do stuff with thing and counter here */
counter++;
}
And now counter goes out of scope when the last loop iteration terminates. This is more important for RAII types than a simple counter. Imagine if you want a std::shared_ptr that holds a pointer to the previous value. Something like (auto expanded for clarity):
for (std::shared_ptr<MyThingy> lastIteration; std::shared_ptr<MyThingy> &thing : collection)
{
/* Do stuff with thing and lastIteration here */
lastIteration = thing;
}
Now, the last iteration of the loop destroys lastIteration and so you're not artificially prolonging the lifetime to the end of an enclosing block.
Well, the whole point of the article is to point out that on C++ 20, you can now initialize the counter in the loop statement, so doing it that way ignores the point of the article
This looks nice, and there's a few cases I would have wanted to use this -- except I didn't know it existed.
Few C++ programmers I know try to keep up to date anymore with the feature treadmill.
C++ has the perfect storm of adding lots of features, and relying on compiler compatability across multiple platforms. Unlike Rust or other languages, there's a huge gulf between "feature released" and "feature is actually usable for cross platform work." When C++26 releases, a lot of folks won't even be able to use it for multiple years, by which time there'll be yet another soup of features people are excited about. There's some great new additions along the way (e.g. designated initializers), but we're already swimming in features.
While we're exploring new C++ features, C++23 standardised std::print...
for (int i=0; auto&& it: vec)
/* cout << (++i) << ": " << it << endl; */
std::println("{}: {}", ++i, it);
Essentially a type-safe printf, no streams or overloaded operators required.
I feel like I'm swimming against the stream on this particular issue. I just really don't get why we need to mess with for loops. for (int i = 0; i < arr.size; i++) { item = arr[i]; ... is
And then every language seems to be pushing foreach super hard, and I just don't get it. The main benefit seems to be that you can iterate things that are not integer iterable, like dictionaries. But why does my dict iteration need to look the same as my array iteration? Especially if I'm losing the convenience of being able to easily do, y'know, array manipulation.
IMO foreach is less understandable, less powerful, more confusing for newbies to your language, requires larger diffs when you need to change something (when you inevitably need to change the foreach into a normal for because you need to do something more complex), and is just generally not worth the marginal benefits. Something like cpp iterators is fine for dicts also imo. I just really don't think iteration is a problem that needs to be special cased at the language syntax level.
In my opinion, the main benefit of foreach syntax is not that it provides a uniform syntax for all collections, but rather that its reduced scope simplifies the common case and eliminates certain classes of programming mistakes. In other words, I view the flexibility of the three-clause for syntax a weakness rather than a strength.
Let me give some examples:
foreach (item in items), or for (int i = 0; i < items.size(); ++I) { T item = items[i]; }? I am not convinced that the index loop is obvious at all to a beginner, and certainly not more obvious than a foreach.for (int I = 0; j < n; ++I). This bug is easily detected and caught, but the point is that the foreach syntax eliminates such mistakes by construction. (If you are not convinced that this is an issue that people run into in practice, I invite you to look at competitive programming forums and look for posts from people who have defined their own cursed FORI(i, 0, n-1) macros in C++...)for (yet most people do not miss it!)readable
obvious
for item in arr { ... }
works in every language
minimum common denominator is usually not the best thing to target
it works almost the same in every language (scoping rules are notoriously divergent) but they certainly look different in different languages:
// in Rust for i in 0..arr.len() { let item = arr[i]; ... }
gives you both the item and its index, if you need it
Languages that lean more on iterators also have that flexibility, with the benefit of being explicit that you're wanting the index:
for (i, item) in arr.iter().enumerate() { ... }
easy to use with parallel arrays
Do you mean like
for item in arr.par_iter() { ... }
or like
for (item1, item2) in arr1.iter().zip(arr2.iter()) { ... }
easy to extend with advanced patterns like skipping variable size chunks
These are always tricky because the specifics change what pattern I end up writing.
delete-while-iterating
I really hope that you mean either replacing the current item slot with a sentinel and not removing the slot and then decrementing the index, because the latter is not only very error-prone, it is also super time-inefficient (but space-efficient). Picture an array where you end up removing every single element: you'll be visiting each slot, removing it which implies moving every single subsequent slot back, so you end up with O(n) moves. For unsorted arrays, you can swap the visited element with the last one in the array, but you still have to deal with managing the index.
easy to reverse
for item in arr.iter().rev() { ... }
easy to start in the middle
let skip = arr.len() / 2;
for item in arr.iter().skip(skip) { ... }
easy to compose multiple loops into one merged one
I find iterator composition to generally be easier, with exceptions. The big difference is that a language that gives you the option you can choose on a case by case basis which one to use.
easy to start in the middle
let skip = arr.len() / 2; for item in arr.iter().skip(skip) { ... }
I agree with the rest but not this one: skip is part of Iterator and the default implementation is a O(n) traversal so it relies on the collection overriding this method with no real indication that this works in O(1) rather than O(n) except checking the implementation. And then while I believe it works in Rust it's not necessarily portable (it definitely doesn't work in python) so it's a bit of a polyglot trap.
I'd rather write, and read, arr[skip..].iter() (or in this case just &arr[skip..]).
The reason "modern" languages have a foreach is that the most common way to loop over a data is by iterating one by one and off by one errors sometimes are hard to track down. Even the linux kernel has a set of foreach macros to prevent common bugs.
Though I do agree that a regular for loop is just fine if it works for what you're doing and sometimes there's no other way to do it but using a regular for loop, sometimes it's also more readable than trying to call a bunch of combinators to create the exact iteration pattern you want.
I would guess because people keep getting it wrong. If you are new to the language you might do <= rather than <, or even if you are experienced, you might make a mistake if you are iterating over multiple ranges or iterating in reverse with an unsigned index. I think foreach type loops are less error prone and better convey what you are iterating over.
In C++23, you can also use std::views::enumerate to get an index while iterating over a collection.
I spend most of my time programming Python and I do not know C++ really. I agree with you for C++ because I feel like the principle advantages of an in-language foreach loop are kind of lost in C++, and C++ already has sufficient features that if I were doing it day-to-day, I would look upon this new syntax with a deep feeling of dread that it will only provide me with a bunch of new edge cases and complexity to worry about.
I thought the whole point of iterators in C++ was to bridge the idea of enumerating collections to what is natural to C++, the integer for loop. Seeing this feature today gives me the same feeling of naturalness as watching a video played backwards, like a river reversing course and flowing back upstream.