No Raw For Loops: Assigning to a Data Member
A few years ago Sean Parent presented his famous C++ Seasoning talk, where he recommended avoiding raw for loop and using STL algorithms instead.
This made a lot of people sensitive to this topic, and encouraged us to think about how to convert the for loops in our code into more declarative constructs.
Recently I encountered a very simple for loop, but that I couldn’t see how to replace with an STL algorithm. Let’s see more modern techniques we can use to transform this for loop into declarative code.
The loop
Example 1
Consider this structure P
:
struct P { int x = 0; int y = 0; };
We have a collection of zero-initialised P
s:
auto ps = std::vector<P>(5);
And a collection of values for x
:
auto const xs = std::vector<int>{1, 2, 3, 4, 5};
We’d like to set each of the x
in the collection of P
with its counterpart in xs
.
Here is how to do it with a for loop:
for (int i = 0; i < 5; ++i)) { ps[i].x = xs[i]; }
Now if x
was a private member in P
, and we could set it by using a setter setX
, then the for loop would look like that:
for (int i = 0; i < 5; ++i)) { ps[i].setX(xs[i]); }
Those for loops are very simple, but it’s because they do only that and because they use vector
that can be indexed.
The loop would become a little more complex if it used a std::map
for example.
Example 2
To illustrate, let’s consider a map that associates int
s to std::string
s:
auto entries = std::map<int, std::string>{ {1,""}, {2,""}, {3,""}, {4,""}, {5,""} };;
We’d like to fill the values of this map with the values in this vector:
auto const values = std::vector<std::string>{"one", "two", "three", "four", "five"};
Then the for loop to do this is not as straightforward as the one in Example 1, because the map cannot be accessed with an index:
auto current = 0; for (auto& entry : entries) { entry.second = values[current]; ++current; }
This loop is already too complex, in my opinion. Indeed, we have to run it in our head to understand what it does, and keep a mental register for the value of current
.
The loops would be even more difficult to read if they were doing more operations, such as testing predicates, applying functions, or performing any other operations.
How can we re-write those two loops with declarative code instead?
The first option that comes to mind is to use STL algorithms. But I can’t see what algorithm can help us here. If you see one, please leave a comment showing you would rewrite for loops with it.
To rewrite those for loops we’re going to see two different ways, one using ranges and one using pipes.
Rewriting the code with ranges
Not having access to a C++20 compiler implementing ranges yet, we’re going to use the range-v3 library as an implementation of C++ ranges. For a refresher on ranges, you can check out this introduction on ranges.
Here we’re accessing an element within a structure. It’s like applying a function on the structure, that returns the member. The operation that comes to mind related to ranges is therefore transform
.
But transform
is generally applied on the input data, whereas here we need to apply it on the result where the input is to be stored.
Example 1
We therefore apply transform
on the output:
ranges::copy(xs, begin(ps | ranges::view::transform(&P::x)));
This seems to work. But to call the setX
member function, I don’t think this is possible with ranges. If you see how to do it, please leave a comment.
Example 2
With a map, the expression is more verbose. We can emulate the range adaptor coming in C++20 that is called values
:
auto view_values = ranges::view::transform(&std::pair<int const, std::string>::second); ranges::copy(values, (entries | view_values).begin());
Rewriting the code with pipes
Contrary to ranges that follow a pull model (an adapted range fetches data from the one before it), pipes follow a push model (a pipe send data to the one after it).
For this reason, ranges are flexible to handle inputs, and pipes lead to natural code when it comes to handling outputs.
Example 1
We can use the override
pipe. override
takes a collection and writes the values it receives to the successive positions of this collection.
A recent version of override
allows to write over a data member of the values in the outputs collections, which is what we need in our example with the P
structure:
xs >>= pipes::override(ps, &P::x);
Another overload of override
takes a member function and sends the data it receives to that member function. This allows us to write the case using the setter this way:
xs >>= pipes::override(ps, &P::setX);
Example 2
Here too, the example with the map is more verbose. But we can write it following the same pattern:
xs >>= pipes::override(results, &std::pair<int const, std::string>::second);
Various tools at your disposal
It is interesting to see that we can twist ranges away from their common use cases, and that they allow to do basic operations on outputs. The above code should closely look like what C++20 allows to do.
For those particular examples pipes give the most natural code, because they are designed to handle outputs, with their push model of pipes receiving data and handling it in elaborate ways.
Whichever the particular implementation you decide to use, it is important to be aware of the many tools at your disposal to do away with raw loops, write in a declarative style instead, to raise the level of abstraction of your code.
You will also like
- The C++ Pipes
- Introduction to the C++ Ranges Library
- Smart Output Iterators >>= become(Pipes)
- Why You Should Use std::for_each over Range-based For Loops
- How to Access the Index of the Current Element in a Modern For Loop
Share this post!