C++ · Software

Range-based for loops with counters

In C++11 we got range-based for loops which allow one to iterate over the elements of a container easily. In some situations, however, having sequential access to each element of the container is not sufficient for the task that shall be done in the loop body. Commonly an additional counting index is required. In this case the auxiliary counting variable has to be declared outside of the loop, which pollutes the surrounding namespace. Furthermore, the counting index is not increased automatically in each round.

std::size_t i=0; 
for (auto v : {3.14, 42., 77.}) {
  // do something with value v and index i
  ++i;
}

Python has a build-in function enumerate which solves this task in a much more elegant way. It returns an iterator to iterate over the values of a container and over a set of indices simultaneously.

l=['foo', 'bar', 42]
for i, value in enumerate(l):
    print(i, value)

This very nice Python syntax relies on tuple unpacking. With structured binding declarations, a similar language feature has been introduced to C++, too. This allows us to implement Python’s enumerate function in modern C++. First, we need an iterator adapter, which wraps an iterator into a pair of an iterator and an integer index.

template<typename iterator_type>
class enumerate_iterator {
public:
  using iterator=iterator_type;
  using reference=typename std::iterator_traits<iterator>::reference;
  using index_type=typename std::iterator_traits<iterator>::difference_type;
private:
  iterator iter;
  index_type index=0;
public:
  enumerate_iterator()=delete;
  explicit enumerate_iterator(iterator iter, index_type start)
    : iter(iter), index(start) {
  }
  enumerate_iterator &operator++() {
    ++iter;
    ++index;
    return *this;
  }
  bool operator==(const enumerate_iterator &other) const {
    return iter==other.iter;
  }
  bool operator!=(const enumerate_iterator &other) const {
    return iter!=other.iter;
  }
  std::pair<reference, const index_type &> operator*() const {
    return { *iter, index };
  }
};

A range-based for loop requires an object for which begin and end member functions or free functions are defined. Thus, we write a pseudo container, which wraps a range given by two iterators into a range of enumerate iterators, as defined above.

template<typename iterator_type>
class enumerate_range {
public:
  using iterator=enumerate_iterator<iterator_type>;
  using index_type=typename std::iterator_traits<iterator_type>::difference_type;
private:
  const iterator_type first, last;
  const index_type start;
public:
  enumerate_range()=delete;
  explicit enumerate_range(iterator_type first, iterator_type last,
               index_type start=0)
    : first(first), last(last), start(start) {
  }
  iterator begin() const {
    return iterator(first, start);
  }
  iterator end() const {
    return iterator(last, start);
  }
};

Now, we are already almost finished. With the two helper classes enumerate_iterator and enumerate_range, the implementation of the enumerate function reduces to the creation of an enumerate_range object from the function’s parameters. For convenience, the enumerate function is overloaded and has a default starting index zero.

// convert a range given by two iterators [first, last) into a range
// of enumerate iterators
template<typename iterator_type>
decltype(auto) enumerate(iterator_type first, iterator_type last,
             typename std::iterator_traits<iterator_type>::difference_type start=0) {
  return enumerate_range(first, last, start);
}

// convert an initializer list into a range of enumerate iterators
template<typename type>
decltype(auto) enumerate(const std::initializer_list<type> &content,
             std::ptrdiff_t start=0) {
  return enumerate_range(content.begin(), content.end(), start);
}

// convert a C-array into a range of enumerate iterators
template<typename type, std::size_t N>
decltype(auto) enumerate(type (&content)[N],
             std::ptrdiff_t start=0) {
  return enumerate_range(content, content+N, start);
}

Utilizing structured bindings, a range-based for loop with counter becomes with the enumerate function:

const std::vector<int> a({1, 2, 3, 4, 5, 6, 7, 8});
for (auto [value, index] : enumerate(a)) 
  std::cout << index << '\t' << value << '\n';

Finally, the listing below shows some working examples of range-based for loops with a counter variable in C++17. Note the versatile usage of the enumerate function as illustrated in the main routine.

#include <cstdlib>
#include <iostream>
#include <iterator>
#include <initializer_list>
#include <vector>
#include <set>

// wraps an iterator into a pair of an iterator and an integer index
template<typename iterator_type>
class enumerate_iterator {
public:
  using iterator=iterator_type;
  using reference=typename std::iterator_traits<iterator>::reference;
  using index_type=typename std::iterator_traits<iterator>::difference_type;
private:
  iterator iter;
  index_type index=0;
public:
  enumerate_iterator()=delete;
  explicit enumerate_iterator(iterator iter, index_type start)
    : iter(iter), index(start) {
  }
  enumerate_iterator &operator++() {
    ++iter;
    ++index;
    return *this;
  }
  bool operator==(const enumerate_iterator &other) const {
    return iter==other.iter;
  }
  bool operator!=(const enumerate_iterator &other) const {
    return iter!=other.iter;
  }
  std::pair<reference, const index_type &> operator*() const {
    return { *iter, index };
  }
};

// pseudo container, wraps a range given by two iterators [first, last)
// into a range of enumerate iterators
template<typename iterator_type>
class enumerate_range {
public:
  using iterator=enumerate_iterator<iterator_type>;
  using index_type=typename std::iterator_traits<iterator_type>::difference_type;
private:
  const iterator_type first, last;
  const index_type start;
public:
  enumerate_range()=delete;
  explicit enumerate_range(iterator_type first, iterator_type last,
               index_type start=0)
    : first(first), last(last), start(start) {
  }
  iterator begin() const {
    return iterator(first, start);
  }
  iterator end() const {
    return iterator(last, start);
  }
};

// convert a conatainer into a range of enumerate iterators
template<typename container_type>
decltype(auto) enumerate(container_type &content,
             typename std::iterator_traits<typename container_type::iterator>::difference_type start=0) {
  using ::std::begin;
  using ::std::end;
  return enumerate_range(begin(content), end(content), start);
}

// convert a range given by two iterators [first, last) into a range
// of enumerate iterators
template<typename iterator_type>
decltype(auto) enumerate(iterator_type first, iterator_type last,
             typename std::iterator_traits<iterator_type>::difference_type start=0) {
  return enumerate_range(first, last, start);
}

// convert an initializer list into a range of enumerate iterators
template<typename type>
decltype(auto) enumerate(const std::initializer_list<type> &content,
             std::ptrdiff_t start=0) {
  return enumerate_range(content.begin(), content.end(), start);
}

// convert a C-array into a range of enumerate iterators
template<typename type, std::size_t N>
decltype(auto) enumerate(type (&content)[N],
             std::ptrdiff_t start=0) {
  return enumerate_range(content, content+N, start);
}

int main() {
  // test the enumberate function
  {
    int a[8]={1, 2, 3, 4, 5, 6, 7, 8};
    // note that value and index have a reference type
    for (auto [value, index] : enumerate(a)) {
      value*=2;
      std::cout << index << '\t' << value << '\n';
    }
    std::cout << '\n';
  }
  {
    for (auto [value, index] : enumerate({4, 2, 1, 3, 8, 7, 6, 5})) 
      std::cout << index << '\t' << value << '\n';
    std::cout << '\n';
  }
  {
    const std::vector<int> a({1, 2, 3, 4, 5, 6, 7, 8});
    for (auto [value, index] : enumerate(a, 8)) 
      std::cout << index << '\t' << value << '\n';
    std::cout << '\n';
  }
  {
    const std::set<int> a({4, 2, 1, 3, 8, 7, 6, 5});
    for (auto [value, index] : enumerate(a)) 
      std::cout << index << '\t' << value << '\n';
    std::cout << '\n';
  }
  return EXIT_SUCCESS;
}

3 thoughts on “Range-based for loops with counters

  1. Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container.

  2. Doesn’t the second test case, using the std::initializer_list, cause an object lifetime issue ? The lifetime of the value by the range expression (the enumerate() function call) is extended until the end of the loop, but that doesn’t extend to the initializer list as far as I can tell. gcc’s address sanitizer reports a stack-use-after-scope error.

  3. Dear Laurent, you are right the code above has an lifetime issue. To solve this issue one can overload the enumerate function for rvalue references and moving the argument. E.g.:

    template
    decltype(auto) enumerate(std::initializer_list &&content,
    std::ptrdiff_t start=0) {
    // overloaded enumerate_range constructor must move the list into the enumerate_range object
    return enumerate_range(content, start);
    }

Leave a Reply

Your email address will not be published. Required fields are marked *