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.
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.
Love the bitflag enum. Nothing says "modern C++" like ORing integer constants together.
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.
Did you actually read past the first benchmark? The whole point of section 7.13 is that nobody wraps a raw vector in
any_viewfor fun. The realistic comparison is against the vector-copy pattern, whereany_viewis 3-4x faster.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_viewvs 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 simplefilter | 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.
The deepest irony of this paper is in section 7.12:
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::functionwas the ABI-stable callable wrapper. Then we neededmove_only_function. Thencopyable_function. Thenfunction_ref. Each one a new vtable shape, a new ABI. Four types to do what one was supposed to do.any_viewalready has 14 configurable properties via template parameters and bitflags. The paper explicitly listscommon_rangeas something they punted on.approximately_sizedwas 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_viewexisting - 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.Cool. Can we get networking in the standard before I retire though. Asking for a friend (the friend is my pension).
The most revealing section in this paper is 7.11:
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. Makingany_viewguarantee 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.Authors shipping code they think is wrong because the committee outvoted them. Process working as designed. Everything is fine.
I hear the precedent argument but I think SG9 got both calls right here.
Move-only default is sensible - if you're using
any_viewat 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 anany_viewparameter, 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.
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 tobegin(), 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.
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.Fine. The branch cost is trivial, I'll give you that. My actual concern is the normative precedent. If
any_viewguarantees empty-after-move, LEWG will be asked "why doesn't the next type-erased wrapper do the same?" And the answer will be "becauseany_viewdid 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.
Boost.Range has had
any_rangefor over a decade. Good to see the committee catching up.It's more than just "catching up." Boost's
any_rangerequires copyability, has no move-only view support, nosized_rangeorborrowed_rangeoptions, 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_viewis the reference type, soany_view<string>silently copies strings on every dereference. P3411 makes the element type the first parameter and defaults the reference toElement&. That alone is worth the paper.Reference implementations: Beman project and the authors' own.
any_viewis 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 thatfilter_viewanddrop_while_viewcache theirbegin()iterator, so they can't be const-iterated. Makingany_viewunconditionally 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.
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 throughviews::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.Probably a dumb question but why can't you just return
autofrom functions and let the compiler figure it out?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:
Good luck maintaining that across API boundaries.
In Rust you just write
impl Iterator<Item=T>and it works. No committee papers needed.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_viewbehind an ABI boundary would cut our incremental build times by minutes.Modules will fix this.
laughs in cmake
[removed by moderator]
Rule 2.
Still bitter SG9 went with the bitflag enum over Alternative 3. Writing
any_view<int, {.sized = true, .borrowed = true}>would read so much better thanany_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.Has this been forwarded to LWG yet? The paper has full wording but I can't tell where it is in the pipeline.