Does the range-based 'for' loop deprecate many simple algorithms?

Algorithm solution:

std::generate(numbers.begin(), numbers.end(), rand);

Range-based for-loop solution:

for (int& x : numbers) x = rand();

Why would I want to use the more verbose std::generate over range-based for-loops in C++11?

8395 次浏览

Whether the for loop is range based or not does not make a difference at all, it only simplifies the code inside the parenthesis. Algorithms are clearer in that they show the intent.

In my opinion, the manual loop, though might reduce verbosity, lacks readabitly:

for (int& x : numbers) x = rand();

I would not use this loop to initialize1 the range defined by numbers, because when I look at it, it seems to me that it is iterating over a range of numbers, but in actuality it does not (in essence), i.e instead of reading from the range, it is writing to the range.

The intent is much clearer when you use std::generate.

1. initialize in this context means to give meaningful value to the elements of the container.

The first version

std::generate(numbers.begin(), numbers.end(), rand);

tells us that you want to generate a sequence of values.

In the second version the reader will have to figure that out himself.

Saving on typing is usually suboptimal, as it is most often lost in reading time. Most code is read a lot more than it is typed.

In my opinion, Effective STL Item 43: "Prefer algorithm calls to hand-written loops." is still a good advice.

I usually write wrapper functions to get rid of the begin() / end() hell. If you do that, your example will look like this:

my_util::generate(numbers, rand);

I believe it beats the range based for loop both in communicating the intent and in readability.


Having said that, I must admit that in C++98 some STL algorithm calls yielded unutterable code and following "Prefer algorithm calls to hand-written loops" did not seem like a good idea. Luckily, lambdas have changed that.

Consider the following example from Herb Sutter: Lambdas, Lambdas Everywhere.

Task: Find first element in v that is > x and < y.

Without lambdas:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

With lambda

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );

My answer would be maybe and no. If we're talkinng about C++11, then maybe (more like no). For example std::for_each is really annoying to use even with lambdas:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
// do stuff with x
});

But using range-based for is a lot better:

for (auto& x : c)
{
// do stuff with x
}

On the other hand, if we're talking about C++1y, then I would argue that no, the algorithms will not be obsoleted by range based for. In C++ standard committee there is a study group that is working on a proposal to add ranges to C++, and also there is work being done on polymorphic lambdas. Ranges would remove the need to use pair of iterators and polymorphic lambda would let you to not specify exact argument type of lambda. This means that std::for_each could be used like this (don't take this as a hard fact, it's just what the dreams look like today):

std::for_each(c.range(), [](x)
{
// do stuff with x
});

Personally, my initial reading of:

std::generate(numbers.begin(), numbers.end(), rand);

is "we're assigning to everything in a range. The range is numbers. The values assigned are random".

My initial reading of:

for (int& x : numbers) x = rand();

is "we're doing something to everything in a range. The range is numbers. What we do is assign a random value."

Those are pretty darn similar, but not identical. One plausible reason I might want to provoke the first reading, is because I think the most important fact about this code is that it assigns to the range. So there's your "why would I want to...". I use generate because in C++ std::generate means "range assignment". As btw does std::copy, the difference between the two is what you're assigning from.

There are confounding factors, though. Range-based for loops have an inherently more direct way of expressing that the range is numbers, than iterator-based algorithms do. That's why people work on range-based algorithm libraries: boost::range::generate(numbers, rand); looks better than the std::generate version.

As against that, int& in your range-based for loop is a wrinkle. What if the value type of the range isn't int, then we're doing something annoyingly subtle here that depends on it being convertible to int&, whereas the generate code only depends on the return from rand being assignable to the element. Even if the value type is int, I still might stop to think about whether it is or not. Hence auto, which defers thinking about the types until I see what gets assigned -- with auto &x I say "take a reference to the range element, whatever type that might have". Back in C++03, algorithms (because they're function templates) were the way to hide exact types, now they're a way.

I think it has always been the case that the simplest algorithms have only a marginal benefit over the equivalent loops. Range-based for loops improve loops (primarily by removing most of the boilerplate, although there's a little more to them than that). So the margins draw tighter and perhaps you change your mind in some specific cases. But there's a still a style difference there.

There are some things you cannot do (simply) with range-based loops that algorithms that take iterators as input can. For example with std::generate:

Fill the container up to limit (excluded, limit is a valid iterator on numbers) with variables from one distribution and the rest with variables from another distribution.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Iterator-based algorithms give you a better control on the range you are operating on.

For the particular case of std::generate, I agree with the previous answers on readability/intent issue. std::generate seems a more clear version to me. But I admit that this is in a way a matter of taste.

That said, I've have another reason to not throw away the std::algorithm - there are certain algorithms which are specialized for some data types.

The simplest example would be std::fill. The general version is implemented as a for-loop over the provided range, and this version will be used when instantiating the template. But not always. E.g. if you'll provide it a range which is a std::vector<int> - often it will actually call memset under the hood, yielding a much faster and better code.

So I'm trying to play an efficiency card here.

Your hand-written loop might be as fast as a std::algorithm version, but it can hardly be faster. And more than that, std::algorithm may be specialized for particular containers and types and it's done under the clean STL interface.

One thing that should be noticed is that an algorithm express what is done, not how.

Range-based loop include the way things are done: start with the first, apply and go next element until the end. Even a simple algorithm could do things differently (at least some overloads for specific containers, not even thinking about the horrible vector), and at least the way it is done is not the writer business.

TO me that's much of the difference, encapsulate as much as you can, and that justifies the sentence when you can, use algorithms.

Range-based for-loop is just that. Until of course standard is changed.

Algorithm is a function. A function which puts some requirements on its parameters. The requirements are phrased in a standard to allow for example implementation that takes advantage of all available execution threads and will speed you up automatically.