First world problems
Have you ever typed out a loop like this…
…just to realise that you actually need the index of the item as well? Inconveniently forcing you to rewrite the loop to…
I know I have.
Would it not be nice to be able to just do…
While at the same time incurring no overhead? Turns out this is possible, so let’s do it.
The enumerate function
To make it possible to pass the result of
enumerate(ts) to a range-based for, we need that object to implement a particular interface. It needs to be possible to call
end() on it, either as free functions or member functions. Those need to return objects that in turn support some functionality of the iterator concept, namely pre-increment, inequality comparison as well as dereferencing to access the item in every iteration.
For us to do what we want, we need to implement that functionality on a simple wrapper that basically just wraps the underlying container’s iterators while also keeping track of the current index.
Nothing magical going on here, just returning an instance of
enumerate_wrapper which wraps our container.
The enumerate_wrapper type
enumerate_wrapper type, let’s start by defining it along with some helpful type aliases that will prove usable later.
By defining these based on
std::is_const_v<container_type> we make sure that if
enumerate_wrapper is constructed from a const object, we will get the const versions of the iterators/pointers/references that the container provides. This will make our wrapper work for both const and non-const use cases.
Next up we need to define
end() for the wrapper.
begin() is going to be used by the loop to create the initial iterator that is then incremented for every iteration in the loop. It will also be dereferenced to access the value that is given to the user on the other side of the
: in the range-based for statement. This means that we need to wrap the begin-iterator in a custom iterator type that keeps track of the index and provides it along with the actual value too.
end() function does nothing special in the loop mechanism - it will only be used to compare against to see if we are at the end of the loop. Conveniently this means that we can just return the underlying container’s end-iterator.
We store the container inside the wrapper so that we can forward the calls to
std::end(). As explained above,
enumerate_wrapper_iter is a type we use to wrap the underlying iterator as well as keeping track of the current loop index. Let’s look at how to implement it.
The enumerate_wrapper_iter type
enumerate_wrapper_iter is used by the for loop to advance the loop, check for termination and passing the iteration values to the user code. To support these things, we need to implement the following.
operator++which should increment both the index and the underlying iterator
end()iterator to know when the loop is terminating
operator*which should return the current index along with the current item
The increment operator is hopefully clear. It simply increments both the index and the underlying iterator.
A thing that might seem unintuitive with the
operator!= is that despite
enumerate_wrapper_iter containing both
value, I chose to only compare
value for inequality. Why? Because the purpose of this operator is only to terminate the loop, and we are wrapping a container that already supports checking for loop termination using its own iterators. Hence we gain nothing by adding a comparison to
index as well. In that sense, the
index member can be viewed as a piece of meta-data which doesn’t take part in the mechanism itself - it only keeps track of the loop count, nothing else.
For the last one,
operator*, we need to return both the current index along with the value from the dereferenced underlying iterator.
std::pair seems like a handy way of doing this since it supports decomposition using structured bindings (meanings we can use it like
auto [a, b] = pair;.
That’s it, now we can use it as we wanted to. It supports both const containers as well as non-const ones and as a bonus since we used the type information provided by the container, it will work with other containers too like
std::map for instance.
How is the overhead?
I tested the code on Godbolt’s Compiler Explorer and aside from one or two instructions differing, it seems to compile down to the exact same binary code as if you’d be keeping track of the index manually alongside an iterator based loop, not bad.
A word of warning regarding structured bindings
enumerate() function works as intended, there are some pitfalls that are good to know about regarding structured bindings. For example, look at the following code.
const auto before the structured binding might make you think that both
t are copied by value and stored as const copies but this is in fact not the case. Remember, the value we return when dereferencing the
enumerate_wrapper_iter is of the type
std::pair<size_t, T&> and the const-copying will apply to this value itself - not what it contains. So
t will be references to entries that live in a
const std::pair<size_t, T&> which is a copy of what was returned from the
enumerate_wrapper_iter instance. Confusing? That is unfortunately the cryptic way that structured bindings work.
This means that if you want to use
enumerate() to loop over a container where the value in each iteration is a copy, tough luck! You have to explicitly copy it. Also it means that if you run
enumerate() on a container that is not const, then the item of each loop is going to be a modifyable reference, no matter if you do
const auto& [i, t] or
auto [i, t] or whatever. It is a shame that we can’t control the const-ness of each member of the structured binding.
Maybe future standards will give us better tools to decompose values with more control.
Thanks for reading!
The code is uploaded on github as a single header. Feel free to grab it, use it, modify it.