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.
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:
std::vector
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;
};
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();
}
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>);
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
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.
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.begin()
and end()
methods must be public for a type to be considered a range.auto
for the return type of begin()
and end()
is common, but exact iterator types can also be explicitly specified.Learn to implement iterators in custom types, and make them compatible with range-based techniques.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.