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.
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:
{begin,end} pair of iterators;{begin,n} pair, where begin is an iterator and n is
the number of elements;
{begin,pred} pair, where begin is an iterator and
pred is a predicate — if pred(p) is true for the iterator
p, we have reached the end of the range. This allows infinite ranges and ranges
generated "on the fly" (§14.3).
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).
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();
}
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
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.
The standard library offers many useful concepts, in three families:
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
<.
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.
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).
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 |
What are the three ways a range can be defined, and what does the third form enable?
Where do the concept-constrained algorithms live, and what errors do ranges eliminate?
What is the key difference between a view and a range, and what lifetime rule follows?
How does reading from a filter_view behave, and what does take_view do?
Match the view to its effect: transform_view, drop_view, join_view, split_view.
Which generator gives an infinite range, and what does istream_view do?
How does the pipeline operator compose views, and in what order?
What does Stroustrup warn about pipeline implementations, and what is the fallback?
What does common_with enable, and what is the ideal regularity ladder?
How do invocable, regular_invocable, predicate, relation, and strict_weak_order differ?
What distinguishes forward, bidirectional, random_access, and contiguous iterators?
What is a sentinel, and how does the C-style-string Sentinel example work?
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
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)
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
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
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
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
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.