Last Update:
5 Ways to Improve or Avoid Loops in C++20...23
Table of Contents
In this short article, I’ll show you several techniques to improve raw loops. I’ll take an example of a reverse iteration over a container and then transform it to get potentially better code.
The loop expression is an essential building block of programming. When you iterate over a container in C++20, we have the following options:
- Range based for loop with an initializer
- Iterators
- Algorithms
- Ranges and views
See examples below:
0. A broken loop
Let’s start with the following code; we’d like to print a std::vector
in reverse:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = vec.size() - 1; i >= 0; --i) {
std::cout << i << ": " << vec[i] << '\n';
}
This code is broken, as auto i
will be unsigned and cannot be “less than 0”! This is not a breakthrough, and we discussed this topic in detail in my other article: Reducing Signed and Unsigned Mismatches with std::ssize() - C++ Stories.
The point here is that such a raw loop adds a possibility of getting into trouble. It’s easy to mix something and break the code.
We can fix it by using modulo 2 arithmetic:
std::vector vec { 1, 2, 3, 4, 5, 6 };
for (auto i = vec.size() - 1; i < vec.size(); --i) {
std::cout << i << ": " << vec[i] << '\n';
}
And below, there’s even a better solution:
1. Fixing with safe types
If we still want to keep working with raw indices and use signed types, then we can at least use “safer” C++ functions:
std::ssize()
from C++20:
for (int i = ssize(vec) - 1; i >= 0; --i)
std::cout << i << ": " << vec[i] << '\n';
For the forward loop, we can write:
for (int i = 0; i < ssize(vec); ++i)
std::cout << i << ": " << vec[i] << '\n';
Alternatively there are also safe comparison functions like: cmp_less()
, also from C++20:
for (int i = 0; std::cmp_less(i, vec.size()); ++i)
std::cout << i << ": " << vec[i] << '\n';
See more in Integer Conversions and Safe Comparisons in C++20 - C++ Stories.
And here’s the full example that you can run @Compiler Explorer
Sidenote: the only warning I get on GCC is conversion for vec[i]
, this occurs when I enable -Wsign-conversion
.
Indices are fine for vectors and arrays… but how about some generic approach? See iterators:
2. Iterators
Iterators are core parts for algorithms and containers from the Standard Library. They are very generic and allow us to work with random access containers, maps, sets, or others. For our case with the reverse loop, we can write the following:
#include <iostream>
#include <set>
#include <vector>
void printReverse(auto cont) {
for (auto it = rbegin(cont); it != rend(cont); ++it)
std::cout << std::distance(rbegin(cont), it) << ": " << *it << '\n';
}
int main() {
std::vector vec { 1, 2, 3, 4, 5};
printReverse(vec);
std::set<std::string> names { "one", "two", "three", "four" };
printReverse(names);
}
Play at @Compiler Explorer
As you can see, the main advantage here is that there’s no need to mess with integral types; we can iterate using some abstract “proxy” objects.
3. Range-based for loop with an initializer
C++11 helped reduce iterations with iterators, and we can use a range-based for loop to perform a basic iteration. In C++20, we can include an “initializer” that can hold some extra properties for the iteration… like a counter:
int main() {
std::vector vec { 1, 2, 3, 4, 5};
for (int i = 0; const auto& elem : vec)
std::cout << i++ << ": " << elem << '\n';
}
Underneath, the compiler transforms the code into a call to begin()
and end()
for the container so that we can try with the reverse and some extra template code:
template<typename T>
class reverse {
private:
T& iterable_;
public:
explicit reverse(T& iterable) : iterable_{iterable} {}
auto begin() const { return std::rbegin(iterable_); }
auto end() const { return std::rend(iterable_); }
};
int main() {
std::vector vec { 1, 2, 3, 4, 5};
for (int i = 0; const auto& elem : reverse(vec))
std::cout << i++ << ": " << elem << '\n';
}
Note that I’ve taken the code idea from Reverse For Loops in C++ @Fluent C++.
You can also have a look at Peter Sommerlad’s repository with his Reverse Adapter: PeterSommerlad/ReverseAdapter: C++ adapter for traversing a container in a range-for in reverse order (C++17). The adapter is more generic and supports temporary objects:
using ::adapter::reversed;
for(auto const &i : reversed({0,1,2,3,4,5})) {
std::cout << i << '\n';
4. Ranges and views
Writing custom template code is fun, but for production, it’s best to rely on well-known patterns and techniques. In C++20, we have ranges that abstract the iterators and give us nicer syntax.
The ranges library offers many “views” that simplify code. For our case, we can use reverse
:
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector vec { 1, 2, 3, 4, 5};
for (int i = 0; const auto& elem : vec | std::views::reverse)
std::cout << i++ << ": " << elem << '\n';
}
The code with the pipe operator is equivalent to:
for (int i = 0; const auto& elem : std::ranges::reverse_view(vec))
std::cout << i++ << ": " << elem << '\n';
There are tons of other views and more to come in C++23. Have a look at this list at Ranges library (C++20) - cppreference.com.
5. Algorithms
We can go even further and rely on algorithms:
In a basic case with the reverse iteration but no indices, we can write:
template <typename T>
void printReverse(const T& cont) {
std::ranges::copy(cont | std::views::reverse,
std::ostream_iterator<typename T::value_type>( std::cout,"\n" ) );
}
The code above copies the container into the std::cout
stream object.
How about the full solution with indices? :)
The code for this simple task might be exaggerated, but for an experiment, let’s try:
#include <algorithm>
#include <iostream>
#include <set>
#include <vector>
#include <ranges>
#include <numeric>
void printReverse(auto cont) {
std::ranges::for_each(
std::views::zip(std::ranges::iota_view{0, ssize(cont)}, cont) | std::views::reverse,
[](const auto&elem) {
std::cout << std::get<0>(elem) << ' '
<< std::get<1>(elem) << '\n';
}
);
}
int main() {
std::vector vec { 1, 2, 3, 4, 5};
printReverse(vec);
std::set<std::string> names { "one", "two", "three", "four" };
printReverse(names);
}
The crucial part here is the zip
view available in GCC, which is part of C++23.
Bonus! One reader (thanks Scutum13) posted a cool trick - how about using lambda to carry state across iterations?
void printReverse(auto cont) {
std::ranges::for_each(cont | std::views::reverse,
[i=0](const auto& elem) mutable {
std::cout << i++ << ' ' << elem << '\n';
}
);
}
Run at Compiler Explorer
In this case, the code is even simpler!
Summary
Taking a loop and transforming it is a cool experiment! Thanks to many techniques available in C++20, it’s cool to explore the best syntax and readability.
In the article, I showed five options, but I could also try coroutine generators. I bet you also have some cool code snippets!
Back to you
- Do you write “raw” loops or try to use algorithms?
- Is a range-based for loop still a raw loop?
- Have you tried ranges?
Share your feedback in the comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: