C++20 Ranges and Views Explained
C++20 introduced the Ranges library, a major evolution of the Standard Template Library (STL) that fundamentally changes how developers interact with sequences of data. This article explores the concepts of ranges and views, explains how they replace verbose iterator pairs with cleaner abstractions, highlights their support for lazy evaluation and functional-style composition using the pipe operator, and demonstrates how they improve both code readability and performance.
Understanding Ranges
In pre-C++20 standards, algorithms in the
<algorithm> header required pairs of iterators to
operate on a sequence:
std::vector<int> numbers = {5, 2, 8, 1, 9};
std::sort(numbers.begin(), numbers.end());A range abstracts this concept. Conceptually, a
range is any object that refers to an iterable sequence of
elements—formally, any type that provides a begin()
iterator and an end() sentinel.
With the <ranges> header, standard algorithms now
accept ranges directly:
std::ranges::sort(numbers);All standard containers (such as std::vector,
std::list, and std::string) satisfy the
std::ranges::range concept, eliminating redundant code and
the risk of mismatched iterator pairs.
Understanding Views
A view is a lightweight, non-owning range that
provides a specific window into an underlying sequence. Views are
defined in the std::ranges::views (or simply
std::views) namespace.
Key characteristics of views include:
- Non-owning: Views do not own the data they reference; they only view elements owned by another container.
- Constant time operations: Creating, copying, moving, and destroying views are guaranteed \(O(1)\) operations.
- Lazy evaluation: Views do not transform or filter elements when created. Computations occur on-the-fly only when an element is accessed during iteration.
The Pipe Operator and Composability
Views can be composed using the bitwise OR operator (|),
functioning similarly to Unix shell pipes. This allows complex data
transformations to be chained sequentially without creating intermediate
containers.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter even numbers and square them
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int n : result) {
std::cout << n << " "; // Outputs: 4 16 36 64 100
}
}In this example, no new vectors are allocated in memory. The
filter and transform logic executes
dynamically as the for loop requests the next value.
Key Differences: Containers vs. Views
| Feature | Containers (e.g.,
std::vector) |
Views (e.g.,
std::views::take) |
|---|---|---|
| Ownership | Owns the underlying elements | Borrows/references elements |
| Memory Allocation | Allocates heap/stack memory | Zero dynamic allocations |
| Copy Complexity | \(O(N)\) | \(O(1)\) |
| Evaluation | Eager (processed immediately) | Lazy (processed on demand) |
Summary
Ranges and views modernize C++ by combining the performance of iterator-based algorithms with the clean, readable semantics of functional programming. By avoiding unnecessary allocations and supporting pipeline syntax, they make sequence manipulation more expressive, less error-prone, and highly efficient.