Jonathan Boccara's blog

How to Make Your Classes Compatible with Range for Loop

Published September 2, 2021 - 0 Comments

Today we have a question from Fluent C++ reader Nithin:

Hi Jonathan,

Thank you for the very useful Fluent C++ site. I am learning a lot from the site and applying things that I learn from there to write more expressive code. I have several classes in my code base as below.

Let’s say I want to have a loop that iterates between beginAVec and endAVec. How can I achieve this using a range based for loop?

class A
{
public:
    vector<unsigned>::iterator beginAVec()
    {
        return begin(_aVec);
    }
    vector<unsigned>::iterator endAVec()
    {
        return end(_aVec);
    }

private:
    vector<unsigned> _aVec;
};

Thanks Nithin for this great question! It is useful indeed to make user defined classes compatible with range based for loops. If you’d also like to ask a question don’t hesitate to send me an email like Ni.

Let’s discuss several approaches to make class A compatible with range based for loops.

Making the class a range

One option is to make the class comply with the range interface: exposing a begin and an end:

class A
{
public:
    vector<unsigned>::iterator begin()
    {
        return begin(_aVec);
    }
    vector<unsigned>::iterator end()
    {
        return end(_aVec);
    }

private:
    vector<unsigned> _aVec;
};

The code generated by range for loops calls begin and end on the object to iterate on. This lets us write code like this:

A a;

for (auto const& element : a)
{
    // ...
}

But this may not be the best option.

Let’s see what happened here. Even if this option involves changing just a few characters in the code of A, it has changed its meaning. A is no longer a class that gives access to a container. A represents the container itself, because we iterate on A.

This is a fundamental change in the semantics of A, and we shouldn’t make this change only for a technical reason, to make A compatible with range based for loops.

For example if A gives access to other data that is not related to the _aVec, or even to another collection _bVec, then A should not represent the collection _aVec only.

In case you decide that A does not represent the collection itself, let’s review our other options.

Giving access to the vector

Here is another way of changing A to make it compatible with range based for loops: returning the collection itself:

class A
{
public:
    vector<unsigned> const& aVec()
    {
        return _aVec;
    }

private:
    vector<unsigned> _aVec;
};

This allows to write code using range based for loops like this:

A a;

for (auto const& element : a.aVec())
{
    // ...
}

In this case, the interface of A makes it clear that A and its collection are two different entities, as A gives access to the collection.

But this code introduces a limitation: we can no longer modify the values inside of the collection. Indeed, a range based for loop with non const elements would not compile:

A a;

for (auto& element : a.aVec()) // compilation error, aVec returns a const reference
{
    // ...
}

But with the initial code with the begin and end interface, we could modify the values inside of the collection.

An easy fix for this is to make the interface of A return a non-const reference of the collection:

class A
{
public:
    vector<unsigned>& aVec()
    {
        return _aVec;
    }

private:
    vector<unsigned> _aVec;
};

The following code now compiles fine:

A a;

for (auto& element : a.aVec())
{
    // ...
}

But by doing this we’ve allowed users of A to do more than modify the values inside the collection: they can now modify the structure of the collection itself! They can push_back new values, erase some values, clear the vector, invalidate iterators, and so on. They can do everything you can do on a vector.

Whereas with begin and end, we could only modify the values, and not the structure of the collection.

Maybe giving full access to the collection is what you want from your interface, but this also has to be a deliberate design choice, not just a technical choice to make the class compatible with range based for loops.

This brings up an interesting point about containers and ranges. When introducing ranges, we often illustrate with containers, saying that containers are ranges. This is true, but it’s important to realise that ranges are only one aspect of containers, that allows accessing and modifying values, but not the structure.

Introducing a range class

What if you don’t want A to represent the collection, and you would still like to give access to the values but not to the structure of the container?

One option is to provide a range (with a begin and end interface), but that is not directly in the interface of A. To do that we can introduce a simple range class:

class A
{
public:
    struct Range
    {
        std::vector<unsigned>::iterator begin_;
        std::vector<unsigned>::iterator end_;
        std::vector<unsigned>::iterator begin(){ return begin_; }
        std::vector<unsigned>::iterator end(){ return end_; }
    };

    Range aVec()
    {
        return Range{beginAVec(), endAVec()};
    }
    std::vector<unsigned>::iterator beginAVec()
    {
        return begin(_aVec);
    }

    std::vector<unsigned>::iterator endAVec()
    {
        return end(_aVec);
    }

private:
    std::vector<unsigned> _aVec;
};

This allows to use A with range based for loops the following way:

A a;

for (auto const& element : a.aVec())
{
    // ...
}

This range class is as simple as it gets, and does the job for this particular case, but it can hardly be reused for other classes:

  • it doesn’t handle other containers than vector,
  • it doesn’t handle other values than unsigned,
  • it doesn’t handle const iterators.

Designing a range class that handles all cases is complex and goes beyond the scope of this post. I rather recommend to use existing solutions, such as C++20 std::ranges::subrange, or Boost old boost::iterator_range.

Decide the meaning of your classes

Nithin’s question about how to make a C++ class compatible with range based for loops allowed us to discuss several ways to make a collection accessible from a class interface.

To choose the right solution, you need to decide what your class represents, its fundamental meaning. One you’ve decided what this is, C++ has a technical option to make your code show it in an expressive way.

Thanks again to Nithin for this great question. If you also have a question about making code expressive do send me an email!

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin