Earlier in the course, we introduced the idea of iterators, which generalize the process of traversing through a container.
Rather than writing a function that works with a specific type of container, we can instead write it to work with iterators.
Below, our function template has no notion of containers - it simply advances an iterator until it reaches the endpoint:
#include <iostream>
void Log(auto Iterator, auto End) {
while (Iterator != End) {
std::cout << *(Iterator++) << ", ";
}
}
Such an algorithm can work with any container that provides iterators.
Containers typically make iterators available through begin()
and end()
methods:
#include <vector>
#include <forward_list>
#include <iostream>
void Log(auto Iterator, auto End) {/*...*/}
int main() {
std::forward_list List{1, 2, 3};
std::cout << "std::forward_list<int>:\n";
Log(List.begin(), List.end());
std::vector Vector{1, 2, 3};
std::cout << "\n\nstd::vector<int>:\n";
Log(Vector.begin(), Vector.end());
}
std::forward_list<int>:
1, 2, 3,
std::vector<int>:
1, 2, 3,
This chapter builds on our knowledge of iterators, to unlock even more capabilities.
All of the standard library sequential containers we’ve introduced so far, such as std::vector
and std::forward_list
, are examples of ranges.
The concept of a range was introduced in C++20, and is built on the foundations created by iterators.
Similar to iterators, a range establishes a convention on how a type must work for it to be considered a range.
Specifically, a range is any type that has a begin()
method that returns an iterator, and an end()
method that lets us determine when the range ends.
If a type decides to implement that specification, then the type is a range, and can then be used with range-based techniques.
Much of this chapter will be dedicated to exploring those techniques.
One of the main techniques that we can apply when working with ranges is the range-based for loop, which we’ve seen a few times in the past.
If a type is a range, then we can use the range-based for-loop syntax to iterate over it:
#include <vector>
#include <iostream>
int main() {
std::vector Vector{1, 2, 3};
for (int x : Vector) {
std::cout << x << ", ";
}
}
1, 2, 3,
In the next lesson, we’ll see how can update a custom type that we create to satisfy the range requirements, thereby becoming compatible with range-based for loops.
Similar to functions, our objects are passed into the body of our loop by value, meaning they are copied.
If our type is expensive to copy, we will likely want to pass it by reference instead.
Additionally, if we’re not planning to modify the object within the body, we can mark those references as const
:
#include <vector>
#include <iostream>
int main() {
std::vector Vector{1, 2, 3};
for (const int& x : Vector) {
std::cout << x << ", ";
}
}
1, 2, 3,
In the introduction to iterators, we covered how not all iterators have the same capabilities. They are broken down into categories, for example:
Given that ranges are built upon iterators, it’s perhaps not surprising that ranges are also separated into categories, based on the capability of the iterator they are based upon.
For example:
std::forward_list
, only supports traversal in one direction, one step at a time.std::list
, supports traversal in either direction, one step at a timestd::vector
, allows access to freely jump to any object in the rangeSimilar to iterator categories, we can imagine ranges existing in a hierarchy, where a more powerful range also meets the requirements of a less powerful range.
For example, a bidirectional range implements all the requirements of a forward range, so it is a forward range.
A random-access range implements all the requirements of a bidirectional range, so it is a bidirectional range. And therefore, it is also a forward range.
Concepts are available to determine if a type is a valid range.
These are available within the std::ranges
namespace, after including the <ranges>
header. For example:
std::ranges::forward_range
determines whether a type is a forward range.std::ranges::bidirectional_range
determines whether a type is a bidirectional rangestd::ranges::random_access_range
determines whether a type is a random access rangeBelow, we use this to investigate the types our template was instantiated with:
#include <vector>
#include <list>
#include <forward_list>
#include <ranges>
#include <iostream>
template <typename T>
void Log(T Range) {
if constexpr (std::ranges::forward_range<T>) {
std::cout << " - Forward Range\n";
}
if constexpr (
std::ranges::bidirectional_range<T>
) {
std::cout << " - Bidirectional Range\n";
}
if constexpr (
std::ranges::random_access_range<T>
) {
std::cout << " - Random Access Range\n";
}
}
int main() {
std::cout << "std::forward_list<int>:\n";
Log(std::forward_list{1, 2, 3});
std::cout << "\nstd::list<int>:\n";
Log(std::list{1, 2, 3});
std::cout << "\nstd::vector<int>:\n";
Log(std::vector{1, 2, 3});
}
std::forward_list<int>:
- Forward Range
std::list<int>:
- Forward Range
- Bidirectional Range
std::vector<int>:
- Forward Range
- Bidirectional Range
- Random Access Range
In this example, we use it to create a template that requires a type to be at least a bidirectional range:
#include <forward_list>
#include <ranges>
#include <iostream>
void LogLast(
std::ranges::bidirectional_range auto R
) {
std::cout << *(std::next(R.end(), -1));
}
int main() {
LogLast(std::forward_list{1, 2, 3});
}
error C2672: 'LogLast': no matching overloaded function found
the associated constraints are not satisfied
the concept 'std::ranges::bidirectional_range<std::forward_list<int>>' evaluated to false
In this lesson, we explored iterators and ranges, demonstrating how they enable flexible traversal through various container types
std::vector
and std::forward_list
offer begin()
and end()
methods, making them compatible with iterators.begin()
and end()
methods.std::ranges
namespace help determine the category of a range.This lesson offers an in-depth look at iterators and ranges, emphasizing their roles in container traversal
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.