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; for (auto x : list)
{
cout << index << " " << x << endl;
index ++; }
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()
":
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)
{
}
EnumIter& operator++()
{
iter_++;
index_++;
return *this;
}
bool operator!=(const EnumIter& rhs)
{
return iter_ != rhs.iter_; }
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 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;
}
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