Iterator and Range-Based Algorithms

An introduction to iterator and range-based algorithms, using examples from the standard library

Ryan McCombe
Updated

In earlier lessons, we saw how we could standardize traversal through potentially any type of container by using iterators or ranges.

By doing so, we gain the benefit of being able to use or create algorithms that work across any of those containers.

The standard library's <algorithm> header includes over a hundred generally useful algorithms based on iterators and ranges.

Example: Sorting Using Iterators

Once we've included the <algorithm> header, we can pass an iterator pair to std::sort(). The algorithm will traverse between those two iterators, putting all of the elements in order:

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
  std::vector Numbers { 3, 1, 2 };
  std::sort(Numbers.begin(), Numbers.end());
  for (auto Num : Numbers) {
    std::cout << Num << ", ";
  }
}
1, 2, 3,

Example: Sorting Using Ranges

Almost all of the iterator-based algorithms in the standard library have a variation that works with ranges. These are available within the std::ranges namespace. For example, the range-based variation of std::sort() is std::ranges::sort().

Rather than an iterator pair, we can call it with a single argument - the range we want to run the algorithm on:

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
  std::vector Numbers { 3, 1, 2 };
  std::ranges::sort(Numbers);
  for (auto Num : Numbers) {
    std::cout << Num << ", ";
  }
}
1, 2, 3,

Applying an Algorithm to a Subrange

Our previous examples sorted our entire container, but we can apply an algorithm to only part of a container.

With iterator-based algorithms, we can do this by passing different iterators as arguments. Below, we sort the middle three elements of our container:

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
  std::vector Numbers { 3, 5, 1, 2, 4 };

  std::sort(
    std::next(Numbers.begin(), 1),
    std::prev(Numbers.end(), 1)
  );

  for (auto Num : Numbers) {
    std::cout << Num << ", ";
  }
}
3, 1, 2, 5, 4,

We can do the same thing using the range-based algorithm overload by defining the range with an iterator and a sentinel. In this case, the sentinel is another iterator but, as we covered in the previous lesson, it doesn't need to be:

#include <vector>
#include <iostream>
#include <algorithm>

int main() {
  std::vector Numbers { 3, 5, 1, 2, 4 };

  std::ranges::sort(
    std::next(Numbers.begin(), 1),
    std::prev(Numbers.end(), 1)
  );

  for (auto Num : Numbers) {
    std::cout << Num << ", ";
  }
}
3, 1, 2, 5, 4,

Defining Ranges using Sentinels

An alternative way of defining ranges, and why we sometimes need to use them

There are many more ways of generating subranges from a range, which we'll explore through the rest of this chapter.

Algorithm Container Requirements

Fixing "the constraint was not satisfied" errors

As we covered earlier in the chapter, not all ranges and iterators have the same capabilities. For example, some ranges might support random access, whilst others may only support forward access.

Many algorithms have specific requirements for the ranges and iterators passed to them.

For example, the std::ranges::sort() or std::sort() algorithms require our range or iterators to support random access.

Therefore, it cannot be run on a container such as a linked list:

#include <forward_list>
#include <algorithm>

int main() {
  std::forward_list Numbers { 3, 1, 2 };
  std::ranges::sort(Numbers);
}

We can check the requirements of any algorithm from the documentation.

We may often also get useful error messages from the compiler if our container does not meet the requirements.

Below, we see std::ranges::sort() requires the std::ranges::random_access_range concept to be satisfied, but std::forward_list does not meet the requirements:

error: the concept 'std::ranges::random_access_range<std::forward_list<int>>' evaluated to false - the constraint was not satisfied

Algorithm Element Requirements

Beyond container requirements, algorithms may additionally have expectations of the type of objects stored within our container.

For example, using std::ranges::sort() or std::sort() on a collection only makes sense when the type of data within that collection is orderable.

If we create an arbitrary type, and attempt to sort a container of them, the algorithm is not going to know how to do it:

#include <vector>
#include <algorithm>

struct SomeType {
  SomeType(int x) : Value{x} {}
  int Value;
};

int main() {
  std::vector<SomeType> Numbers { 3, 1, 2 };
  std::ranges::sort(Numbers);
}
the associated constraints are not satisfied
the concept 'std::sortable' evaluated to false

We cover ordering in more detail in a later chapter, but we can make a type compatible with std::sort() by implementing the == and <=> operators:

#include <vector>
#include <algorithm>
#include <iostream>

struct SomeType {
  SomeType(int x) : Value{x} {}
  auto operator<=>(const SomeType& Other) const {
    return Value <=> Other.Value;
  }
  bool operator==(const SomeType& Other) const {
    return Value == Other.Value;
  }

  int Value;
};

int main() {
  std::vector<SomeType> Container{3, 1, 2};
  std::ranges::sort(Container);

  for (const SomeType& Object : Container) {
    std::cout << Object.Value << ", ";
  }
}
1, 2, 3,

The Spaceship Operator and Expression Rewriting

A guide to simplifying our comparison operators using C++20 features

Other algorithms may have different requirements. Some algorithms might require our type to be default constructible or to implement an assignment operator for example.

Algorithm Preconditions

The final requirement that is worth considering is whether or not the algorithm we're using makes any assumptions that are not enforced by the compiler.

For example, binary search is a very efficient algorithm for finding objects in a container, but it relies on the container being sorted.

As such, the standard library's implementation of binary search - std::binary_search - has that same assumption.

When reading documentation, we must check for these types of requirements carefully, because they're very difficult to enforce automatically. Instead of a compiler error, we'll typically just get incorrect behaviors at run time instead.

Below, we use binary search on a container that isn't sorted, causing our program to have an incorrect output:

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
  std::vector Container{3, 1, 4, 5, 2};

  bool Found{
    std::ranges::binary_search(Container, 2)};

  if (!Found) {
    std::cout << "The container doesn't have 2";
  }
}
The container doesn't have 2

Algorithm Customisations

Depending on the algorithm we're using, we can often pass additional arguments to customise the behavior.

For example, most range-based algorithms allow us to modify their behavior by providing an additional projection function, which we cover in detail in the next lesson.

Other algorithms, such as std::sort and std::ranges::sort may offer additional parameters specific to their use case.

Example: Customizing the Sort Behaviour of std::sort()

By default, the std::sort() and std::ranges::sort() algorithms use our type's < operator to determine the ordering. That is, if A < B, then A should come before B after the algorithm completes.

However, these algorithms have overloads that allow us to provide a different comparison function as an additional argument.

This function is going to receive two objects from our collection and should return true if the first object should come before the second object.

Previously, our vector of numbers was arranged in ascending order. Let's provide a lambda that will instead cause the algorithm to sort the objects in descending order:

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
  std::vector Container{3, 1, 4, 5, 2};

  std::ranges::sort(Container, [](int a, int b) {
    return a > b;
  });

  for (int x : Container) {
    std::cout << x << ", ";
  }
}
5, 4, 3, 2, 1,

With the ability to pass a custom comparison function, we can now sort items of any type, in any order we desire. Let's sort a party of players by their level:

#include <vector>
#include <iostream>
#include <algorithm>

struct Player {
  std::string Name;
  int Level;
};

int main() {
  std::vector Party {
    Player {"Legolas", 49},
    Player {"Gimli", 47},
    Player {"Gandalf", 53}
  };

  std::ranges::sort(Party,
    [](Player& a, Player& b){
      return a.Level > b.Level;
    }
  );

  for (const auto& Player : Party) {
    std::cout << "[" << Player.Level << "] "
              << Player.Name << "\n";
  }
}
[53] Gandalf
[49] Legolas
[47] Gimli

Summary

In this lesson, we introduced the idea of iterator and range-based algorithms. We learned how these concepts enable standardized traversal and manipulation of data across various container types.

Key Learnings

  • The use of iterators and ranges for standardized container traversal.
  • Implementing std::sort() with iterator pairs to order elements.
  • Utilizing std::ranges::sort() for range-based sorting.
  • Applying algorithms to subranges using both iterator-based and range-based methods.
  • Understanding container requirements for different algorithms, like the need for random access in std::sort().
  • Workarounds for performing tasks on non-compatible containers, such as using std::forward_list's native sort() method to sort a linked list.
  • Algorithms may have additional requirements of the type stored within the containers - for example, sort algorithms only work on containers of types that can be ordered.
  • The importance of algorithm preconditions, illustrated with std::binary_search.
  • Customizing algorithms' behavior using additional arguments, like a custom comparison function in sorting.
  • Utilization of standard library comparison functors for common operations.
  • Sorting items of any type and order using custom comparison functions in algorithms.
Next Lesson
Lesson 80 of 128

Projection Functions

Learn how to use projection functions to apply range-based algorithms on derived data

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

How to Reverse the Order of Elements in a Vector Using the C++ Standard Library
How can I reverse the order of elements in a vector using the C++ standard library?
Difference Between std::sort() and std::stable_sort()
What is the difference between std::sort() and std::stable_sort()?
How to Remove Duplicates from a Sorted Vector in C++
How do I remove duplicates from a sorted vector in C++?
Finding the Smallest and Largest Elements in a Range in C++
What is the best way to find the smallest and largest elements in a range?
How to Check If All Elements in a Range Satisfy a Specific Condition in C++
How can I check if all elements in a range satisfy a specific condition?
Using std::ranges Algorithms with C-Style Arrays
Can I use std::ranges algorithms with C-style arrays?
How to Debug Iterator and Range-Based Algorithm Issues in C++
What is the best way to debug iterator and range-based algorithm issues in C++?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant