Python-Like enumerate() In C++17
Python has a handy built-in function called enumerate()
,
which lets you iterate over an object (e.g. a list) and have access to both the index and the
item in each iteration. You use it in a for
loop, like this:
for i, thing in enumerate(listOfThings):
print("The %dth thing is %s" % (i, thing))
Iterating over listOfThings
directly would give you thing
, but not i
, and there are plenty of
situations where you’d want both (looking up the index in another data structure, progress reports,
error messages, generating output filenames, etc).
C++ range-based for
loops work a lot like
Python’s for
loops. Can we implement an analogue of Python’s enumerate()
in C++? We can!
C++17 added structured bindings
(also known as “destructuring” in other languages), which allow you to pull apart a tuple type and
assign the pieces to different variables, in a single statement. It turns out that this is also
allowed in range for
loops. If the iterator returns a tuple, you can pull it apart and assign the
pieces to different loop variables.
The syntax for this looks like:
std::vector<std::tuple<ThingA, ThingB>> things;
...
for (auto [a, b] : things)
{
// a gets the ThingA and b gets the ThingB from each tuple
}
So, we can implement enumerate()
by creating an iterable object that wraps another iterable and
generates the indices during iteration. Then we can use it like this:
std::vector<Thing> things;
...
for (auto [i, thing] : enumerate(things))
{
// i gets the index and thing gets the Thing in each iteration
}
The implementation of enumerate()
is pretty short, and I present it here for your use:
#include <tuple>
template <typename T,
typename TIter = decltype(std::begin(std::declval<T>())),
typename = decltype(std::end(std::declval<T>()))>
constexpr auto enumerate(T && iterable)
{
struct iterator
{
size_t i;
TIter iter;
bool operator != (const iterator & other) const { return iter != other.iter; }
void operator ++ () { ++i; ++iter; }
auto operator * () const { return std::tie(i, *iter); }
};
struct iterable_wrapper
{
T iterable;
auto begin() { return iterator{ 0, std::begin(iterable) }; }
auto end() { return iterator{ 0, std::end(iterable) }; }
};
return iterable_wrapper{ std::forward<T>(iterable) };
}
This uses SFINAE to ensure it can only be applied to iterable types, and will generate readable error messages if used on something else. It accepts its parameter as an rvalue reference so you can apply it to temporary values (e.g. directly to the return value of a function call) as well as to variables and members.
This compiles without warnings in C++17 mode on gcc 8.2, clang 6.0, and MSVC 15.9. I’ve banged on it a bit to ensure it doesn’t incur any extra copies, and it works as expected with either const or non-const containers. It seems to optimize away pretty cleanly, too! 🤘