Range-based for statements: from begin() to end()

One of the new features which makes C++ code much more readable, is the range-based for statements. They can be found in the standard at section [stmt.ranged]. The standard outlines 3 ways to use this new feature. As a braced initializer list, an array, or a class. It’s easy to imagine using a list, array, or other container to iterate over. But what if you want to define a new class, and use it in a range loop? What is the interface your class needs to expose?

Let’s start with braced-init-list. Pretty simple stuff and comes in handy sometimes:

for( auto& chipmunks : 
   { “Alvin”, “Simon”, “Theodore” } )
{
  // Do what chipmunks do. 
}

The array is similar to the above example. The only caveats are the list size must be known, and that the array can’t be composed of incomplete types.

Let’s move on to the interesting part, the iterations over a class. The necessary interface is as simple as a begin() and end(), either exposed as a member function, or as a free function. NOTE: Slightly more than that. The return type of the begin() and end() need to act as a pointer or an iterator.

The free function is the same idea, so let’s see an example with a member function.

B b_object;
	
for (auto b_iter : b_object)
{
	// Do something
}

class B
{

	iterator_impl start_iter;
	iterator_impl end_iter;

public:

	B() : start_iter( /* start value */ ), 
          end_iter( /*end value*/ )
	{}

	iterator_impl begin()
	{
		return start_iter;
	}

	iterator_impl end()
	{
		return end_iter;
	}
}


Pretty simple so far. If your begin() and end() are returning pointers, you’re set. However in this example I’m writing my own iterators. There are 3 operators in this case which need to be exposed:

  1. Pre increment ++ operator: Used to increment forward the loop.
  2. De-reference operator: To access the value pointed to by the iterator inside the loop.
  3. Not equal operator: To test for the end of the range loop.

Continuing our example:

class iterator_impl
{
	private:
		int my_val;

	public:
		iterator_impl(int val) : my_val(val) {}

		int value() const 
		{
			return my_val;
		}

		iterator_impl& operator++()
		{
			// Move it forward, 
			
			// return *(this);
		}

		int operator*()
		{
			return my_val;
		}

		bool operator!=(const iterator_impl &rhs)
		{
			return ( my_val != rhs.value() );
		}
};

We can now use our new B class in a range based loop:

B b_object;
	
for (auto b_iter : b_object)
{
	// Do something
}

The standard outlines what is necessary in their example below. The range-based loop would be equivalent to :

{
   auto && __range = range-init;
   for ( auto __begin = begin-expr,
      __end = end-expr;
      __begin != __end;
      ++__begin ) 
   {
       for-range-declaration = *__begin;
         statement
   }
}

The begin-expr and end-expr equate to calling begin() and end(). Those return us the iterators. Looking at __begin, we can see the 3 operators (not equal, pre-increment, and de-refence) being called on it.

Final thoughts:

If you have access to iterator like pointers or actual iterators, exposing your new type to range based loops can be as easy as having member or free function begin() and end().

Note

A new development in this area is the introduction of an optional init-statement in C++20. Similar to a normal for loop. The goal is to allow for proper management of variable lifetimes. This looks as you’d expect:

for( T myObj = TempListObjGenerator(); 
     auto& x:  myObj.list() )
{
   // Do stuff.
}


for ( init-statementopt for-range-declaration : for-range-initializer ) 
    statement

// Equivalent to:

{
		init - statementopt
		auto &&__range = for - range - initializer;
		auto __begin = begin - expr;
		auto __end = end - expr;
		for (; __begin != __end; ++__begin) {
			for - range - declaration = *__begin;
			statement
		}
}

Leave a Reply

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