Lesson 14: Ranges

Lesson 0014 — A Tour of C++, Chapter 14 (§14.1–§14.6)

Chapter 13's algorithms operate on sequences defined by {begin(), end()} iterator pairs. This chapter's thread: a range is a generalization of that C++98 idea — the standard library's concept-constrained algorithms in <ranges> (namespace ranges) accept ranges directly, and a range can be defined three ways: by a {begin,end} pair of iterators, by a {begin,n} pair (an iterator and a count), or by a {begin,pred} pair (an iterator and a predicate that ends the range — enabling infinite ranges and ranges generated "on the fly"). This is what lets us write sort(v) instead of sort(v.begin(),v.end()), expresses "something like 99% of the common uses of algorithms" more directly, and eliminates a class of silly errors such as sort(v1.begin(),v2.end()) — yes, such errors have been seen "in the wild."

Why it matters: ranges are the modern surface of the standard library — views let you look at a range through filters, transforms, and slices without owning or copying elements; generators produce ranges on the fly (infinite ones included); and the pipeline operator r | views::filter(odd) | views::take(3) composes those operations left-to-right, far more readably than nested function calls. And the concept machinery from Chapter 8 shows up complete: type concepts, iterator concepts, and range concepts exist precisely to make algorithms and containers talk to each other safely.

14.1 Introduction

The standard library offers algorithms both constrained using concepts (Chapter 8) and unconstrained (for compatibility). The constrained (concept) versions are in <ranges>, in namespace ranges. Naturally, Stroustrup prefers the versions using concepts. A range can be defined by:

The range concept is what allows us to say sort(v) rather than sort(v.begin(),v.end()) as we had to using the STL since 1994. We can do similarly for our own algorithms:

template<forward_range R> requires sortable<iterator_t<R>>
void my_sort(R& r) {                       // modern, concept-constrained version of my_sort
    return my_sort(r.begin(), end());      // use the 1994-style sort
}

In addition to the notational advantage, ranges offer some opportunities for optimization and eliminate a class of "silly errors," such as sort(v1.begin(),v2.end()) and sort(v.end(),v.begin()). Naturally, there are different kinds of ranges corresponding to the different kinds of iterators: input_range, forward_range, bidirectional_range, random_access_range, and contiguous_range are represented as concepts (§14.5).

14.2 Views

A view is a way of looking at a range. For example:

void user(forward_range auto& r) {
    filter_view v {r, [](int x) { return x % 2; }};   // view (only) odd numbers from r
    for (int x : v) cout << x << ' ';                // "odd numbers: "
}

When reading from a filter_view, we read from its range. If the value read matches the predicate, it is returned; otherwise, the filter_view tries again with the next element from the range. Many ranges are infinite, and we often only want a few values, so there are views for taking only a few values from a range:

void user(forward_range auto& r) {
    filter_view v {r, [](int x) { return x % 2; }};    // view (only) odd numbers in r
    take_view tv {v, 100};                             // view at most 100 elements from v
    for (int x : tv) cout << x << ' ';
}
// We can avoid naming the views by using them directly:
for (int x : take_view{filter_view{r, [](int x) { return x % 2; }}, 3}) cout << x << ' ';

Such nesting of views can quickly get a bit cryptic — hence pipelines (§14.4). The standard library offers many views, also known as range adaptors (in <ranges>; here v is a view, r is a range, p is a predicate, n is an integer):

View Meaning
v = all_view{r} v is all elements from r
v = filter_view{r,p} v is elements from r that meet p
v = transform_view{r,f} v is the results of calling f on each element from r
v = take_view{r,n} v is at most n elements from r
v = take_while_view{r,p} v is elements from r until one doesn't meet p
v = drop_view{r,n} v is elements from r starting with the n+1th element
v = drop_while_view{r,p} v is elements from r starting with the first element that doesn't meet p
v = join_view{r} v is a flattened version of r; the elements of r must be ranges
v = split_view(r,d) v is a range of sub-ranges of r determined by the delimiter d (an element or a range)
v = common_view{r} v is a range with the same iterator and sentinel type

A view offers an interface very similar to a range, so in most cases we can use a view wherever we can use a range and in the same way. The key difference: a view doesn't own its elements; it is not responsible for deleting the elements of its underlying range — that's the range's responsibility. On the other hand, a view must not outlive its range:

auto bad() {
    vector v = {1, 2, 3, 4};
    return filter_view{v, odd};   // v will be destroyed before the view
}

Views are supposed to be cheap to copy, so we pass them by value. And views work on user-defined types too — e.g., averaging temperatures by looking at just one field of a Reading struct via views::elements<1>:

struct Reading { int location {}; int temperature {}; int humidity {}; int air_pressure {}; /* ... */ };

int average_temp(vector<Reading> readings) {
    if (readings.size() == 0) throw No_readings{};
    double s = 0;
    for (int x : views::elements<1>(readings))   // look at just the temperatures
        s += x;
    return s / readings.size();
}

14.3 Generators

Often, a range needs to be generated on the fly. The standard library provides a few simple generators (aka factories) for that (in <ranges>; here v is a view, x is of the element type T, is is an istream):

Generator Meaning
v = empty_view<T>{} v is an empty range of type T elements (had it any)
v = single_view{x} v is a range of the one element x
v = iota_view{x} v is an infinite range of elements: x, x+1, x+2, ... (incrementing via ++)
v = iota_view{x,y} v is a range of n elements: x, x+1, ..., y-1 (incrementing via ++)
v = istream_view<T>{is} v is the range obtained by calling >> for T on is

The iota_views are useful for generating simple sequences:

for (int x : iota_view(42, 52))   // 42 43 44 45 46 47 48 49 50 51
    cout << x << ' ';

The istream_view gives a simple way of using istreams in range-for loops, and — like other views — composes with other views:

for (auto x : istream_view<complex<double>>(cin)) cout << x << '\n';

auto cplx = istream_view<complex<double>>(cin);
for (auto x : transform_view(cplx, [](auto z) { return z * z; })) cout << x << '\n';
// an input of 1 2 3 produces 1 4 9

14.4 Pipelines

For each standard-library view (§14.2), the standard library provides a function that produces a filter — an object usable as an argument to the filter operator |. For example, filter() yields a filter_view. This lets us combine filters in a sequence rather than as a set of nested function calls:

void user(forward_range auto& r) {
    auto odd = [](int x) { return x % 2; };
    for (int x : r | views::filter(odd) | views::take(3)) cout << x << ' ';
}

The pipeline style (using the Unix pipeline operator |) is widely regarded as more readable than nested function calls. The pipeline works left to right: in f|g, the result of f is passed to g, so r|f|g means (g_filter(f_filter(r))). The initial r has to be a range or a generator. These filter functions are in namespace ranges::views:

for (int x : r | views::filter([](int x) { return x % 2; }) | views::take(3)) cout << x << ' ';
// or, shorter:
using namespace views;
auto odd = [](int x) { return x % 2; };
for (int x : r | filter(odd) | take(3)) cout << x << ' ';

The implementation of views and pipelines involves some quite hair-raising template metaprogramming, so if you are concerned about performance, make sure to measure whether your implementation delivers what you need. If not, there is always a conventional workaround:

void user(forward_range auto& r) {
    int count = 0;
    for (int x : r)
        if (x % 2) { cout << x << ' '; if (++count == 3) return; }
}
// however, here the logic of what's going on is obscured.

14.5 Concepts Overview

The standard library offers many useful concepts, in three families:

14.5.1 Type Concepts

The concepts related to properties of types and the relations among types reflect the variety of types; they help simplify most templates. Core language concepts (in <concepts>; T and U are types):

Concept Meaning
same_as<T,U> T is the same as U
derived_from<T,U> T is derived from U
convertible_to<T,U> A T can be converted to a U
common_reference_with<T,U> T and U share a common reference type
common_with<T,U> T and U share a common type
integral<T> T is an integral type
signed_integral<T> T is a signed integral type
unsigned_integral<T> T is an unsigned integral type
floating_point<T> T is a floating point type
assignable_from<T,U> A U can be assigned to a T
swappable_with<T,U> A T can be swapped with a U
swappable<T> swappable_with<T,T>

Many algorithms should work with combinations of related types — e.g., expressions mixing ints and doubles. We use common_with to say whether such a mix is mathematically sound: if common_with<X,Y> is true, we can use common_type_t<X,Y> to compare an X with a Y by first converting both to common_type_t<X,Y>. To specify a common type for a pair of types, we specialize common_type_t used in the definition of common — e.g., using common_type_t<Bigint,long> = Bigint; — but fortunately we don't need such a specialization unless we want to use operations on mixes of types for which a library doesn't (yet) have suitable definitions. The comparison concepts are strongly influenced by [Stepanov, 2009]:

Concept Meaning
equality_comparable_with<T,U> A T and a U can be compared for equivalence using ==
equality_comparable<T> equality_comparable_with<T,T>
totally_ordered_with<T,U> A T and a U can be compared using <, <=, >, and >= yielding a total order
totally_ordered<T> totally_ordered_with<T,T>
three_way_comparable_with<T,U> A T and a U can be compared using <=> yielding a consistent result
three_way_comparable<T> three_way_comparable_with<T,T>

Curiously, there is no standard boolean concept — Stroustrup often needs one, so he shows a version: concept Boolean = requires(B x, B y) { { x = true }; ... };. When writing templates, we often need to classify types (object concepts in <concepts>):

Concept Meaning
destructible<T> A T can be destroyed and have its address taken with unary &
constructible_from<T,Args> A T can be constructed from an argument list of type Args
default_initializable<T> A T can be default constructed
move_constructible<T> A T can be move constructed
copy_constructible<T> A T can be copy constructed and move constructed
movable<T> move_constructible<T>, assignable<T&,T>, and swappable<T>
copyable<T> copy_constructible<T>, movable<T>, and assignable<T, const T&>
semiregular<T> copyable<T> and default_constructible<T>
regular<T> semiregular<T> and equality_comparable<T>

The ideal for types is regular — a regular type works roughly like an int and simplifies much of our thinking about how to use a type (§8.2). The lack of a default == for classes means that most classes start out semiregular even though most could and should be regular.

Whenever we pass an operation as a constrained template argument, we need to specify how it can be called, and sometimes also what assumptions we make of their semantics (callable concepts in <concepts>):

Concept Meaning
invocable<F,Args> An F can be invoked with an argument list of type Args
regular_invocable<F,Args> invocable<F,Args> and is equality preserving
predicate<F,Args> A regular_invocable<F,Args> returning a bool
relation<F,T,U> A predicate<F,T,U>
equivalence_relation<F,T,U> A relation<F,T,U> that provides an equivalence relation
strict_weak_order<F,T,U> A relation<F,T,U> that provides strict weak ordering

A function f() is equality preserving if x==y implies f(x)==f(y). An invocable and a regular_invocable differ only semantically; we can't (currently) represent that in code, so the names simply express our intent. Similarly, a relation and an equivalence_relation differ only semantically — an equivalence relation is reflexive, symmetric, and transitive — and a relation and a strict_weak_order differ only semantically: strict weak ordering is what the standard library usually assumes for comparisons, such as <.

14.5.2 Iterator Concepts

The traditional standard algorithms access their data through iterators, so we need concepts to classify properties of iterator types (in <iterators>):

Concept Meaning
input_or_output_iterator<I> An I can be incremented (++) and dereferenced (*)
sentinel_for<S,I> An S is a sentinel for an iterator type; S is a predicate on I's value type
sized_sentinel_for<S,I> A sentinel S where the - operator can be applied to I
input_iterator<I> An I is an input iterator; * can be used for reading only
output_iterator<I> An I is an output iterator; * can be used for writing only
forward_iterator<I> An I is a forward iterator, supporting multi-pass and ==
bidirectional_iterator<I> A forward_iterator<I> supporting --
random_access_iterator<I> A bidirectional_iterator<I> supporting +, -, +=, -=, and []
contiguous_iterator<I> A random_access_iterator<I> for elements in contiguous memory
permutable<I> A forward_iterator<I> supporting move and swap
mergeable<I1,I2,R,O> Can merge sorted sequences defined by I1 and I2 into O using relation<R>
sortable<I> / sortable<I,R> Can sort sequences defined by I using < / using relation<R>

The different categories of iterators are used to select the best algorithm for a given set of arguments (§8.2.2, §16.4.1). For an example of an input_iterator, see §13.3.1.

The basic idea of a sentinel: we can iterate over a range starting at an iterator until the predicate becomes true for an element. That way, an iterator p and a sentinel s define a range [p : s(*p)). For example, we could define a predicate for a sentinel for traversing a C-style string using a pointer as the iterator. This requires some boilerplate because the idea is to present the predicate as something that can't be confused with an ordinary iterator but that you can compare the iterator used to traverse the range to:

template<class Iter> class Sentinel {
public:
    Sentinel(int ee) : end(ee) { }
    Sentinel() : end(0) {}   // Concept sentinel_for requires a default constructor
    friend bool operator==(const Iter& p, Sentinel s) { return (*p == s.end); }
    friend bool operator!=(const Iter& p, Sentinel s) { return !(p == s); }
private:
    iter_value_t<const char*> end;   // the sentinel value
};
// The friend declarator defines == and != for comparing an iterator to a sentinel within the class scope.

static_assert(sentinel_for<Sentinel<const char*>, const char*>);   // check the Sentinel for C-style strings

const char aa[] = "Hello, World!\nBye for now\n";
ranges::for_each(aa, Sentinel<const char*>('\n'), [](const char x) { cout << x; });
// Yes, this really writes Hello, World! not followed by a newline.

14.5.3 Range Concepts

The range concepts define the properties of ranges (in <ranges>):

Concept Meaning
range<R> An R is a range with a begin iterator and a sentinel
sized_range<R> An R is a range that knows its size in constant time
view<R> An R is a range with constant time copy, move, and assignment
common_range<R> An R is a range with identical iterator and sentinel types
input_range<R> An R is a range whose iterator type satisfies input_iterator
output_range<R> An R is a range whose iterator type satisfies output_iterator
forward_range<R> An R is a range whose iterator type satisfies forward_iterator
bidirectional_range<R> An R is a range whose iterator type satisfies bidirectional_iterator
random_access_range<R> An R is a range whose iterator type satisfies random_access_iterator
contiguous_range<R> An R is a range whose iterator type satisfies contiguous_iterator

There are a few more concepts in <ranges>, but this set is a good start. The primary use of these concepts is to enable overloading of implementations based on type properties of their inputs (§8.2.2).

14.6 Advice

Here is a summary of the guidance from this chapter. All 9 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.

# Guideline §
1 When the pair-of-iterators style becomes tedious, use a range algorithm. 13.1, 14.1
2 When using a range algorithm, remember to explicitly introduce its name. 13.3.1
3 Pipelines of operations on a range can be expressed using views, generators, and filters. 14.2, 14.3, 14.4
4 To end a range with a predicate, you need to define a sentinel. 14.5
5 Using static_assert, we can check that a specific type meets the requirements of a concept. 8.2.4
6 If you want a range algorithm and there isn't one in the standard, just write your own. 13.6
7 The ideal for types is regular. 14.5
8 Prefer standard-library concepts where they apply. 14.5
9 When requesting parallel execution, be sure to avoid data races (§18.2) and deadlock (§18.3). 13.6

Retrieval Quiz

What is a range?

What are the three ways a range can be defined, and what does the third form enable?

Constrained algorithms

Where do the concept-constrained algorithms live, and what errors do ranges eliminate?

View ownership

What is the key difference between a view and a range, and what lifetime rule follows?

filter_view semantics

How does reading from a filter_view behave, and what does take_view do?

Range adaptors

Match the view to its effect: transform_view, drop_view, join_view, split_view.

Generators

Which generator gives an infinite range, and what does istream_view do?

Pipelines

How does the pipeline operator compose views, and in what order?

Pipeline performance

What does Stroustrup warn about pipeline implementations, and what is the fallback?

Type concepts

What does common_with enable, and what is the ideal regularity ladder?

Callable concepts

How do invocable, regular_invocable, predicate, relation, and strict_weak_order differ?

Iterator concepts

What distinguishes forward, bidirectional, random_access, and contiguous iterators?

Sentinels

What is a sentinel, and how does the C-style-string Sentinel example work?


Notes

Ranges :-

range generalization of {begin,end} sequences (<ranges>, namespace ranges)

3 forms {begin,end} | {begin,n} | {begin,pred} infinite + on-the-fly ranges

sort(v)sort(v.begin(),v.end()) ; kills silly errors (sort(v1.begin(),v2.end()))

kinds input / forward / bidirectional / random_access / contiguous concepts

Views :-

view way of looking at a range doesn't own elements must not outlive range

cheap to copy pass by value ; interface ≈ range

adaptors all / filter / transform / take / take_while / drop / drop_while / join / split / common

views::elements<1> one field of a struct sequence (Reading temps)

Generators :-

factories empty_view | single_view{x} | iota_view{x} infinite | iota_view{x,y} | istream_view<T>{is}

iota_view(42,52) 42..51 ; istream_view >>-driven range-for

composable transform_view(cplx, [](auto z){ return z*z; }) → 1 2 3 ⇒ 1 4 9

Pipelines :-

r | views::filter(odd) | views::take(3) Unix-style |

left to right r|f|g = (g_filter(f_filter(r))) ; initial r = range or generator

filters in namespace ranges::views (using namespace views;)

perf hair-raising template metaprogramming measure ; loop fallback obscures logic

Type concepts :-

core same_as, derived_from, convertible_to, common_with, integral family, assignable_from, swappable

common_with<X,Y> common_type_t<X,Y> conversion for mixed compare (string v/s const char*) ; specialize when needed (Bigint,long)

comparison equality_comparable(_with), totally_ordered(_with), three_way_comparable(_with)

object ladder destructible → constructible_from → move_constructible → movable → copyable → semiregular (+ default_initializable)regular (+ equality_comparable)

ideal regular ≈ int ; most classes start semiregular (no default ==)

callable invocable → regular_invocable (equality preserving) → predicate (bool) → relation → equivalence_relation (reflexive/symmetric/transitive) → strict_weak_order (<) — differences semantic only

Iterator concepts :-

ladder input_or_output → input (read) / output (write) → forward (multi-pass, ==) → bidirectional (--) → random_access (+ - [] ) → contiguous (memory)

composite permutable (move+swap), mergeable, sortable

purpose select best algorithm per argument category §8.2.2

Sentinels + range concepts :-

sentinel ends range by predicate [p : s(*p)) ; sentinel_for<S,I> requires default ctor

C-string example friend ==/!= iterator↔sentinel ; static_assert(sentinel_for<...>) ; for_each stops at '\n'

range concepts range, sized_range, view (const-time copy), common_range, input/output/forward/bidirectional/random_access/contiguous_range

use overloading on input type properties §8.2.2


Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed., Chapter 14: "Ranges." Addison-Wesley.
Reference: Chapter 1–12 Quick Reference & Glossary — keep it beside you while you study.
Recommended supplement: cppreference on ranges library, views (range adaptors), and concepts library.

Questions? Ask your agent — your teacher — about anything unclear: when a view dangles, why common_with matters for mixed-type arithmetic, or how a sentinel differs from an iterator. Follow-ups are expected, not optional.

Chapters

  1. Lesson 1: The Basics (Ch. 1)
  2. Lesson 2: User-Defined Types (Ch. 2)
  3. Lesson 3: Separate Compilation (Ch. 3)
  4. Lesson 4: Error Handling (Ch. 4)
  5. Lesson 5: Classes (Ch. 5)
  6. Lesson 6: Essential Operations (Ch. 6)
  7. Lesson 7: Templates (Ch. 7)
  8. Lesson 8: Concepts and Generic Programming (Ch. 8)
  9. Lesson 9: Library Overview (Ch. 9)
  10. Lesson 10: Strings and Regular Expressions (Ch. 10)
  11. Lesson 11: Input and Output (Ch. 11)
  12. Lesson 12: Containers (Ch. 12)
  13. Lesson 13: Algorithms (Ch. 13)
  14. Lesson 14: Ranges (Ch. 14 — this lesson)
  15. Lesson 15: Pointers and Containers (Ch. 15)
  16. Lesson 16: Utilities (Ch. 16)
  17. Lesson 17: Numerics (Ch. 17)
  18. Lesson 18: Concurrency (Ch. 18)
  19. Lesson 19: History and Compatibility (Ch. 19)
← Course Dashboard ← Ch. 13: Algorithms Ch. 15: Pointers & Containers → Quick Reference →