Iterators, Algorithms, and Ranges

Introduction to C++ Ranges

A number of algorithms that we're accustomed to expect take two iterators that describe a range together. Rather than repeating that pair of iterators at each call site, it becomes unnecessarily verbose and error prone; it's possible to accidentally introduce an iterator range that doesn't describe the actual contents of the underlying data. The C++20 ranges package bundles that pair into a single range object, allowing an algorithm to take that range in one go.

The conventional algorithm to sort a vector will work, but it requires that the caller explicitly give the iterator to begin() and the iterator to end().

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

int main()
{
    std::vector v{ 40, 10, 30, 20, 50 };

    // Call this function with the provided arguments
    std::sort(v.begin(), v.end());

    for (int n : v)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

The program still calls into std::sort, receiving the iterator from v.begin() and the iterator from v.end(). After the function returns, the vector now contains the elements in ascending order, 10 20 30 40 50, and the program prints the contents of the vector. This algorithm took two iterators which it could have obtained from a container of any variety; the algorithm doesn't actually care that it's a vector at all.

The corresponding range algorithm takes the container directly.

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

int main()
{
    std::vector v{ 40, 10, 30, 20, 50 };

    std::ranges::sort(v);

    // Visit each element in the range directly
    for (int n : v)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

The ranges version takes the single container object, v. In order to compute the sum, it retrieves a begin iterator and an end iterator that describe the container, but the user doesn't have to write those out. The program, as shown, now prints 10 20 30 40 50. Now, in the case that the type of the container should change from std::vector to, say, std::array, the function call doesn't need to change at all.

A range also need not represent every element in an existing sequence, and may instead refer to a subset of an existing sequence. A particularly common example of such a range is a view that selects the first few elements. The adapter std::views::take creates such a range.

#include <iostream>
#include <ranges>
#include <vector>

int main()
{
    std::vector v{ 10, 20, 30, 40, 50, 60 };

    // Build a lazy view instead of copying elements
    auto firstThree{ v | std::views::take(3) };

    for (int n : firstThree)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

v | std::views::take(3) creates a view that describes the first 3 elements of v. This view is bound to the variable firstThree. Walking through this view using the range based for loop will cause the program to visit each element in this sequence, and print 10 20 30.

Another common range adaptor is one to reverse the order of elements. This takes the sequence described by a range and effectively produces another range that walks the same underlying elements, but does so in reverse order.

#include <iostream>
#include <ranges>
#include <vector>

int main()
{
    std::vector v{ 10, 20, 30, 40, 50 };

    // Pipe the range through this view adaptor
    auto reversed{ v | std::views::reverse };

    for (int n : reversed)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

In this example, the range reversed still describes all the same elements as v, they are just seen from back to front. Thus the output to the console from this loop is 50 40 30 20 10. Note that since we're observing a view, no elements were copied or moved; the adaptor simply altered how we traverse those elements.

Views are lazy. They only process the contents that the element that the view refers to, when actually needed. Creating a view will never involve any potentially computationally expensive operations; the calculation only occurs when the iteration (or access) of the view actually occurs. This has an obvious benefit, in that such a view can be created, passed around the program, even stored, yet that operation is ultimately postponed until the point that access to its elements actually occurs.

#include <iostream>
#include <ranges>
#include <vector>

auto makeView()
{
    std::vector v{ 1, 2, 3 };
    return v | std::views::take(2);
}

int main()
{
    // Initialize r with the value returned by makeView
    auto r{ makeView() };
    std::cout << *r.begin() << '\n';

    return 0;
}

The local vector to makeView is destroyed by the time the function returns. The returned view, however, still refers to that now destroyed vector. Performing any operation on this view (accessing elements, etc.) is undefined behavior; the behavior observed at runtime is totally unpredictable, a crash, random values, and even apparent correctness by sheer chance are all possible outcomes. As is the case with the examples of view, any data being described by a view must remain in scope and be valid for the entire duration that the view is expected to be used.

Together, the facilities described by the C++20 ranges package provide a pair of convenient facilities, allowing the user to avoid unnecessarily writing an explicit range formed by two iterators, and allowing for the construction of very lightweight, "view" ranges that describe subsets of underlying sequences without necessarily performing expensive copying.

Exercise: Introduction to C++ RangesStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Introduction to C++ Ranges

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

After sorting, reverse the vector with std::ranges::reverse and print it again. Predict both orders and identify whether either algorithm creates an owning copy.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

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

int main()
{
    std::vector v{ 40, 10, 30, 20, 50 };

    std::ranges::sort(v);

    // Visit each element in the range directly
    for (int n : v)
        std::cout << n << ' ';
    std::cout << '\n';

    return 0;
}

Hints

Hint 1

std::ranges::sort receives the whole vector instead of an explicit begin/end pair. It rearranges v itself; it is not a new sorted copy.

Hint 2

After the first output, call std::ranges::reverse(v) and print v again. Both this call and the earlier sort modify the same container.

Worked solution

After the original range sort puts v in ascending order, a range reverse call rearranges that same vector into descending order. Both algorithms act on the existing container rather than produce owning copies.

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

int main()
{
    std::vector v{ 40, 10, 30, 20, 50 };

    std::ranges::sort(v);

    // Visit each element in the range directly
    for (int n : v)
        std::cout << n << ' ';
    std::cout << '\n';

    std::ranges::reverse(v);
    std::cout << "Reversed: ";
    for (int n : v) std::cout << n << ' ';
    std::cout << '\n';
    return 0;
}

Example output

10 20 30 40 50 
Reversed: 50 40 30 20 10 

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.