Home » Idioms in Software Development: The Iterator Protocol

Idioms in Software Development: The Iterator Protocol

by admin
Idioms in Software Development: The Iterator Protocol

If a user-defined data type MyType is to be used in a range-based for loop, MyType must implement the Iterator Protocol.

What must a user-defined data type be like in order to be used in a range-based for loop? I will get to the bottom of this question below.

I want to start with a simple experiment and a std::array use in C++ Insights. Here is a simple example:

// iteratorProtocol.cpp

#include 

int main() {
   
    std::array myArr{1, 2, 3, 4, 5};
    for (auto a: myArr) a;
  
}

From this, C++ Insights generates the following code:

#include 

int main()
{
  std::array myArr = {{1, 2, 3, 4, 5}};
  {
    std::array & __range1 = myArr;
    int * __begin1 = __range1.begin();
    int * __end1 = __range1.end();
    for(; __begin1 != __end1; ++__begin1) {
      int a = *__begin1;
      a;
    }
    
  }
  return 0;
}

In more general terms: Should a range-based for loop be used (for(range_declaration : range_expression)), the compiler produces the following code:

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;      
  auto __end = end_expr;          
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

  • begin_expr and end_expr: return an iterator object
  • Iterator object:
    • operator++: Increases the iterator
    • operator*: Dereferencing the iterator and accessing the current element
    • operator!=: Compare the iterator with another iterator

begin_expr and end_expr call the crucial functions begin and end in the range_expression on. begin and end can be either member functions or free functions of the range_expression be.

Now I apply the theory and create a number generator.

The first implementation supports the iterator protocol.

The following class Generator supports the elementary iterator protocol.

// iterator.cpp

#include 

class Generator {
    int begin_{};
    int end_{};

public:
    Generator(int begin, int end) : begin_{begin}, end_{end} {}

    class Iterator {
        int value_{};
    public:
        explicit Iterator(int pos) : value_{pos} {}

        int operator*() const { return value_; }           // (3)

        Iterator& operator++() {                           // (4)
            ++value_;
            return *this;
        }

        bool operator!=(const Iterator& other) const {      // (5)
            return value_ != other.value_;
        }
    };

    Iterator begin() const { return Iterator{begin_}; }     // (1)
    Iterator end() const { return Iterator{end_}; }         // (2)
};

int main() {

   std::cout << 'n';
    
   Generator gen{100, 110};
   for (auto v : gen) std::cout << v << " ";

   std::cout << "nn";

}

The class Generator has member features begin and end (lines 1 and 2) that return iterator objects created with begin_ and end_ are initialized. begin_ and end_ stand for the range of generated numbers. Let me the inner class Iterator analyze that controls the generated numbers.

  • operator* returns the current value
  • operator++ increases the current value
  • operator!= compares the current value with the end_-Marke.

This results in the following output of the program:

I want the iterator created by begin() and end() is returned, generalize it and make it a forward iterator. After that, the class can Generator used in most of the Standard Template Library algorithms. For example, the associative containers support a forward iterator.

The following improved Generator has an inner class Iteratorwhich is a forward iterator.

// forwardIterator.cpp

#include 
#include 

class Generator {
    int begin_{};
    int end_{};

 public:
    Generator(int begin, int end) : begin_{begin}, end_{end} {}

    class Iterator {
        using iterator_category = std::forward_iterator_tag;    // (1)
        using difference_type   = std::ptrdiff_t;
        using value_type        = int;
        using pointer           = int*;
        using reference         = int&;
        int value_{};
     public:
        explicit Iterator(int pos) : value_{pos} {}

        value_type operator*() const { return value_; }
        pointer operator->() { return &value_; }                // (2)         

        Iterator& operator++() {                           
            ++value_;
            return *this;
        }
        Iterator operator++(int) {                              // (3)
            Iterator tmp = *this; 
            ++(*this); 
            return tmp; 
        }
                                                                // (4)
        friend bool operator==(const Iterator& fir, const Iterator& sec) {      
            return fir.value_ == sec.value_;
        }
        friend bool operator!=(const Iterator& fir, const Iterator& sec) {      
            return fir.value_ != sec.value_;
        }
    };

    Iterator begin() const { return Iterator{begin_}; }     
    Iterator end() const { return Iterator{end_}; }         
};

int main() {

    std::cout << 'n';
    
    Generator gen{1, 11};
    for (auto v : gen) std::cout << v << " ";                  // (5)

    std::cout << "nn";
                                                               // (6)
    std::cout << "sum:  " << std::accumulate(std::begin(gen), std::end(gen), 0);

    std::cout << "nn";
                                                                // (7)
    std::cout << "prod: " << std::accumulate(gen.begin(), gen.end(), 1, 
                                             [](int fir, int sec){ return fir * sec; });

    std::cout << "nn";

}

First needs Iterator a few aliases that I use in the following member function declarations. In addition to the previous iterator implementation in the program iterator.cpp the current iterator supports the following member functions: the arrow operator (operator-> in line 2), the post-increment operator (operator++(int) in line 3) and the equality operator (operator== in line 4).

Now the improved generator can be used in a range-based for loop (line 5), but also in the STL algorithm std::accumulate. The code on line 6 calculates the sum of all the numbers from 1 to 10; in line 7 follows a similar task: here a number from 1 to 11 is multiplied. In the first case I choose the neutral element 0 for the summation, in the second case the neutral element 1 for the multiplication.

There is a subtle difference between the first and second invocation of std::accumulate. The first call uses the non-member functions std::begin and std::end des Generators: std::accumulate(std::begin(gen), std::end(gen), 0)but the second call uses the member functions directly begin() and end() of the generator that I implemented.

This results in the following output of the program:

In my next article I will introduce the covariant return type. The covariant return type of a member function allows a parent member function to return a narrower type. This is particularly useful when implementing the Prototype design pattern.


(map)

To home page

See also  Correction: Bug in the priority scheduler for coroutines in the C++ blog

You may also like

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

Privacy & Cookies Policy