Blog 2026 10 21 C++26 Ranges: Better Infrastructure
Post
Cancel

C++26 Ranges: Better Infrastructure

Not every ranges improvement is a shiny new view adaptor. Some of the most impactful C++26 changes are infrastructure: making containers work better with lazy ranges, bringing random number generation into the ranges world, and removing a long-standing ergonomic paper cut from algorithms. This post covers all three.

approximately_sized_range and reserve_hint

Here’s a scenario that should bother you: you have a views::transform pipeline that maps Unicode codepoints to their uppercase equivalents. A single “ss” might become “SS” — the output length isn’t knowable in advance. When you ranges::to<std::vector>() the result, the implementation has two bad choices: traverse the range twice (once to count, once to fill), or call push_back repeatedly and eat the reallocations.

P2846R6 (Jakub Mazurkiewicz) introduces a middle ground with a new concept and a new customization point object.

sized_range vs approximately_sized_range

A sized_range gives you the exact element count in O(1) via ranges::size. An approximately_sized_range gives you a close enough estimate in O(1) via ranges::reserve_hint. Every sized_range automatically satisfies approximately_sized_range — the CPO falls back to ranges::size when it’s available.

The ranges::reserve_hint CPO

ranges::reserve_hint(r) resolves in this order:

  1. ranges::size(r) if the range is sized_range
  2. r.reserve_hint() member function
  3. ADL-found reserve_hint(r) free function

The result must be non-negative and representable in range_difference_t. It must evaluate in O(1).

Usage

A custom view provides reserve_hint by forwarding from its base:

1
2
3
4
5
6
7
8
9
10
11
template <input_range V>
class uppercase_view : public ranges::view_interface<uppercase_view<V>> {
    V base_;
public:
    constexpr auto reserve_hint()
        requires ranges::approximately_sized_range<V>
    {
        return ranges::reserve_hint(base_);
    }
    // ...
};

The consumer side is where the payoff comes. std::vector’s construction from an approximately_sized_range of MoveConstructible elements now calls reserve() with the hint. When the hint is accurate, zero reallocations.

Which views propagate the hint?

This is where the design gets thoughtful. Views that can reasonably estimate output size propagate reserve_hint: take, drop, adjacent, chunk, slide, stride, zip (minimum of hints), cartesian_product (product of hints).

Views where the output size is fundamentally unpredictable do not propagate it: filter, take_while, drop_while, join, split. A filter view might drop everything or keep everything — any hint would be misleading.

Why not a (lower, upper) bound?

The design was inspired by Rust’s Iterator::size_hint, which returns a (lower, Option<upper>) pair. P2846 deliberately chose a single value. The Rust community’s experience was that the upper bound was rarely populated and often confusing. A single best-effort estimate is simpler and in practice just as useful for calling reserve().

ranges::generate_random

C++11’s <random> API is scalar by design: you call engine() or dist(engine) one element at a time. If you want to fill a container, you write a loop. This makes auto-vectorization nearly impossible — the compiler sees one call at a time and can’t prove that batching them would produce the same sequence.

P1068R11 (Ilya Burylov, Alina Muracheva, and others) adds std::ranges::generate_random, a ranges-friendly way to fill a range with random numbers:

1
2
3
4
5
6
7
8
9
std::mt19937 engine{42};
std::vector<int> v(1000);

// Fill with raw engine output:
std::ranges::generate_random(v, engine);

// Fill with a distribution:
std::uniform_int_distribution<int> dist{1, 100};
std::ranges::generate_random(v, engine, dist);

The batch optimization hook

The real design insight is that generate_random is a customization point. The algorithm first checks whether the engine (or distribution) provides a generate_random member function for batch generation. If so, it calls that. If not, it falls back to element-wise generation via ranges::generate.

No standard-library engine provides this member yet — today the fallback always fires. But the interface is ready for custom engines that wrap vectorized backends (think Intel MKL or similar). When those engines implement the batch member, existing code using ranges::generate_random gets the speedup for free.

Argument order

The API follows ranges::fill-style conventions: range first, then engine, then optional distribution. Both range-based and iterator-pair overloads exist.

1
2
3
4
5
// Range overload
std::ranges::generate_random(v, engine);

// Iterator-pair overload
std::ranges::generate_random(v.begin(), v.end(), engine, dist);

The distribution overload requires std::is_arithmetic_v on the result type — no user-defined numeric types.

List-initialization for algorithms

This one is a pure ergonomic fix. Consider:

1
2
3
4
5
6
7
std::vector<std::complex<double>> v = { {1.0, 2.0}, {3.0, 4.0}, {5.0, 6.0} };

// C++23: you must spell out the type
auto it = std::find(v.begin(), v.end(), std::complex<double>{3.0, 4.0});

// C++26: braced-init-list works
auto it2 = std::find(v.begin(), v.end(), {3.0, 4.0});

P2248R8 (Barry Revzin) makes this work by giving the value-type template parameter a default. The same pattern applies to a long list of algorithms.

How it works

Before C++26, std::find looks like:

1
2
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value);

Template argument deduction can’t deduce T from a braced-init-list {3.0, 4.0}. C++26 adds a default:

1
2
3
template <class InputIterator,
          class T = typename iterator_traits<InputIterator>::value_type>
InputIterator find(InputIterator first, InputIterator last, const T& value);

Now T defaults to the iterator’s value type, and the braced-init-list initializes a T — which is std::complex<double> in our example. Member functions like v.push_back({3.0, 4.0}) already worked this way; algorithms just couldn’t because of template deduction rules.

Which algorithms benefit

find, count, fill, remove, replace, lower_bound, upper_bound, binary_search, search_n, and their ranges counterparts. Container erasure functions (std::erase) benefit too.

Notable exclusions: replace_copy doesn’t get a default because its single T serves both the old and new values — the committee decided the default would be ambiguous. Numeric algorithms from <numeric> are excluded due to lack of consensus.

Ranges algorithms: reordering for projections

The ranges versions needed a subtler fix. The value type must account for projections — ranges::find(v, {3.0}, &point::x) should deduce T as double, not point. P2248 reorders the template parameters so Proj precedes T, enabling T to default to projected_value_t<iterator_t<R>, Proj> — a new alias that correctly handles projections and proxy iterators (like vector<bool>).

Conclusion

These three changes don’t introduce new ways to compose views, but they make the existing ranges ecosystem meaningfully more practical. approximately_sized_range eliminates unnecessary reallocations when materializing lazy pipelines. generate_random is both a convenience and a future-proofing hook for vectorized random generation. And list-initialization for algorithms removes a paper cut that has annoyed every C++ programmer who’s ever written std::find with a compound type.

In the next part, we’ll cover the headliner: parallel range algorithms, plus the safety improvements for filter_view.

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.