Jonathan Boccara's blog

How to Make for_each Stop After N Elements

Published February 14, 2020 - 0 Comments

for_each is an STL algorithm that takes a range (in the form of two iterators) and a function, and applies the function to each element of the range:

std::for_each(begin(v), end(v), f); // applies f to each element of v

It is arguably the simplest algorithm of the STL library. But it is so simple that sometimes it seems almost too simple.

Indeed, it happens sometimes (for example in situations pointed out by Stefano and Martin), that we want for_each to apply the function to the first elements of the collection, and stop at some point. But for_each doesn’t allow that. Once you call it, it applies the function to the whole collection.

Let’s see how to make for_each stop before the end of the collection.

We can define the point where to stop in the collection in two ways:

  • stop after N elements (this post),
  • stop when a condition becomes true on an element (the next post).

for_each_n

For the first case, the STL offers a (questionable, as we’ll see in a moment) solution since C++17, with std::for_each_n.

Like the other _n algorithms, std::for_each_n take a begin and a number of elements N, and it applies the function to the first N elements:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::for_each_n(begin(numbers), 3, [](int& i){ i *= 10; });

If you don’t have access to C++17 yet, or if your standard library implementation hasn’t caught up with for_each_n yet, this algorithm can be implemented with C++98, as in the implementation suggested on cppreference.com:

template<class InputIt, class Size, class UnaryFunction>
InputIt for_each_n(InputIt first, Size n, UnaryFunction f)
{
    for (Size i = 0; i < n; ++first, (void) ++i) {
        f(*first);
    }
    return first;
}

Or you can use the classic for_each and pass it an iterator pointing to the inside of the collection instead of the end:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::for_each(begin(numbers), begin(numbers) + 3, [](int& i){ i *= 10; });

But all those solutions have drawbacks.

The drawbacks of for_each_n

for_each_n is convenient because it is in the standard library. But this is probably the only advantage there is to it. On the other hand, for_each_n has several drawbacks.

A pitfall

The first drawback is that it is dangerous! Indeed, in for_each_n we don’t give the end of the range. We only pass in the beginning and the number of elements we want to apply the function on.

What happens if we pass a non-null number and the collection is empty? Or more generally if it contains less elements than the number we pass it?

Then the program gets into undefined behaviour! The application can crash for example. This is all the more dangerous if we use it on containers such as std::vector (and not std::array for example), which size is variable at runtime.

One way to prevent this from happening is to cap the number of elements to the size of the collection:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::for_each_n(begin(numbers), std::min(3, numbers.size()), [](int& i){ i *= 10; });

But this code doesn’t compile. Indeed, std::min expects two arguments of the same type. And 3 is an int whereas numbers.size() is a size_t which is often an unsigned int. One way to make this code compile is to add a static_cast:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::for_each_n(begin(numbers), std::min(static_cast<size_t>(3), numbers.size()), [](int& i){ i *= 10; });

or like we saw in how to handle multiple types in max without a cast, we can specify the template parameter of min:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::for_each_n(begin(numbers), std::min<size_t>(3, numbers.size()), [](int& i){ i *= 10; });

Still, this is not the most expressive code we can imagine to express the simple concept of applying a function to the first N elements of a collection.

A non-modular design

Beyond that pitfall related to the size of the collection, for_each_n shows an issue in its design, as it is not modular.

If we’d like to apply the transform algorithm to the first N elements of a collection then we’d need yet another algorithm, transform_n. And if we’d like to search a value in the first N elements of the collection, we’d need a find_n algorithm. This design of multiplicating algorithms doesn’t scale well.

Modern C++ offers solutions that allow to apply a function to the first N elements of a collection, without those two drawbacks. Let’s see two: ranges and pipes.

Applying a function on the first N elements, with ranges

Ranges are a new addition to the standard in C++20. Before C++20, the range-v3 library implements most of what is in the C++20 standard (and a lot more things too).

Here is how to apply a function to the first N elements of a collection with ranges:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};
std::ranges::for_each(numbers | std::views::take(3), [](int& i){ i *= 10; });

This code uses two components of the ranges library:

  • the take(3) range adaptor. It combines with a range (here, numbers) and creates a view that gives access to the first 3 elements of the underlying range,
  • the ranges::for_each algorithm: it is essentially like for_each but taking a range instead of two iterators.

This solves the two problems we saw of for_each_n:

  • ranges::view::take creates a view over the whole underlying range if the number we pass is it larger than the size of the range,
  • the design is modular: we can reuse take with other algorithms and range adaptors of the library.

Note that I don’t use ranges::view::transform here because it has a semantics of producing an output, and not performing a side-effect on the input.

Applying a function on the first N elements, with pipes

Pipes is a library allowing to create pipelines for expressive code with collections in C++.

Here is how to apply a function to the first N elements of a collection with pipes:

auto numbers = std::vector<int>{1, 2, 3, 4, 5};

numbers
>>= pipes::take(3)
>>= pipes::for_each([](int& i){ i *= 10; });

This also solves the two issues of for_each_n:

  • pipes::take just stops sending data to the next pipe (here, for_each) after it received N values,
  • the design is modular, we can reuse take with other pipes.

Applying a function until a condition is met

Another way to stop before the end of the collection is to apply the function to elements until they start meeting a certain condition.

The challenges to express this in code are different than those we saw for applying a function to the first N elements. We will explore this other way of stopping before the end of the collection in the next post. Stay tuned!

You will also like

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