P3411R5 - any_view WG21
Posted by u/ranges_aficionado_23 · 7 hr. ago

Authors: Hui Xie, S. Levent Yilmaz, Louis Dionne
Document: P3411R5
Date: 2025-06-16
Target: SG9, LEWG
Link: wg21.link/p3411r5

If you've ever tried to return a ranges pipeline from a function and wept at the return type, this paper is for you. P3411 proposes std::ranges::any_view - a type-erased view that lets you hide the template gore behind a clean interface, the same way std::function did for callables but for ranges.

The design uses a bitflag enum any_view_options to configure properties: input/forward/bidirectional/random_access/contiguous, sized, borrowed, copyable. Default is move-only input. There's a thorough performance section - the naive benchmark shows 8.6x overhead vs raw vector iteration, but the paper argues the real comparison is against the vector-copy pattern people actually use at ABI boundaries, where any_view wins by 3-4x.

R5 adds approximately_sized support and derives from view_interface. Five revisions in, the design has been through multiple SG9 polls. Notable detail: the authors explicitly disagree with SG9's decision on moved-from semantics but implemented it anyway. Prior art is range-v3's any_view and Boost.Range's any_range, both of which this subsumes.

▲ 156 points (84% upvoted) · 27 comments
sorted by: best
u/AutoModerator 1 point 7 hr. ago pinned comment

Paper: P3411R5 - any_view
Authors: Hui Xie, S. Levent Yilmaz, Louis Dionne
Target: SG9, LEWG
Link: wg21.link/p3411r5

Reminder: be civil. Paper authors sometimes read these threads.

u/bitflag_enjoyer 187 points 5 hr. ago

Love the bitflag enum. Nothing says "modern C++" like ORing integer constants together.

u/perf_is_king 143 points 6 hr. ago

8.6x slower than vector iteration. Doesn't matter how you frame the benchmarks, that number is going to be in every "why C++ ranges are slow" blog post for the next decade.

u/actually_reads_papers 34 points 5 hr. ago

Did you actually read past the first benchmark? The whole point of section 7.13 is that nobody wraps a raw vector in any_view for fun. The realistic comparison is against the vector-copy pattern, where any_view is 3-4x faster.

u/pipeline_pragmatist 52 points 4 hr. ago 🏆

The paper's argument is actually well-structured here, but it buries the lede.

There are three benchmarks. The first (8.6x overhead) is explicitly called a strawman by the authors. Fair enough. The third (3-4x faster vs vector-copy) is the strongest case, and it's real - people absolutely do copy into vectors at ABI boundaries because there's no good type-erased alternative today.

The one to scrutinize is benchmark 2: any_view vs the spelled-out pipeline type across a TU boundary. That's 68% overhead, and the paper somewhat hand-waves it by saying "well you wouldn't spell out the return type anyway." True for complex pipelines, but for a simple filter | transform, plenty of codebases will accept the ugly return type over a 68% runtime hit.

The deeper point the paper hints at but doesn't say outright: if you're at an API boundary where you'd use any_view, you're already paying for indirection - virtual calls, dynamic dispatch, cross-TU function calls. Three more indirect calls per iteration step aren't free, but they're in the noise compared to the cost you're already paying at that boundary.

Also all benchmarks are on Apple M4 Max, which has deep pipelines and excellent branch prediction. Would be nice to see numbers on more typical server hardware.

u/abi_boundary_survivor 89 points 5 hr. ago 🏆🏆

The deepest irony of this paper is in section 7.12:

As a type intended to exist at ABI boundaries, ensuring the ABI stability of any_view is extremely important. However, since almost any change to the API of any_view will require a modification to the vtable, this makes any_view somewhat fragile to incremental evolution.

So the pitch is: here's a type for your ABI boundary. But if we add any feature to it later, your ABI breaks.

We've seen this movie before. std::function was the ABI-stable callable wrapper. Then we needed move_only_function. Then copyable_function. Then function_ref. Each one a new vtable shape, a new ABI. Four types to do what one was supposed to do.

any_view already has 14 configurable properties via template parameters and bitflags. The paper explicitly lists common_range as something they punted on. approximately_sized was just added in R5. What happens when R8 needs another flag? Another bit, another vtable entry, another ABI break for everyone who was relying on the old layout.

The paper acknowledges the problem and then says "this evolutionary challenge should be kept in mind by WG21." That's not a solution. That's a prayer.

I'm not against any_view existing - the use case is real. But if you're designing a type whose primary selling point is "put this at your ABI boundary," you need an actual strategy for vtable evolution, not a paragraph of hand-waving.

u/tcp_enjoyer 76 points 6 hr. ago

Cool. Can we get networking in the standard before I retire though. Asking for a friend (the friend is my pension).

u/sg9_regular 47 points 4 hr. ago

The most revealing section in this paper is 7.11:

However, the authors still believe that this is a wrong decision. This is an unprecedented design in the standard library type erasure facilities. All existing type erasure utilities in the library leave the moved-from object in a valid but unspecified state.

The authors implemented something they believe is wrong because SG9 voted for it (5 SF / 2 F / 0 N / 2 A / 0 SA in Sofia). This is actually how the process should work - you don't have to agree with the committee's direction to write the wording. But dissent being in the paper is unusual and worth paying attention to.

The precedent argument is the strong one. Every type-erased wrapper in the standard (function, move_only_function, any) leaves moved-from objects in a valid-but-unspecified state. Making any_view guarantee an empty view after move creates a divergence. Either future type-erased types will be expected to match this, or we get an inconsistency where some wrappers guarantee empty-after-move and others don't.

u/process_cynic_99 67 points 3 hr. ago

Authors shipping code they think is wrong because the committee outvoted them. Process working as designed. Everything is fine.

u/api_designer_daily 28 points 3 hr. ago

I hear the precedent argument but I think SG9 got both calls right here.

Move-only default is sensible - if you're using any_view at an API boundary, you're probably not copying it. And for the moved-from state: "valid but unspecified" for a type designed for API boundaries is actively hostile. If I take an any_view parameter, move from it, and then someone iterates the moved-from object in a code path I didn't foresee - what happens? With unspecified state, anything. With the empty-view guarantee, you get an empty range. Predictable and safe.

The "don't pay for what you don't use" argument doesn't hold here. The type already heap-allocates in most cases. Setting a null pointer on move is one store instruction.

u/sg9_regular 19 points 2 hr. ago

The cost isn't the null pointer store on move. It's the check on every begin(). If the moved-from state is "empty view," the implementation has to produce a valid empty range from a moved-from object. That means a branch on every call to begin(), and it's a branch that's always-not-taken in correct code.

"Don't pay for what you don't use" means something specific here. This guarantee benefits exactly one scenario: buggy code that uses an object after moving from it.

u/api_designer_daily 14 points 2 hr. ago

A branch that's always-not-taken costs exactly nothing on any modern CPU - perfectly predicted. And "buggy code that uses after move" describes roughly 40% of the C++ code I've reviewed in the last decade. The guarantee isn't for people who write correct code. It's for the other 95% of us.

Meanwhile you're defending consistency with std::function's moved-from behavior, which is responsible for approximately infinite segfaults in production. Great precedent to uphold.

u/sg9_regular 11 points 1 hr. ago

Fine. The branch cost is trivial, I'll give you that. My actual concern is the normative precedent. If any_view guarantees empty-after-move, LEWG will be asked "why doesn't the next type-erased wrapper do the same?" And the answer will be "because any_view did it and it was fine." Then we're locked into guaranteeing moved-from behavior for every type-erased wrapper going forward, including ones where the cost isn't trivial.

But you're probably right that it's the correct call for this type specifically.

u/senior_boost_user 34 points 6 hr. ago

Boost.Range has had any_range for over a decade. Good to see the committee catching up.

u/ranges_nerd_42 31 points 5 hr. ago

It's more than just "catching up." Boost's any_range requires copyability, has no move-only view support, no sized_range or borrowed_range options, and uses the old Boost.Iterator traversal categories instead of C++20 concepts.

This proposal also fixes a range-v3 design trap. In range-v3, the first template parameter to any_view is the reference type, so any_view<string> silently copies strings on every dereference. P3411 makes the element type the first parameter and defaults the reference to Element&. That alone is worth the paper.

Reference implementations: Beman project and the authors' own.

u/const_correctness_guy 38 points 3 hr. ago

any_view is not const-iterable. Let that sink in.

If you write a function that takes const any_view<Widget>&, you cannot iterate it. The paper's rationale is that filter_view and drop_while_view cache their begin() iterator, so they can't be const-iterated. Making any_view unconditionally const-iterable would exclude these views.

This is technically correct. It's also going to generate a thousand Stack Overflow questions from people who wrote perfectly reasonable-looking code that doesn't compile. For a type that exists to simplify APIs, that's a meaningful ergonomic landmine.

u/pipeline_pragmatist 26 points 2 hr. ago

The const-iterability thing I can live with - it mirrors the underlying views and you learn the rule once. The bigger gap is the lack of common_range. The paper says "just pipe through views::common" but that's wrapping one type-erased type in another adaptor that will itself need to be type-erased. At some point you're paying double indirection per iteration step, which undermines the whole performance story.

u/junior_dev_2025 12 points 5 hr. ago

Probably a dumb question but why can't you just return auto from functions and let the compiler figure it out?

u/template_fatigue 42 points 5 hr. ago

Because then your return type is the implementation. Change a lambda or add a filter step and every translation unit that includes your header recompiles. That's literally section 3 of the paper - the spelled-out return type of a simple filter+transform pipeline is:

std::ranges::filter_view<
  std::ranges::elements_view<
    std::ranges::ref_view<
      std::unordered_map<Key, Widget>>,
    1>,
  MyClass::getWidgets()::<lambda(const auto&)>>

Good luck maintaining that across API boundaries.

u/crab_evangelist -8 points 4 hr. ago

In Rust you just write impl Iterator<Item=T> and it works. No committee papers needed.

u/compile_time_sufferer 19 points 4 hr. ago

The compile-time argument is undersold in this paper. We have a project where one ranges-heavy header pulls in 50,000 lines of template instantiations. any_view behind an ABI boundary would cut our incremental build times by minutes.

u/modules_believer 5 points 3 hr. ago

Modules will fix this.

u/not_holding_breath 44 points 3 hr. ago

laughs in cmake

[deleted] 5 hr. ago

[removed by moderator]

u/paper_trail_2019 3 points 5 hr. ago

Rule 2.

u/designated_init_fan 23 points 3 hr. ago

Still bitter SG9 went with the bitflag enum over Alternative 3. Writing any_view<int, {.sized = true, .borrowed = true}> would read so much better than any_view<int, any_view_options::sized | any_view_options::borrowed>. The vote was 10 vs 4 so I lost, but I will die on this hill.

u/standard_schedule_watcher 8 points 2 hr. ago

Has this been forwarded to LWG yet? The paper has full wording but I can't tell where it is in the pipeline.