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.
#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.
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.
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.
| Facility | Kind | Standard Baseline |
|---|---|---|
views::empty<T> | Empty source view | C++20 |
views::single(value) | One stored value | C++20 |
views::iota(first, bound) | Incrementing generated sequence | C++20 |
views::istream<T>(stream) | Lazily extracted stream values | C++20 |
views::repeat(value, bound) | Repeated value | C++23 |
views::cartesian_product(...) | Cartesian product | C++23 |
views::counted(iterator, count) | Counted source range | C++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:
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:
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:
| Concept | Meaning |
|---|---|
input_range | Supports single-pass input traversal. |
forward_range | Supports multipass forward traversal. |
bidirectional_range | Supports backward traversal. |
random_access_range | Supports constant-time indexed iterator movement. |
contiguous_range | Elements occupy contiguous storage. |
sized_range | ranges::size is valid with amortized constant complexity. |
common_range | Iterator and sentinel have the same type. |
borrowed_range | Rvalue-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:
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:
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.
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:
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)