Click here to Skip to main content
14,692,582 members
Articles » Languages » C / C++ Language » Templates
Tip/Trick
Posted 18 Sep 2015

Tagged as

Stats

15.1K views
13 bookmarked

Python-like Enumeration in C++11

Rate me:
Please Sign up or sign in to vote.
4.36/5 (12 votes)
23 Oct 2015CPOL
Classes that provide simple Pythonic enumeration of STL container items using range-for loop in C++11

Introduction

Range-based for loops in C++11 are a nice addition to the language. However, there is still no support in the STL for enumerating items in a container without creating an extra counter variable that must be incremented separately from the looping variable:

std::list<int> list = { 1, 2, 3, 4, 5, 6, 7 };

int index = 0;  // this is outside the loop block although only used inside it
for (auto x : list)
{
    cout << index << " " << x << endl;
    index ++;  // this has to be incremented manually; error-prone in more complex loop logic
}

Note that the index is not to access the items in the container; it is to keep track of how many items have been processed so far in the loop.

I would like to be able to enumerate items in an STL container as simply as it can be done in Python, i.e. via a generic "enumerate()":

# Python code: 
list = { "a", "b", "c", "d", "e", "f" }

for index, x in enumerate(list):
    print(index, x)

In C++11, it could look like this:

std::list<int> list = { 1, 2, 3, 4, 5, 6, 7 };

for (auto x : enumerate(list))
    cout << x.index << " " << x.value << endl;

This is concise and clear. The next section provides some background. The main section shows the code that supports the above syntax and more. Finally, some possible extensions. 

Background

I'm a big fan of Python's simplicity when it comes to looping over containers via ranges, comprehensions, iterators, generators, and enumerations, rather than C++ STL's "philosophy" that you should always be working with iterators. I find it overly verbose, and the same level of genericity can now likely be achieved in C++11 by dealing with the containers directly. So I wrote some classes that enable Python-style enumeration looping.

Using the Code

Put the following in a .h file in your project:

template <typename Container>
class EnumIter
{
public:
    typedef decltype(std::begin(std::declval<Container>())) iterator;

    EnumIter(iterator begin) : iter_(begin) 
    {
        // cout << typeid(iterator).name() << endl;
    }

    EnumIter& operator++()
    {
        iter_++;
        index_++;
        return *this;
    }

    bool operator!=(const EnumIter& rhs)
    {
        return iter_ != rhs.iter_; // or self.index_ != rhs.index_;
    }

    using iter_ref = typename std::iterator_traits<iterator>::reference;

    std::pair<int, iter_ref> operator*() const
    {
        return { index_, *iter_ };
    }

private:
    iterator iter_;
    int index_ = 0;
};

template <typename Container>
class EnumerationAdaptor
{
public:
    EnumerationAdaptor(Container& container) : container_(container) {}
    EnumIter<Container> begin() const { return std::begin(container_); }
    EnumIter<Container> end() const { return std::end(container_); }

private:
    Container& container_;
};

template <typename Container>
EnumerationAdaptor<Container> 
	enumerate(Container& container) { return container; }

template <typename Container>
EnumerationAdaptor<const Container> 
	const_enumerate(const Container& container) { return container; }

and add the necessary header inclusions. You may have minor adjustments to make based on your compiler. I tested this with MSVC++ 2015.

Points of Interest

The top-level API is simple to fulfill via a "container wrapper" which provides what the range-based for loop needs: begin() and end() methods that return a forward iterator (see http://en.cppreference.com/w/cpp/language/range-for for details).

template <typename Container>
class EnumerationAdaptor
{
public:
    EnumerationAdaptor(Container& container) : container_(container) {}
    EnumIter<Container> begin() const { return container_.begin(); }
    EnumIter<Container> end() const { return container_.end(); }

private:
    Container& container_;
};

template <typename Container>
EnumerationAdaptor<Container> enumerate(Container& container) { return container; }

template <typename Container>
EnumerationAdaptor<const Container> const_enumerate(const Container& container) { return container; }

If we only needed to enumerate items of a non-const container, we could implement EnumIter like this:

template <typename Container>
class EnumIter
{
public:
    typedef typename Container::iterator iterator;

    EnumIter(iterator begin) : iter_(begin) {}

    EnumIter& operator++()
    {
        iter_++;
        index_ ++;
        return *this;
    }

    bool operator!=(const EnumIter& rhs) const
    {
        return iter_ != rhs.iter_;
    }

    typedef typename iterator::reference iter_ref;

    std::pair<int, iter_ref> operator*() const
    {
        return { index_, *iter_ };
    }

private:
    iterator iter_;
    int index_ = 0;
};

This would be sufficient to support the first (i.e., non-const) loop shown in the Introduction section. A problem arises when extending this to support const enumeration: the iterator to use must be of type Container::const_iterator, not Container::iterator. In C++03, this could easily be achieved by using a couple of small templates to "extract" the iterator type based on whether the Container template argument is actually a const container or not:

template <typename Container>
struct IterType {
    typedef typename Container::iterator type;
};

template <typename Container>
struct IterType<const Container> {
    typedef typename Container::const_iterator type;
};

template <typename Container>
class EnumIter
{
public:
    typedef typename IterType<Container>::type iterator;

... rest is same...
};

But really the iterator type is the type of object returned by the begin() chosen by the compiler in the EnumIter class: for a const container, the compiler will chose const_iterator begin() const, whereas for a non-const container, it will choose iterator begin(). In C++11, there is decltype() which can be used in the typedef, and we no longer need the IterType templates:

template <typename Container>
class EnumIter
{
public:
    typedef decltype(Container().begin()) iterator;

... rest is same ...
};

The instance given to decltype is never actually created at run-time, it is just used by the compiler to deduce type. Interestingly, in Visual C++ 2015 (used in Microsoft Visual Studio 2015 Express) this results in a C2440 error, but in GCC and Clang (according to http://stackoverflow.com/questions/32544354/template-parameter-for-const-iterator-instead-of-iterator) it works. Even if this technique should work in standard-compliant compilers, it limits us to default-constructible containers: not a big deal (every container I've ever worked with is default-constructible), but since there is better, might as well use it: use declval which is provided precisely for this task (see http://en.cppreference.com/w/cpp/utility/declval for details).

template <typename Container>
class EnumIter
{
public:
    typedef decltype((std::declval<Container>()).begin()) iterator;

... rest is same ...
};

There are other ways to select the right iterator type, as mentioned in a comment by T.C. on http://stackoverflow.com/questions/32544354/template-parameter-for-const-iterator-instead-of-iterator):

template <typename Container>
class EnumIter
{
public:
    using iterator = std::conditional_t<
        std::is_const<Container>::value, 
        typename Container::const_iterator, 
        typename Container::iterator>;

This statement is understandable just by reading it (i.e., you can guess what each piece does just by the names used), whereas the workings of the previous one is hidden in decltype and declval. A range-based for loop requires that a begin()/end() method or global function exist on the range container, so there is no negative in using the decltype/declval approach other than readbility. But if the problem were different, such that begin()/end() were not already required, then the decltype/declval approach would constrain our EnumIter to containers that define begin()/end() methods, whereas the conditional approach would constain our code to containers that define iterator and const::iterator.

To enumerate raw arrays, you can wrap your array with std::vector<array type>(std::begin(array), std::end(array)) but this is verbose and involves a temporary (and wasteful) copy of the array data into the vector. It is better to fix the EnumIter so it can accommodate raw arrays. An iterator for a raw array is just a pointer to the type of object in the array. The only issue is that the "using iterator" in the above code snippet uses Container::iterator: it gets the iterator type member of the Container class: arrays don't have such a type member. We could go back to the trusty template-specialization trick used earlier by creating a specialization of IterType for ArrayType*:

template <typename ArrayVal, std::size_t size>
struct IterType<ArrayVal[size]>
{
    //typedef typename std::conditional<std::is_const<ArrayVal>::value, 
    //ArrayVal* const, ArrayVal*>::type type;
    typedef ArrayVal* type;
    typedef ArrayVal& reference;
};

Turns out decltype is worth revisiting, only two modifications are needed in EnumIter (This is like a real ping-pong match!):

template <typename Container>
class EnumIter
{
public:
    typedef decltype(std::begin(std::declval<Container&>())) iterator;

...

    using iter_ref = typename std::iterator_traits<iterator>::reference;

The first typedef uses the same combination of decltype and declval as earlier, but this time via the standard library std::begin() function, since it defines overloads for raw arrays. So when Container is int[N], iterator is int*. The second modification is the iter_ref: for raw arrays of type T, iterator is T*, which does not have reference as a type member. The iterator_traits defines an overload for raw arrays. This change allows you to use the enumerate and const_enumerate as follows:

int raw_array[] = { 1,2,3,4,5 };

for (auto x : enumerate(raw_array))
{
    cout << "array before:" << x.first << " " << x.second << endl;
    x.second += 10;
    cout << raw_array[x.first] << endl;
}

for (auto x : const_enumerate(raw_array))
{
    cout << "array before:" << x.first << " " << x.second << endl;
    //x.second += 10;  // compile error
}

Improvements and Further Reading

Lots of improvements could be made. Some of the following were pointed out by some readers: 

  • Change EnumIter so it could be used in STL algorithms etc: change the template parameter to use iterator type instead of container type
  • EnumIter, being an iterator type, should not have an iterator typedef
  • Define the type members needed to define its traits and make it usable in iterator_traits<>; possibly derive from std::iterator
  • Replace std::pair by custom type that provides member names clearly related to enumeration such as "index" and "value", instead of the generic "first" and "second"
  • Support EnumIter dereference via -> operator
  • Make EnumIter support operations available on value iterator, such as random access

Those who like this article may find interesting the CppCon 2015 presentations by Stroustrup and Sutter on the C++ Core Guidelines. 

History

  • v2015-09-14: Draft

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

schollii
Software Developer (Senior)
Canada Canada
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
koothkeeper22-Sep-15 17:44
professionalkoothkeeper22-Sep-15 17:44 
QuestionC++11 can already do this. Pin
Member 437908122-Sep-15 0:44
professionalMember 437908122-Sep-15 0:44 
AnswerRe: C++11 can already do this. Pin
schollii22-Sep-15 16:40
Memberschollii22-Sep-15 16:40 
GeneralRe: C++11 can already do this. Pin
Member 437908123-Sep-15 0:50
professionalMember 437908123-Sep-15 0:50 
GeneralRe: C++11 can already do this. Pin
schollii23-Sep-15 17:15
Memberschollii23-Sep-15 17:15 
GeneralRe: C++11 can already do this. Pin
qzq28-Oct-15 21:43
Memberqzq28-Oct-15 21:43 
GeneralRe: C++11 can already do this. Pin
schollii18-Dec-15 18:32
Memberschollii18-Dec-15 18:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.