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.
Requirements for a range-based for loop
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
andend_expr
: return an iterator object- Iterator object:
operator++
: Increases the iteratoroperator*
: Dereferencing the iterator and accessing the current elementoperator!=
: 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.
A generator
The first implementation supports the iterator protocol.
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 valueoperator++
increases the current valueoperator!=
compares the current value with theend_
-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.
A forward iterator
The following improved Generator
has an inner class Iterator
which 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:
What's next?
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)