Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

Enabling range-based for loops for custom types

As we saw in the preceding recipe, range-based for loops, known as for each in other programming languages, allow you to iterate over the elements of a range, providing a simplified syntax over the standard for loops and making the code more readable in many situations. However, range-based for loops do not work out of the box with any type representing a range, but require the presence of begin() and end() functions (for non-array types), either as a member or free function. In this recipe, we will learn how to enable a custom type to be used in range-based for loops.

Getting ready

It is recommended that you read the Using range-based for loops to iterate on a range recipe before continuing with this one if you need to understand how range-based for loops work, as well as what code the compiler generates for such a loop.

To show how we can enable range-based for loops for custom types representing sequences, we will use the following implementation of a simple array:

template <typename T, size_t const Size>
class dummy_array
{
  T data[Size] = {};
public:
  T const & GetAt(size_t const index) const
  {
    if (index < Size) return data[index];
    throw std::out_of_range("index out of range");
  }
  void SetAt(size_t const index, T const & value)
  {
    if (index < Size) data[index] = value;
    else throw std::out_of_range("index out of range");
  }
  size_t GetSize() const { return Size; }
};

The purpose of this recipe is to enable writing code like the following:

dummy_array<int, 3> arr;
arr.SetAt(0, 1);
arr.SetAt(1, 2);
arr.SetAt(2, 3);
for(auto&& e : arr)
{
  std::cout << e << '\n';
}

The steps necessary to make all this possible are described in detail in the following section.

How to do it...

To enable a custom type to be used in range-based for loops, you need to do the following:

  • Create mutable and constant iterators for the type, which must implement the following operators:
    • operator++ (both the prefix and the postfix version) for incrementing the iterator
    • operator* for dereferencing the iterator and accessing the actual element being pointed to by the iterator
    • operator!= for comparing the iterator with another iterator for inequality
  • Provide free begin() and end() functions for the type.

Given the earlier example of a simple range, we need to provide the following:

  • The following minimal implementation of an iterator class:
    template <typename T, typename C, size_t const Size>
    class dummy_array_iterator_type
    {
    public:
      dummy_array_iterator_type(C& collection,
                                size_t const index) :
      index(index), collection(collection)
      { }
      bool operator!= (dummy_array_iterator_type const & other) const
      {
        return index != other.index;
      }
      T const & operator* () const
      {
        return collection.GetAt(index);
      }
      dummy_array_iterator_type& operator++()
      {
        ++index;
        return *this;
      }
      dummy_array_iterator_type operator++(int)
      {
        auto temp = *this;
        ++*this;
        return temp;
      }
    private:
      size_t   index;
      C&       collection;
    };
    
  • Alias templates for mutable and constant iterators:
    template <typename T, size_t const Size>
    using dummy_array_iterator =
      dummy_array_iterator_type<
        T, dummy_array<T, Size>, Size>;
    template <typename T, size_t const Size>
    using dummy_array_const_iterator =
      dummy_array_iterator_type<
        T, dummy_array<T, Size> const, Size>;
    
  • Free begin() and end() functions that return the respective begin and end iterators, with overloads for both alias templates:
    template <typename T, size_t const Size>
    inline dummy_array_iterator<T, Size> begin(
      dummy_array<T, Size>& collection)
    {
      return dummy_array_iterator<T, Size>(collection, 0);
    }
    template <typename T, size_t const Size>
    inline dummy_array_iterator<T, Size> end(
      dummy_array<T, Size>& collection)
    {
      return dummy_array_iterator<T, Size>(
        collection, collection.GetSize());
    }
    template <typename T, size_t const Size>
    inline dummy_array_const_iterator<T, Size> begin(
      dummy_array<T, Size> const & collection)
    {
      return dummy_array_const_iterator<T, Size>(
        collection, 0);
    }
    template <typename T, size_t const Size>
    inline dummy_array_const_iterator<T, Size> end(
      dummy_array<T, Size> const & collection)
    {
      return dummy_array_const_iterator<T, Size>(
        collection, collection.GetSize());
    }
    

How it works...

Having this implementation available, the range-based for loop shown earlier compiles and executes as expected. When performing an argument-dependent lookup, the compiler will identify the two begin() and end() functions that we wrote (which take a reference to a dummy_array), and therefore, the code it generates becomes valid.

In the preceding example, we have defined one iterator class template and two alias templates, called dummy_array_iterator and dummy_array_const_iterator. The begin() and end() functions both have two overloads for these two types of iterators.

This is necessary so that the container we have considered can be used in range-based for loops with both constant and non-constant instances:

template <typename T, const size_t Size>
void print_dummy_array(dummy_array<T, Size> const & arr)
{
  for (auto && e : arr)
  {
    std::cout << e << '\n';
  }
}

A possible alternative to enable range-based for loops for the simple range class we considered for this recipe is to provide the member begin() and end() functions. In general, that will make sense only if you own and can modify the source code. Conversely, the solution shown in this recipe works in all cases and should be preferred to other alternatives.

See also

  • Creating type aliases and alias templates, to learn about aliases for types
  • Chapter 12, Iterating over collections with the ranges library, to learn about the fundamentals of the C++20 ranges library
Previous PageNext Page
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila