C++ from Basics to Mastery — Part 18
Build lazy data pipelines with ranges and views
The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.
Iterators separate containers from algorithms, but classic calls repeatedly pass a begin and an end iterator. C++20 ranges accept a range as a unit, while views compose lazy transformations without copying the underlying data.
Pass a range directly
#include <algorithm>
#include <vector>
std::vector<int> scores{30, 80, 50, 100};
std::ranges::sort(scores);
auto found = std::ranges::find(scores, 80);
if (found != scores.end())
{
// Found the value.
}
There is less opportunity to mix iterators from different containers. A projection can also select a member without a separate transforming lambda.
std::ranges::sort(players, {}, &Player::Score);
A view computes values on demand
#include <ranges>
auto visibleNames = players
| std::views::filter([](const Player& player)
{
return player.IsVisible;
})
| std::views::transform([](const Player& player) -> const std::string&
{
return player.Name;
})
| std::views::take(10);
for (const std::string& name : visibleNames)
{
DrawName(name);
}
This pipeline does not create three intermediate vectors. Filtering and transformation occur while iteration requests elements, and take(10) stops after at most ten results.
Most views do not own their data
auto MakeBadView()
{
std::vector<int> local{1, 2, 3};
return local | std::views::filter([](int value) { return value > 1; });
}
The returned view refers to a local vector that is destroyed when the function returns. Iterating it accesses an expired object.
Before storing or returning a view, prove that the source range outlives every use. If the result needs independent ownership, materialize it into a container explicitly.
Lazy evaluation also postpones failures and side effects until iteration. Mutating the source can invalidate iterators exactly as it would in ordinary code.
Ranges are valuable because they describe stages of a data flow, not merely because they shorten syntax. If a pipeline grows long or mixes mutation with queries, name intermediate steps or return to explicit algorithms and loops.