Implementing Ranges for Custom Types

Learn to implement iterators in custom types, and make them compatible with range-based techniques.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In the previous lesson, we introduced how types that implement appropriate begin() and end() methods can be considered ranges and therefore become compatible with range-based techniques.

Often, we’d like to give our custom, user-defined types this capability too. For example, we might have a Party type that contains a collection of Player objects.

We’d like to be able to iterate over those players using a range-based for loop:

#include "Party.h"

int main() {
  Party MyParty;
  for (const auto& Player : MyParty) {
    // ...
  }
}

In this lesson, we’ll show how we can implement this capability.

Adding Iterators to our Custom Types

In the next two lessons, we will see an example where we implement iterators for our types entirely from scratch. This gives us full control over the implementation, and how our iterators behave.

But, in most cases, this isn’t necessary. When we’re implementing a custom collection, two things are often true:

  • Behind the scenes, our collection is letting some other type manage storage, such as a std::vector
  • That type implements iterators, and we want our iterators to behave the same way

We’ll focus on this simpler scenario in this lesson. A typical Party class might look like this, where we’re relying on a private std::vector to manage the storage:

#include <vector>

class Player {};

class Party {
public:
  // Party-specific logic
  void AddMember() {};
  void StartQuest() {};
  void Disband() {};
  void SetLeader() {};

private:
  // Underlying Collection
  std::vector<Player> PartyMembers;
};

To make our party iterable, we can just expose the begin() and end() methods of our underlying std::vector.

Note, that our begin() and end() methods must be made public for our container to be considered a range:

#include <vector>

class Player {};

class Party {
public:
  // Party-specific logic
  // ...

  // Iterators
  auto begin() {
    return PartyMembers.begin();
  }
  auto end() {
    return PartyMembers.end();
  }
private:
  // Underlying Collection
  std::vector<Player> PartyMembers;
};

Iterator Types

Above, the return type of our new functions was set to auto. This is typical but if we ever need to be specific about the type of an iterator from the standard library containers, there’s a static iterator property that we can use:

vector<Player>::iterator begin() {
  return PartyMembers.begin();
}

vector<Player>::iterator end() {
  return PartyMembers.end();
}

Asserting a Type is a Range

If our type is intended to be a range, it’s useful to statically assert this using the range concepts we introduced in the previous lesson.

This ensures that if some future change to our type causes it to violate the range specification, we’re alerted.

Below, we assert our type is a random access range. This is the case because the iterators we use are from a std::vector, which is a container that supports random access:

#include <vector>
#include <ranges>

class Player {};

class Party { /*...*/ } static_assert( std::ranges::random_access_range<Party>);

In this case, our type is also a contiguous range. A contiguous range is simply a random access range, with the additional guarantee that adjacent elements are also adjacent in memory.

This is how arrays like std::vector work, so our type also satisfies this stricter concept:

static_assert(
  std::ranges::contiguous_range<Party>);

Using Range-Based For Loops

Now that our type implements all the range requirements, the objects instantiated from the class are now ranges.

This allows us to use them in range-based techniques, such as a range-based for loop:

int main() {
  Party MyParty;

  for (const auto& Player : MyParty) {
    // ...do things
  }
}

A complete example is available here:

#include <vector>
#include <iostream>
#include <utility>

class Player {
public:
  Player(std::string Name) : mName(Name) {}
  std::string GetName() const { return mName; }
private:
  std::string mName;
};

class Party {
public:
  void AddMember(const std::string& NewMember) {
    PartyMembers.emplace_back(NewMember);
  }

  // Iterators
  auto begin() {
    return PartyMembers.begin();
  }
  auto end() {
    return PartyMembers.end();
  }
private:
  std::vector<Player> PartyMembers;
};

int main() {
  Party MyParty;
  MyParty.AddMember("Legolas");
  MyParty.AddMember("Gimli");
  MyParty.AddMember("Frodo");

  for (const auto& Player : MyParty) {
    std::cout << Player.GetName() << '\n';
  }
}
Legolas
Gimli
Frodo

Summary

In this lesson, we explored how to create a range using a custom type by implementing begin() and end() methods.

We also discussed how to assert that a type correctly implements the range requirements by combining static_assert and concepts from the standard library <ranges> header.

Key Takeaways:

  • Custom types can be valid ranges by implementing begin() and end() methods. When they encapsulate a standard library container like std::vector, this can be done simply by returning that container’s iterators.
  • The begin() and end() methods must be public for a type to be considered a range.
  • Using auto for the return type of begin() and end() is common, but exact iterator types can also be explicitly specified.
  • If our type is required to be a range, we should statically assert that it is.
  • Once a type is confirmed as a range, it can be used with with range-based techniques, such as range-based for loops.

Was this lesson useful?

Next Lesson

Defining Ranges using Sentinels

An alternative way of defining ranges, and why we sometimes need to use them
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Implementing Ranges for Custom Types

Learn to implement iterators in custom types, and make them compatible with range-based techniques.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Defining Ranges using Sentinels

An alternative way of defining ranges, and why we sometimes need to use them
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved