C++ Ranges and Views

Last update: 2021-10-01 09:45:43 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

The ranges library separates the ability to iterate from ownership, traversal strength, and lazy transformation. A range supplies iteration boundaries; a view is a range suited to adaptor pipelines; an adaptor builds a new view whose elements are evaluated when iteration requests them.

cpp
#include <ranges>
#include <vector>

std::vector<int> source{1, 2, 3, 4, 5};
auto even_squares = source
    | std::views::filter([](int value) { return value % 2 == 0; })
    | std::views::transform([](int value) { return value * value; });

Constructing even_squares does not transform every element. Iteration invokes the predicate and transformation, so changes to source before iteration can be observed subject to the source container’s invalidation rules.

Core Concepts #

A type models std::ranges::range when std::ranges::begin and std::ranges::end are valid for an lvalue of that type. Iterator and sentinel types need not be the same. The concept establishes only that iteration is possible; it says nothing about ownership, size, multipass traversal, contiguity, or iterator lifetime.

cpp
template<class T>
concept range = requires(T& value) {
    std::ranges::begin(value);
    std::ranges::end(value);
};

The standard’s definition has equivalent intent but uses its own exposition-only details. std::vector<int> and std::vector<int>& both model range.

std::ranges::view refines range for lightweight, adaptable range objects. Its syntactic definition combines range, std::movable, and std::ranges::enable_view; the important requirements are semantic. A view must have constant-time move construction, and its copying, moving, assignment, and destruction requirements prevent an adaptor pipeline from unexpectedly doing element-wise work. A std::vector is a range but not a view because copying its elements is not constant time. std::string_view, std::ranges::ref_view, and many generated ranges are views.

A program-defined view generally derives from std::ranges::view_interface or std::ranges::view_base. view_interface<D> is a CRTP helper that conditionally supplies members such as empty, front, back, data, size, and indexing only when D has the required range capabilities. A program may specialize enable_view to true only for a cv-unqualified program-defined type that actually satisfies the view requirements; the specialization is a semantic promise, not a constraint bypass.

viewable_range and views::all #

viewable_range answers a different question: whether a particular range expression can be converted to a view. It admits constructible views and suitable lvalue or movable non-view inputs, while excluding problematic initializer-list cases. The specification has evolved since C++20, so application code should use the concept rather than reproduce its definition.

std::views::all is the conversion boundary. Conceptually, it preserves a constructible input view, wraps a non-view lvalue in ref_view, and wraps an eligible non-view rvalue in owning_view.

cpp
std::vector<int> values{1, 2, 3};
auto borrowed = std::views::all(values);              // ref_view
static_assert(std::ranges::view<decltype(borrowed)>);

auto owned = std::views::all(std::vector<int>{1, 2}); // owning_view

A ref_view does not extend the source object’s lifetime. Returning a view that refers to a local container is therefore erroneous. Not every view is non-owning: owning_view owns its base range and single_view stores its value. Determine lifetime from the concrete view and the source expression, not from the word “view.”

borrowed_range is another independent property. It permits an algorithm to return iterators from an rvalue range rather than std::ranges::dangling, because iterators from that concrete range can survive destruction of the range object. It does not mean that every iterator is perpetually valid; container mutation and the lifetime of any underlying storage still matter.

Factories and Adaptors #

Factories create source views, while adaptors transform an existing viewable_range.

FacilityKindStandard Baseline
views::empty<T>Empty source viewC++20
views::single(value)One stored valueC++20
views::iota(first, bound)Incrementing generated sequenceC++20
views::istream<T>(stream)Lazily extracted stream valuesC++20
views::repeat(value, bound)Repeated valueC++23
views::cartesian_product(...)Cartesian productC++23
views::counted(iterator, count)Counted source rangeC++20

views::transform, filter, take, drop, reverse, split, lazy_split, join, common, elements, keys, and values are C++20 adaptors. C++23 adds adaptors including adjacent, slide, chunk, stride, zip, zip_transform, join_with, and as_const. Availability depends on both the selected language mode and the standard library’s implementation.

An adaptor object can be invoked directly or partially applied:

cpp
auto doubled = std::views::transform(source, [](int value) {
    return value * 2;
});

auto double_each = std::views::transform([](int value) {
    return value * 2;
});
auto also_doubled = double_each(source);

The partially applied result is a range adaptor closure object. For a closure closure and viewable_range range, range | closure has the effect of closure(range). Closures can compose:

cpp
auto useful = std::views::filter(is_valid) | std::views::transform(normalize);
auto result = source | useful;

C++23 provides std::ranges::range_adaptor_closure for portable user-defined closures. Before C++23, implementation-internal closure bases were not application interfaces.

Range Properties and Utilities #

Refinement concepts describe independent properties of a range:

ConceptMeaning
input_rangeSupports single-pass input traversal.
forward_rangeSupports multipass forward traversal.
bidirectional_rangeSupports backward traversal.
random_access_rangeSupports constant-time indexed iterator movement.
contiguous_rangeElements occupy contiguous storage.
sized_rangeranges::size is valid with amortized constant complexity.
common_rangeIterator and sentinel have the same type.
borrowed_rangeRvalue-origin iterators need not dangle solely from range destruction.

Traversal concepts refine one another from input through contiguous. sized_range, common_range, and borrowed_range describe separate dimensions. C++23 adds constant_range; use it only when the project baseline provides it.

Customization-point objects handle arrays, members, and ADL correctly in generic code:

cpp
auto first = std::ranges::begin(source);
auto last = std::ranges::end(source);
auto count = std::ranges::size(source);

Other utility objects include cbegin, cend, rbegin, rend, empty, data, cdata, and ssize. Prefer them to assuming a .begin() member.

Range type aliases preserve iterator and proxy-reference behavior:

cpp
template<std::ranges::range R>
using iterator_t = std::ranges::iterator_t<R>;

template<std::ranges::range R>
using value_t = std::ranges::range_value_t<R>;

template<std::ranges::range R>
using reference_t = std::ranges::range_reference_t<R>;

Related aliases include sentinel_t, range_difference_t, range_size_t, and range_rvalue_reference_t. Const-oriented aliases such as const_iterator_t and range_const_reference_t have C++23 availability constraints.

Algorithms, Projections, and Materialization #

Range algorithms accept ranges directly and can apply a projection before their main operation. A projection changes how one algorithm observes an element; a transform view changes what iteration yields.

cpp
struct Record {
    int id;
    std::string name;
};

std::ranges::sort(records, {}, &Record::name);

The empty comparator selects the default ordering, while the projection supplies member names to it. Projections do not materialize transformed values.

Materialize a view when an owning result, stable traversal after a source change, or an eager boundary is required. In C++23, std::ranges::to expresses the conversion directly:

cpp
auto output = std::ranges::to<std::vector<int>>(even_squares);

On a C++20 baseline, construct a compatible container from the view’s iterator and sentinel only when that container supports the pair, or insert through its appropriate range/iterator interface. Materialization is the point that performs the lazy work and establishes a separate result; it should be deliberate rather than an accidental side effect of choosing a container API.

Comments

Read or comment on GitHub (sign-in required to comment)