Flatten Nested Collections Using std::views::join
The std::views::join range adaptor, introduced in C++20,
provides a clean, declarative way to flatten nested sequences without
allocating intermediate containers or copying underlying data. This
article explores how std::views::join works, its primary
mechanics, lazy evaluation semantics, and practical applications in
modern C++ codebases.
Understanding
std::views::join
In modern C++, nested collections—such as a
std::vector<std::vector<T>> or a collection of
strings—are common. Historically, flattening these structures into a
single continuous stream of elements required manual nested loops or
combining elements into an explicitly allocated secondary container via
algorithms like std::copy or
std::for_each.
The std::views::join adaptor (defined in the
<ranges> header) operates on a "range of ranges." It
transforms a collection containing sub-ranges into a single, linear
view. It acts as an abstraction layer that iterates through the outer
range, accesses each inner range in sequence, and yields each inner
element one by one.
Lazy Evaluation and Memory Efficiency
A key feature of std::views::join is that it is a
view. Views in C++20 Ranges exhibit the following
properties:
- Zero Allocations:
std::views::joindoes not allocate memory on the heap. It does not create a new, flattened container; rather, it stores an iterator pointing into the nested structure. - Lazy Evaluation: Elements are flattened on demand.
Iteration occurs only when an algorithm or range-based
forloop requests the next element. This makes it suitable for working with large collections or streaming pipelines where creating a flattened copy would be prohibitively expensive. - Reference Semantics: The yielded elements are references to the elements inside the original nested containers. Modifying non-const elements through the joined view modifies the underlying source data.
Basic Syntax and Usage
The adaptor can be applied using standard function-call syntax or the
pipe operator (|), which is idiomatic for range
composition.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
// Flattening the 2D vector using the pipe operator
auto flat_view = matrix | std::views::join;
for (int value : flat_view) {
std::cout << value << ' ';
}
// Output: 1 2 3 4 5 6 7 8 9
}Flattening Strings
Because a std::string is itself a range of characters,
applying std::views::join to a container of strings
flattens them into a continuous sequence of characters:
#include <iostream>
#include <vector>
#include <string>
#include <ranges>
int main() {
std::vector<std::string> words = {"Hello", " ", "World"};
for (char c : words | std::views::join) {
std::cout << c;
}
// Output: Hello World
}Composition with Other Range Adaptors
Because std::views::join returns a range, it can be
combined with other adaptors in a pipeline. For example, filtering
elements or transforming data before or after flattening:
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<std::vector<int>> nested = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Flatten the ranges, filter for even numbers, and multiply by 2
auto pipeline = nested
| std::views::join
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
for (int n : pipeline) {
std::cout << n << ' ';
}
// Output: 4 8 12 16
}Constraints and Considerations
While std::views::join simplifies code, certain
constraints apply:
- Input Requirements: The input must satisfy the
std::ranges::input_rangeconcept, and its value type must also satisfystd::ranges::input_range. It cannot be used directly on non-range elements. - Iterator Category: The resulting iterator category depends on the properties of both the outer and inner ranges. If either the outer or inner range is an input-only range, the resulting joined range is also an input-only range. Bidirectional iteration is only supported if both the outer range and inner ranges support bidirectional traversal and the inner range does not yield elements by value.
- Separators: Standard
std::views::joindoes not insert delimiters between inner ranges. For cases where a delimiter is needed (such as joining strings with a comma), C++23 introducesstd::views::join_with.