Jonathan Boccara's blog

Why You Should Use std::for_each over Range-based For Loops

Published February 7, 2019 - 0 Comments

Daily C++

Today’s guest post is written by Jon Kalb. Jon’s infrequently updated blog is // info and he podcasts with Phil Nash on Cpp.chat. For onsite training he can be reached at jon@cpp.training.

I’d like start by thanking Jonathan for creating and maintaining the Fluent{C++} blog, for the conversations that it spawns, and for letting me contribute with this guest post. Jonathan has invited me to add my thoughts on his previous posting, Is std::for_each obsolete?

In that posting, Jonathan reminds us that for_each is useful as a tool for respecting appropriate levels of abstraction. I’m going to go further and argue that for_each should be used instead of range-based for loops in most cases, because it encourages us to create correct abstraction levels.

I often begin my Modern C++ (C++11 or later) training classes by asking students for their favorite feature of Modern C++. I usually hear about range-based for loops within the first three or four responses. It is a very popular feature and, in a sense, a no-brainer. When discussing this feature I tell students, “You might be familiar with this feature if you’ve ever used… any other language ever (except C).”

I don’t think it was a mistake to have added this to C++11, but I don’t encourage it use. Why?

More flexibility

Range-based for loops win over classic for loops in the area of clarity and expressiveness with no performance cost. (See Matt Godbolt’s talk on how Compiler Explorer was created to address the question of performance for this specific feature.) So they seem like something we should embrace.

When I first started using C++11, I was intrigued by the fact that this new for loop syntax was, essentially, the for_each algorithm. It was a bit different because, as a for loop, it supports break and continue, but it is logically the same operation with a different syntax. Which should we use, the raw loop or the algorithm?

The conclusion that I came to is the same one that Arno Schödl of Think-Cell came to. He discusses this issue with Jason and Rob on CppCast.

I agree with Arno that range-based for loops don’t generalize very well. They are an easy way to do a for_each on each item in a container, in order, but if that isn’t exactly what you want to do? You have re-write your loop body. If you only want to operate on part of a container? You have to re-write your loop body. Want to operate on “n” items? Re-write. Reverse order? Re-write. Only operate on items that satisfy a particular predicate? Re-write.

But if you capture the body of your loop in a lambda expression, then you can very easily operate on a subset of a container by selecting appropriate iterators. You could switch to std::for_each_n. If want, you can use reverse iterators or filter iterators. Even more possibilities are unlocked once we start using the ranges library.

Getting in the habit of thinking of your loop bodies as functions to call (and writing them as lambda expressions) is a good habit to acquire. It is STL friendly and generalizes much better than a for loop body. It is also future-friendly, as teams start to adopt the ranges library.

More freedom with abstraction levels

You can think of it as adding a level of abstraction between your looping construct and your loop body. Range-based for loops couple the body with the loop construct, but generic algorithms separate the loop construct (the algorithm) from the body (the lambda expression).

In his previous post, Jonathan defended for_each for its ability to separate the abstraction levels of the loop construct from the loop body implementation and I couldn’t agree more. But he also defended range-based for loops because they “allow [us] to write code directly at the loop site.” The price we pay for this, he pointed out, is that we need to accept the constraint that “this code needs to be at the same abstraction level as the code surrounding the loop.”

My opinion is that we shouldn’t accept that constraint. There may be time when it happens to apply (the loop body is at the same abstraction level as the loop construct), but if we write code that depends on the fact that it happens to apply, we lock that code into that needless constraint and make it harder for us to refactor or generalize the code later.

Far better, in my opinion, to think generically and functionally, separating the code that performs the function (the lambda expression) from the code that decides where and/or when it is done (the loop embedded in an algorithm).

As Jonathan said, it all comes down to respecting levels of abstraction.

Any reactions are welcome in the comment section below.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin