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

Play @Compiler Explorer

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

Play @Compiler Explorer.

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

Play @Compiler Explorer.

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

Play @Compiler Explorer.

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.