Blog 2026 10 28 C++26 Ranges: Parallel Algorithms and Safer Filters
Post
Cancel

C++26 Ranges: Parallel Algorithms and Safer Filters

In part 1 we looked at the new views C++26 adds, and in part 2 we covered the infrastructure improvements. This final part is about the biggest gap in C++20 ranges: you couldn’t run them in parallel. C++17 added execution policies to the classic std:: algorithms, but C++20 ranges never got them. If you wanted parallelism, you had to decompose your ranges pipeline back into iterator pairs. C++26 finally closes this gap - and while it was at it, the committee also fixed a long-standing foot-gun in filter_view that could silently produce undefined behaviour.

Parallel range algorithms

P3179R9 brings execution policies to over 50 range algorithms. The syntax is straightforward - the execution policy goes first:

1
2
3
4
5
6
7
std::vector<int> v = /* ... */;

std::ranges::sort(std::execution::par, v);

std::ranges::for_each(std::execution::par_unseq,
    std::views::iota(0, n),
    [&](int i) { data[i] *= 2; });

All the execution policies you know from C++17 work: seq, par, par_unseq, unseq.

Which algorithms?

Essentially all of the non-numeric ones. The list includes for_each, for_each_n, transform, copy, copy_if, move, fill, generate, replace, replace_if, remove, remove_if, unique, reverse, rotate, sort, stable_sort, partial_sort, nth_element, merge, partition, stable_partition, find, find_if, find_if_not, find_last, find_end, find_first_of, adjacent_find, count, count_if, all_of, any_of, none_of, equal, mismatch, search, swap_ranges, min_element, max_element, minmax_element, includes, set_union, set_intersection, set_difference, set_symmetric_difference, is_sorted, is_partitioned, is_heap, contains, starts_with, ends_with, among others.

There are some notable absences though, such as numeric algorithms (reduce, transform_reduce) and algorithms without C++17 parallel counterparts (push_heap, binary_search).

The key constraint: random access and sized

Here’s the design decision that matters most. Parallel range algorithms require random_access_range and sized_range — not just forward_range. Parallel backends need random access for divide-and-conquer decomposition, and they need a known size to partition the work.

This means you can’t directly parallelize a filter_view (which is bidirectional at best) or a views::iota over an unbounded range. You’d need to materialize or constrain the range first.

Projections and output ranges

Projections are fully supported. The output side is also ranges-aware - output is a range (not a bare output iterator), ensuring bounds are known:

1
2
3
4
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(5);
std::ranges::transform(std::execution::par, src, dst,
    [](int x) { return x * x; });

A few gotchas

for_each returns only the iterator, not the function object. With parallel execution, the callable is copied across threads - accumulating state in it would be a data race. If you need to collect results, use transform into an output range.

Callables must be copy_constructible (not just move_constructible), since the parallel backend may copy them across worker threads.

constexpr support is not included yet.

Filter view safety improvements

filter_view has been one of the more dangerous standard views since C++20. P3725R3 addresses three related foot-guns.

The foot-guns

1. Composing with reverse can crash. Consider:

1
2
3
4
5
auto result = v
    | views::filter(pred)
    | views::reverse
    | views::as_rvalue
    | ranges::to<std::vector>();

Depending on the values and predicate, this can overwrite memory of other objects leading to a core dump caused by undefined behaviour. The problem is subtle: reverse on a filter_view interacts badly with the cached begin().

2. Modifying elements through a filter iterator is UB. If you write through a filter_view iterator and the modification changes whether the predicate holds, the behaviour is undefined - even when it appears to work:

1
2
3
for (auto& entity : entities | views::filter(&Entity::isDead)) {
    entity.bringBackToLife();  // UB: invalidates the predicate
}

3. Filter views don’t work with const. Passing a filtered range to a function taking const auto& fails to compile because filter_view caches begin() internally, requiring mutability.

The fix

P3725 makes two changes:

First, filter_view now provides const begin() and const end() when the underlying range is an input range (not forward). Input ranges are single-pass, so no begin() caching is needed.

Second, the UB wording for modifying elements through filter iterators is relaxed. It’s now well-defined as long as the predicate is not re-evaluated on the modified element.

The practical pattern: views::to_input

The recommended fix for the dangerous compositions is to prepend views::to_input (which we covered in part 1) before the filter:

1
2
3
auto safe = v
    | views::to_input
    | views::filter(pred);  // well-defined, const-iterable

This downgrades the range to input-only, which prevents reverse from compiling (it requires bidirectional), turning the runtime UB into a compile-time error. It also enables const iteration since input ranges don’t need begin() caching.

Smaller ranges cleanups

C++26 also ships several defect resolutions and smaller fixes in the ranges space. None of these made it to their own post, but they’re worth knowing about:

  • std::basic_const_iterator convertibility (P2836R1) — const_iterator now follows its underlying type’s convertibility, fixing cases where conversions that should work were rejected.

  • Removing the common reference requirement from indirectly invocable concepts (P2997R1) - the indirectly_readable and related concepts had an overly strict common_reference requirement that rejected valid use cases with proxy iterators. Relaxed.

  • Algorithm function objects (P3136R1) - standardizes ranges::fold_left and related algorithm function objects.

  • Converting contiguous iterators to pointers (P3349R0) - allows implementations to convert contiguous iterators to pointers for better codegen in algorithms.

  • View iterator/sentinel constructors made private (P3059R2) - user-defined constructors that were accidentally part of the public API are now private, preventing misuse.

Conclusion

With parallel range algorithms, C++26 finally unifies the two halves of the standard algorithms story that have been split since C++17 and C++20. You can now compose views and run the result in parallel - as long as the range is random-access and sized. The filter_view safety improvements and views::to_input adaptor (covered in part 1) work together to turn a class of silent UB into compile-time errors. And the handful of DR fixes tighten up edge cases that have bitten early adopters of C++20 ranges.

This wraps up the C++26 ranges mini-series. The range library isn’t done evolving — there are plenty of features deferred to C++29 — but the C++26 additions fill the most painful gaps.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.

For C++ developers who give a damn

Better code. Better career. One email a week on modern C++ and what it takes to grow — written by someone still figuring it out too.

    We won't send you spam. Unsubscribe at any time.