24

Python-Like enumerate() In C++17

 5 years ago
source link: https://www.tuicool.com/articles/hit/YbmIJjf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK