How to Make for_each Stop After N Elements
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 likefor_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
- Is std::for_each obsolete?
- The Pipes library
- Why You Should Use std::for_each over Range-based For Loops
- for_each_arg: Applying a Function to Each Argument of a Function in C++
- The World Map of C++ STL Algorithms
Share this post!