Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 11 – Iterators, algorithms and ranges

Writing your own C++ ranges and views

Write C++ ranges and views of your own: an iterator and a sentinel, std::ranges::view_interface, and static_assert checks of the range concepts.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A range is a begin() and an end() that meet at the end · A sentinel ends the Collatz sequence without computing where · A base class makes it a view, and assertions prove its concepts · Practise: iterators, sentinels and views. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Write an iterator and sentinel pair that models a range
  • Derive a view from std::ranges::view_interface and use what it adds
  • Prove with static_assert which range concepts a type models
  • Identify which iterator category an iterator reaches from the operations it defines

Before you start

The Collatz sequence of 27 has 112 values, the last of them 1, and nothing tells you that length without computing every value. Writing your own C++ ranges turns such a sequence into something every loop, algorithm and view accepts, computed as it is read, with two small types: an iterator: An object that marks a position in a sequence: *it reads the element there and ++it moves to the next one. Every standard container hands out begin(), its first element, and end(), the position just past its last; what else an iterator can do depends on its category. More in “C++ iterators, iterator categories and sentinels”, which produces the elements one at a time, and a sentinel: An object that marks where a range ends by a condition rather than by a position: an iterator is compared with it to ask whether the end has been reached, as with one that equals any pointer at a '\0'. Since C++20 its type may differ from the iterator's. More in “C++ iterators, iterator categories and sentinels”, which recognises where they end.

To the standard library, anything whose begin() returns an iterator and whose end() returns a sentinel that compares equal to that iterator at the end is a range. A view: A range that does not own its elements: it refers to elements stored elsewhere or computes them, is cheap to copy, and does its work only while it is iterated, as std::views::filter and std::views::iota do. More in “C++ views and pipelines: filter, transform, iota” adds a promise about cost: copying or moving it does not depend on how many elements it has. Deriving from std::ranges::view_interface makes a class a view and gives it members such as empty() and front(), and static_assert(std::ranges::forward_range<MyView>) proves at compile time that the type meets the concept you claim, before any code depends on it.

A range is a begin() and an end() that meet at the end

A range is checked with concept: A named compile-time test of template arguments, such as std::integral<T>, defined as template <typename T> concept Name = condition;. Used in a template's declaration, it makes the template accept only the types that satisfy it, and a type that fails is reported at the call. More in “C++20 concepts and requires-clauses”, and each concept names a few expressions that must compile:

  • std::ranges::range<R>: ranges::begin(r) gives an iterator and ranges::end(r) gives a sentinel for it. Member functions begin() and end() are the usual way to provide them.
  • std::sentinel_for<S, I>: the sentinel can be copied and default-constructed, and it == s compiles. The sentinel may be a type of its own, such as std::default_sentinel_t, which carries no data at all.
  • std::input_iterator<I>: *it reads an element, ++it steps to the next one, and the iterator names its value_type and difference_type. A nested iterator_concept tag says which category it aims for.
  • std::ranges::view<V>: a range that is movable and declared a view, which deriving from view_interface does. Since P2325R3, a view no longer has to be default-constructible.

Writing your own C++ ranges therefore starts with the iterator: everything else follows from what it can do. Each iterator category: The set of operations an iterator supports: input (read each element once, moving forwards), forward, bidirectional, random access and contiguous (the elements sit side by side in memory). Each category adds operations to the one before it, and an algorithm names the weakest it can work with. More in “C++ iterators, iterator categories and sentinels” adds operations, and each one makes more algorithms and views available:

The five iterator categories from input to contiguous, each with the operations it adds; CollatzView sits at forward.inputreads *it, steps ++it, one passforwardadds: copies replay; it == itbidirectionaladds: steps back with --itrandom accessadds: it + n, it[n], it - it, <contiguousadds: elements side by sideCollatzViewend() may return a sentinel type

The iterator categories, each adding to the one above it

Text description of the diagram

The figure stacks the five iterator categories of C++20, each one including everything above it.

  1. Input: an iterator that reads with *it and steps with ++it, in one pass.
  2. Forward adds that copies of an iterator replay the same values, and that two iterators can be compared with ==. The CollatzView of this lesson sits at this level, and its box is highlighted.
  3. Bidirectional adds stepping back with --it.
  4. Random access adds jumps: it + n, it[n], the distance it - it and comparisons such as <.
  5. Contiguous adds that the elements sit side by side in memory.

A dashed box below the stack notes that end() may return a sentinel, a type of its own rather than an iterator.

CollatzView, the view of the next section, sits at forward on this ladder: copies of its iterator replay the same values and can be compared, but it cannot step back.

Checkpoint

Check: What the library asks of a range and its iterator

  1. Question 1 of 2 What makes a type a range to the standard library?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: begin() returns an iterator, and end() returns a sentinel that compares equal to that iterator at the end

    std::ranges::range asks only that ranges::begin(r) gives an iterator and ranges::end(r) gives a sentinel for it, usually through member functions begin() and end().

  2. Question 2 of 2 An iterator supports *it, ++it, --it and comparison with ==, but not it + n. Where does it sit on the ladder?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Bidirectional

    Each category includes everything above it on the ladder and adds operations: forward adds == and replaying copies, bidirectional adds --it, and random access adds the jumps.

A sentinel ends the Collatz sequence without computing where

The Collatz sequence of a number halves it when it is even and replaces it with 3n + 1 when it is odd, until it reaches 1. Nobody knows in advance where it ends, which makes it a natural case for a sentinel: end() does not have to compute the last position, it only has to recognise it. The iterator stores the current value and a flag that becomes true after it has produced 1, and the sentinel compares equal to an iterator whose flag is set:

collatz.hpp C++ header · collatz.hpp
// The Collatz sequence of a start value as a view: the iterator computes the next value on ++, and a sentinel ends
// the sequence once it has passed 1. Deriving from view_interface makes it a view and adds front() and empty().
#pragma once
#include <cstdint>
#include <iterator>
#include <ranges>

class CollatzView : public std::ranges::view_interface<CollatzView> {
public:
    class iterator {
    public:
        using iterator_concept = std::forward_iterator_tag;
        using value_type = std::uint64_t;
        using difference_type = std::ptrdiff_t;

        iterator() = default;
        explicit iterator(std::uint64_t start) : value_(start) {}

        std::uint64_t operator*() const { return value_; }
        iterator& operator++() {
            if (value_ == 1) {
                done_ = true;  // 1 was the last value: now the iterator equals the sentinel
            } else {
                value_ = value_ % 2 == 0 ? value_ / 2 : 3 * value_ + 1;
            }
            return *this;
        }
        iterator operator++(int) {
            iterator before = *this;
            ++*this;
            return before;
        }

        bool operator==(const iterator&) const = default;
        bool operator==(std::default_sentinel_t) const { return done_; }

    private:
        std::uint64_t value_ = 1;
        bool done_ = false;
    };

    CollatzView() = default;
    explicit CollatzView(std::uint64_t start) : start_(start) {}

    iterator begin() const { return iterator{start_}; }
    std::default_sentinel_t end() const { return {}; }

private:
    std::uint64_t start_ = 1;
};

// The promises, checked by the compiler: if a requirement is missing, the build stops here.
static_assert(std::forward_iterator<CollatzView::iterator>);
static_assert(std::sentinel_for<std::default_sentinel_t, CollatzView::iterator>);
static_assert(std::ranges::forward_range<CollatzView>);
static_assert(std::ranges::view<CollatzView>);
static_assert(!std::ranges::sized_range<CollatzView>);
collatz.cpp C++ · collatz.cpp
// Using CollatzView: printing it, measuring it, and putting it in a pipeline with the standard views.
#include <algorithm>
#include <cstdint>
#include <print>
#include <ranges>
#include "collatz.hpp"

int main() {
    std::println("{}", CollatzView{6});
    CollatzView c{27};
    std::println("27: {} values, largest {}, first {}", std::ranges::distance(c), std::ranges::max(c), c.front());
    auto odd_tens = c | std::views::filter([](std::uint64_t x) { return x % 2 == 1; }) |
                    std::views::transform([](std::uint64_t x) { return x * 10; }) | std::views::take(5);
    std::println("{}", odd_tens);
}
The sequence, its length and a pipeline, on both compilers Python · collatz_session.py
from session import run

# C++20 ranges, so both compilers build the same file.
run("""
clang++ -std=c++23 -Wall -Wextra collatz.cpp -o collatz && ./collatz
g++-16 -std=c++23 -Wall -Wextra collatz.cpp -o collatz && ./collatz
""")

Output

$ clang++ -std=c++23 -Wall -Wextra collatz.cpp -o collatz && ./collatz
[6, 3, 10, 5, 16, 8, 4, 2, 1]
27: 112 values, largest 9232, first 27
[270, 410, 310, 470, 710]
$ g++-16 -std=c++23 -Wall -Wextra collatz.cpp -o collatz && ./collatz
[6, 3, 10, 5, 16, 8, 4, 2, 1]
27: 112 values, largest 9232, first 27
[270, 410, 310, 470, 710]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collatz_session.py

The sequence of 6 takes eight steps to reach 1, and the sequence of 27 has 112 values with a peak of 9232, numbers that the program computed without storing a single value. Step through the calls that any loop over CollatzView{6} makes, a range-for loop as much as std::println:

Interactive · message by message A loop over CollatzView{6}, one call at a time Step through the calls a loop makes. The iterator holds two members, value_ and done_; the sentinel holds nothing, and the comparison with it decides when the loop ends.
A loop over CollatzView{6}, one call at a time, step 10 of 10. Messages so far: The loop to The iterator: begin(); The loop to The sentinel: it == end?; The iterator to The loop: *it: 6; The loop to The iterator: ++it; The loop to The iterator: ++it; The iterator to The loop: *it: 1; The loop to The iterator: ++it; The loop to The sentinel: it == end?.The loop27: 112 values countedThe iteratorvalue_ 1, done_ trueThe sentineldefault_sentinel_tbegin()it == end?*it: 6++it++it*it: 1++itit == end?

The drawing shows the whole scene; its 10 steps are listed below.

Every step as text
  1. begin() and end(). A loop starts with begin(), an iterator whose value_ is 6 and whose done_ is false, and end(), a std::default_sentinel_t that holds no data at all. Nothing has computed where the sequence stops.
  2. Is this the end?. Before each value the loop compares the iterator with the sentinel. The iterator answers itself: its operator== with std::default_sentinel_t returns done_, which is false.
  3. The first value. *it returns value_, so the loop receives 6, the first value printed.
  4. 6 is even. ++it computes the next value from the current one: 6 is even, so value_ becomes 3. The sequence is computed one step at a time and never stored.
  5. 3 is odd. The loop compares, reads 3 and steps again: 3 is odd, so value_ becomes 3 × 3 + 1 = 10.
  6. The same three calls. Compare, read, step: the loop repeats the same three calls for 10, 5, 16, 8, 4 and 2. Each ++ halves an even value or triples an odd one and adds 1.
  7. 1 arrives. done_ is still false, so the comparison says no, and the loop reads 1, the ninth value.
  8. ++ after 1. At 1 the sequence has ended, so ++it computes nothing new: it sets done_ to true, and from now on the iterator compares equal to the sentinel.
  9. The end. The comparison now returns true and the loop stops after [6, 3, 10, 5, 16, 8, 4, 2, 1]. end() never computed the last position; the sentinel only recognised it when the iterator got there.
  10. Counting is walking. Nothing knows the length in advance, so std::ranges::distance(c) for CollatzView{27} makes the same calls to count the values, 112 of them, and std::ranges::max walks them again to find 9232.

Four details make it work:

  • begin() and end() are const, so the view can also be iterated through a const reference, the way many functions take their ranges.
  • The iterator defines operator== with std::default_sentinel_t, and C++20 derives != and the reversed operand order from it, so one comparison covers every test the loops and algorithms write.
  • It also has a default constructor, compares iterators with each other and has a postfix ++ that returns the old position, which is what std::forward_iterator requires in addition to an input iterator.
  • std::ranges::distance walks the whole sequence to count it, because nothing knows its size in advance; the static_assert that it is not a sized_range documents that.

Key idea

end() returns a sentinel that only recognises the end: the iterator’s operator== with it answers from the iterator’s own state, so the sequence is never computed ahead of the loop.

Checkpoint

Check: An iterator and a sentinel for a sequence that ends on a condition

  1. Question 1 of 2 In CollatzView, what decides that a loop has reached the end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The iterator's operator== with std::default_sentinel_t, which returns done_, set by the ++ that follows 1

    ++ at value 1 sets done_ to true, and from then on it == std::default_sentinel compares true, so the loop stops after printing 1.

  2. Question 2 of 2 Why are CollatzView's begin() and end() const member functions?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that the view can also be iterated through a const reference, the way many functions take their ranges

    A const view can be iterated only through const begin() and end(), and passing views by const reference is common, so const members keep the view usable everywhere.

A base class makes it a view, and assertions prove its concepts

Because CollatzView is a view, the standard views compose with it: filter, transform and take turned the sequence of 27 into its first five odd values times ten, computed lazily as println asked for them. front() came from std::ranges::view_interface: A class template of <ranges> that a view derives from, passing its own type: it declares the class a view and gives it members such as empty() and front(), built from the class's own begin() and end(), so the class writes none of them., which builds it from begin().

The five assertions at the end of collatz.hpp state what the type promises, and the compiler checks each against the members of CollatzView. Step through them in order:

Interactive · stage by stage Five promises that collatz.hpp makes the compiler check Step through the assertions in the order they appear. Each one names a concept, and the compiler checks it against the members of CollatzView before any code uses the view.
Five promises that collatz.hpp makes the compiler check, step 6 of 6. Showing std::forward_iterator (holds: forward), std::sentinel_for (holds: it == end compiles), ranges::forward_range (holds: begin() and end()), ranges::view (holds: via view_interface), not ranges::sized_range (holds: no size()).std::forward_iteratorholds: forwardstd::sentinel_forholds: it == end compilesranges::forward_rangeholds: begin() and end()ranges::viewholds: via view_interfacenot ranges::sized_rangeholds: no size()

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The iterator. The first assertion checks the iterator on its own: *it, ++it and it++, the member types value_type and difference_type, a default constructor and it == it. CollatzView::iterator defines every one of them.
  2. The sentinel. The second asks whether std::default_sentinel_t can mark the end of that iterator: it can be copied and default-constructed, and it == std::default_sentinel compiles through the operator== that returns done_.
  3. The view as a range. The third looks at CollatzView itself: begin() returns that iterator and end() that sentinel, so it is a forward range, whose copies of iterators replay the same values.
  4. The promise about cost. The fourth checks that CollatzView is a view: it can be moved, and deriving from std::ranges::view_interface declared it one. Copying it copies one number, whatever the length of the sequence.
  5. A promise it does not make. The last assertion states what the type does not do: it has no constant-time size, so nobody expects size(), and std::ranges::distance walks the 112 values of 27 to count them.
  6. All five hold. Both compilers accepted all five, so the build went on to main. Had a member been missing, the build would have stopped at that assertion, next to the class, before any code used CollatzView.

Five more ideas build on this one. Bidirectional iterators add --it. Random-access iterators add it + n, it[n] and it - it, which give a range its size. A range adaptor closure: An object such as std::views::take(2) that takes a range and returns a view, so that r | c means c(r): the form that lets adaptors chain with | into a pipeline. gives an adaptor of your own the | syntax. The debug mode: A checked build of a standard library, such as libstdc++ with -D_GLIBCXX_DEBUG or libc++'s debug hardening mode, whose containers and iterators check how they are used and stop the program at the first misuse. It is for testing, because it slows code down. More in “std::deque, std::list and iterator invalidation” of GCC’s library checks every step an iterator makes. A borrowed range is one whose iterators stay valid after the range object itself is gone.

Text Diff Paste the outputs of the GCC and Apple clang builds of your view to see at a glance whether both compilers produced the same sequence.

Checkpoint

Check: view_interface and the static_assert proofs

  1. Question 1 of 2 CollatzView defines begin() and end() only. Where did c.front() come from?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: From std::ranges::view_interface, which builds front() from begin()

    view_interface is a base class that a view derives from: it declares the class a view and builds members such as empty() and front() out of the class's own begin() and end().

  2. Question 2 of 2 The pipeline keeps the odd values of CollatzView{27}, multiplies them by 10 and takes 5. What did it print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [270, 410, 310, 470, 710]

    Because CollatzView is a view, filter, transform and take compose with it like with any standard view, computing lazily: only as many Collatz values are computed as take asks for.

Practise: iterators, sentinels and views

These questions cover the range concepts, the Collatz sentinel and the five assertions; each answer explains itself at once.

Practice that adapts to you

Practise this lesson

9 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Which expression must compile for std::sentinel_for<S, I>, besides copying and default-constructing the sentinel?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: it == s

    std::sentinel_for asks that the sentinel can be copied and default-constructed and that it == s compiles. It may be a type of its own, such as std::default_sentinel_t, which carries no data at all.

  2. Question 2 of 9 An iterator defines *it, ++it, it++, value_type and difference_type, and nothing else. Which category can it model?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Input

    What forward adds to input is a default constructor, comparing iterators with each other and a postfix ++ that returns the old position, which CollatzView's iterator has.

  3. Question 3 of 9 You write a view over lines read from std::cin, which can be read only once. Which iterator category is honest for it?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Input, because a copy of the iterator cannot replay lines that were already consumed

    Claim the category the iterator can keep: an input iterator reads each element once, in one pass, and that is all a stream offers.

  4. Question 4 of 9 What does CollatzView's end() return?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: A std::default_sentinel_t, an empty object

    end() only has to recognise the end, not compute it: std::default_sentinel_t holds nothing, and the iterator's operator== with it returns done_.

  5. Question 5 of 9 How many values does a loop over CollatzView{6} receive?

    Type a number.

    A hint

    Count 6 and 1 as well as every value between them.

    Show the answer: Question 5 of 9

    Answer: 9 values

    6, 3, 10, 5, 16, 8, 4, 2 and 1: eight steps reach 1, and with the start that is nine values, as the session printed.

  6. Question 6 of 9 Why would giving CollatzView a constant-time size() be the wrong design?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Its length is known only by walking the sequence, so a size() would hide a full walk behind a call that looks cheap

    The static_assert that CollatzView is not a sized_range documents it: counting means walking all 112 values for 27, so the honest interface leaves size out and lets distance do the walk when someone asks.

  7. Question 7 of 9 What does deriving from std::ranges::view_interface give a class?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: It declares the class a view and adds members such as empty() and front(), built from its begin() and end()

    view_interface is the base class that marks a view and gives it convenience members, all computed from the class's own begin() and end().

  8. Question 8 of 9 Which assertion of collatz.hpp documents that std::ranges::distance has to walk the whole sequence?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: static_assert(!std::ranges::sized_range<CollatzView>);

    Each assertion states one promise, and the negative one states a promise the type does not make, so nobody expects a size().

  9. Question 9 of 9 Why put the static_asserts of the concepts right after the class, rather than wait for the first algorithm that needs them?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: A missing operation then fails at the class, where it is defined, before any code depends on it

    A static_assert next to the class proves at compile time that the type meets the concept you claim, so a missing operation is reported at the class, before any other code relies on it.

Forward, bidirectional and random-access views, proved and broken Pro: opens with a Pro, Premium or Ultimate pass

Each step up the iterator ladder costs a few operations and pays back with algorithms and members that only work from that step on. This part climbs from an endless forward view to a random-access view that binary search can use, and reproduces what both compilers report when one operation is missing.

In this part:

  • An endless Fibonacci view, forward and multi-pass
  • A random-access view that binary search can use
  • One missing operator, reproduced: what each compiler reports

6 runnable examples (C++, Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A stride view for libc++, tested against GCC 16's views::stride Pro: opens with a Pro, Premium or Ultimate pass

The libc++ 22 of Apple clang 21 has no views::stride, so this part builds one: a forward view that never steps past the end, with the pipe syntax of the standard adaptors, checked against GCC 16’s views::stride on 294 cases.

In this part:

  • Your turn: every n-th element on libc++
  • The stride view, its iterator and its adaptor object
  • Checked against std::views::stride on both compilers
  • Stepping past the end, reproduced
  • What the standard view does that this one does not

5 runnable examples (C++, C++ header, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Borrowed ranges and dangling iterators, reproduced Pro: opens with a Pro, Premium or Ultimate pass

An algorithm called on a temporary range returns an iterator into something that is about to be destroyed. This part shows how the library turns that bug into a compile error, and when a view of your own may safely declare that its iterators outlive it.

In this part:

  • What find returns for a range that is about to die
  • Declaring a view of your own borrowed

2 runnable examples (C++, Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between a range and a view in C++20, and why is std::vector a range but not a view?

    A range is anything whose begin() returns an iterator and whose end() returns a sentinel for it, so a vector, a std::list and a pair of iterators wrapped in a subrange are all ranges. A view is a range with an extra promise about cost: copying or moving it, and destroying the copies, does not depend on how many elements it has, and it is declared a view, usually by deriving from std::ranges::view_interface. Views such as views::filter or a string_view refer to elements or compute them, so they are cheap to pass around by value. A std::vector owns its elements: copying one copies every element, so it is a range but not a view, and a pipeline that starts from a vector wraps it in a view first.

  2. Mid to senior

    A range-for loop over your custom container works, but std::ranges::sort and views::reverse reject it with long errors. How do you find what the iterator is missing, and which operations does each iterator category require?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Design a view over the lines of a text buffer that ends at the first empty line. Would end() return an iterator or a sentinel, and why? What changes for the code that uses the view?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A hand-written stride view advances its iterator with std::ranges::next(it, n). Its tests use vectors whose sizes are multiples of n and pass, but production crashes now and then. Explain the bug, how you would reproduce it, and how std::views::stride avoids it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A range is a begin() that returns an iterator and an end() that returns a sentinel comparing equal to it at the end, and the operations of the iterator decide which category on the ladder it reaches.
  • A sentinel of its own type, such as std::default_sentinel_t, ends a sequence on a condition, so end() never has to compute where the sequence stops.
  • Make begin() and end() const, define iterator_concept, value_type and difference_type, and let one operator== with the sentinel cover every comparison.
  • Deriving from std::ranges::view_interface makes the class a view with members such as front(), and static_asserts of the concepts next to the class make a missing operation fail there, before any code uses it.

Terms in this lesson

range adaptor closure
An object such as std::views::take(2) that takes a range and returns a view, so that r | c means c(r): the form that lets adaptors chain with | into a pipeline.
std::ranges::view_interface
A class template of <ranges> that a view derives from, passing its own type: it declares the class a view and gives it members such as empty() and front(), built from the class's own begin() and end(), so the class writes none of them.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.