any_view
Document #: | P3411R1 |
Date: | 2024-12-29 |
Project: | Programming Language C++ |
Audience: |
SG9, LEWG |
Reply-to: |
Hui Xie <hui.xie1990@gmail.com> Louis Dionne <ldionne.2@gmail.com> S. Levent Yilmaz <levent.yilmaz@gmail.com> |
range_reference_t
?any_view_options
constexpr
Supportview
Supportiterator
Supportany_view_options::contiguous
Needed ?any_view
const-iterable?common_range
supportborrowed_range
supportany_view
vector
vs
any_view
used in function
argumentsany_view<Foo>
and any_view<const Foo>
should just workThis paper proposes a new type-erased view: std::ranges::any_view
.
That type-erased view allows customizing the traversal category of the
view, its value type and a few other properties. For example:
class MyClass {
::unordered_map<Key, Widget> widgets_;
stdpublic:
::ranges::any_view<Widget> getWidgets();
std};
::ranges::any_view<Widget> MyClass::getWidgets() {
stdreturn widgets_ | std::views::values
| std::views::filter(myFilter);
}
Since being merged into C++20, the Ranges library has gained an increasingly rich and expressive set of views. For example,
// in MyClass.hpp
class MyClass {
::unordered_map<Key, Widget> widgets_;
stdpublic:
auto getWidgets() {
return widgets_ | std::views::values
| std::views::filter([](const auto&){ /*...*/ });
}
};
While such use of ranges is exceedingly convenient, it has the drawback of leaking implementation details into the interface. In this example, the return type of the function essentially bakes the implementation of the function into the interface.
In large applications, such liberal use of
std::ranges
can lead to increased header dependencies and often a significant
compilation time penalty.
Attempts to separate the implementation into its own translation
unit, as is a common practice for non-templated code, are futile in this
situation. The return type of the above definition of
getWidgets
is:
::ranges::filter_view<
std::ranges::elements_view<
std::ranges::ref_view<std::unordered_map<Key, Widget>>,
std1>,
::getWidgets()::<lambda(const auto:11&)> > MyClass
While this type is already difficult to spell once, it is much harder and more brittle to maintain it as the implementation or the business logic evolves. These challenges for templated interfaces are hardly unique to ranges: the numerous string types in the language and lambdas are some common examples that lead to similar challenges.
Type-erasure is a very popular technique to hide the concrete type of
an object behind a common interface, allowing polymorphic use of objects
of any type that model a given concept. In fact, it is a technique
commonly employed by the standard. std::string_view
std::function
and
std::function_ref
,
and std::any
are the type-erased facilities for the examples above.
std::span<T>
is another type-erasure utility recently added to the standard; and is
closely related to ranges in fact, by allowing type-erased
reference of any underlying contiguous range of
objects.
In this paper, we propose to extend the standard library with std::ranges::any_view
,
which provides a convenient and generalized type-erasure facility to
hold any object of any type that satisfies the
ranges::view
concept itself. std::ranges::any_view
also allows further refinement via customizable constraints on its
traversal categories and other range characteristics.
Designing a type like any_view
raises a lot of questions.
Let’s take std::function
as
an example. At first, its interface seems extremely simple: it provides
operator()
and users only need to configure the return type and argument types of
the function. In reality, std::function
makes many other decisions for the user:
std::function
and
the callable it contains
copyable
?std::function
own
the callable it contains?std::function
propagate const-ness?After answering all these questions we ended up with several types:
function
move_only_function
function_ref
copyable_function
The design space of any_view
is a
lot more complex than that:
input_range
,
forward_range
,
bidirectional_range
,
random_access_range
, or a
contiguous_range
?copyable
?sized_range
?borrowed_range
?common_range
?range_reference_t
?range_value_t
?range_rvalue_reference_t
?range_difference_t
?range
const-iterable?copyable
for
input_iterator
?input_iterator
?sized_sentinel_for<S, I>
?We can easily get a combinatorial explosion of types if we follow the
same approach we did for std::function
.
Fortunately, there is prior art to help us guide the design.
boost::ranges::any_range
The type declaration is:
template<
class Value
class Traversal
, class Reference
, class Difference
, class Buffer = any_iterator_default_buffer
, >
class any_range;
Users will need to provide
range_reference_t
,
range_value_t
and
range_difference_t
.
Traversal
is equivalent to
iterator_concept
, which decides the
traversal category of the range. Users don’t need to specify
copyable
,
borrowed_range
and
common_range
, because all
Boost.Range ranges are copyable
,
borrowed_range
and
common_range
.
sized_range
and
range_rvalue_reference_t
are not
considered in the design.
ranges::views::any_view
The type declaration is:
enum class category
{
= 0,
none = 1,
input = 3,
forward = 7,
bidirectional = 15,
random_access = random_access,
mask = 16,
sized };
template<typename Ref, category Cat = category::input>
struct any_view;
Here Cat
handles both the
traversal category and sized_range
.
Ref
is the
range_reference_t
. It does not allow
users to configure the
range_value_t
,
range_difference_t
,
borrowed_range
and
common_range
.
copyable
is mandatory in
range-v3.
This paper proposes the following interface:
enum class any_view_options
{
= 1,
input = 3,
forward = 7,
bidirectional = 15,
random_access = 31,
contiguous = 32,
sized = 64,
borrowed = 128
move_only };
template <class Element,
= any_view_options::input,
any_view_options Opts class Ref = Element&,
class Value = remove_cvref_t<Element>,
class RValueRef = add_rvalue_reference_t<remove_reference_t<Ref>>,
class Diff = ptrdiff_t>
class any_view {
class iterator; // exposition-only
class sentinel; // exposition-only
template <class View>
requires(!std::same_as<View, any_view> && std::ranges::view<View> &&
<View>())
view_options_constraint(View view);
any_view
(const any_view &) requires (!(Opts & any_view_options::move_only));
any_view
(any_view &&) = default;
any_view
&operator=(const any_view &) requires (!(Opts & any_view_options::move_only));
any_view
&operator=(any_view &&);
any_view
();
iterator begin();
sentinel end
size_t size() const requires(Opts & any_view_options::sized);
};
template <class Element, any_view_options Opts, class Ref, class Value, class RValueRef,
class Diff>
inline constexpr bool
<any_view<Value, Opts, Ref, RValueRef, Diff>> =
enable_borrowed_range& any_view_options::borrowed; Opts
The intent is that users can select various desired properties of the
any_view
by bitwise-or
ing
them. For example:
using MyView = std::ranges::any_view<Widget,
::ranges::any_view_options::bidirectional |
std::ranges::any_view_options::sized>; std
In Wrocław meeting, one important point was made: The majority of the
use case of any_view
is to use it as
a function parameter in the API boundary.
(any_view<Foo>); Bar algo
And in most of cases, the implementation of
algo
only iterate over the range
once. The design should make it easy to specify an
“input_range
only”
view
, and sometimes “read-only”
access to the elements (a
const
reference element type). That is,
<Foo>; // should be an input_range where the range_reference_t is Foo&
any_view<const Foo>; // should be an input_range where the range_reference_t is const Foo& any_view
With the proposed design, the above two use cases would work. Even though there are lots of template parameters, we do not expect users to specify them often because the default would work for majority of the use cases.
namespace any_view_options {
template <class> struct iterator_concept;
template <class> struct reference_type;
template <class> struct value_type;
template <class> struct difference_type;
template <class> struct rvalue_reference_type;
template <bool> struct sized;
template <bool> struct move_only;
template <bool> struct borrowed;
} // any_view_options
template <class Element, class... Options>
class any_view;
With this design, the two main use cases would still work
using MyView1 = any_view<Foo>; // should be an input_range where the range_reference_t is Foo&
using MyView2 = any_view<const Foo>; // should be an input_range where the range_reference_t is const Foo&
If the default options do not work, users can specify the options in this way:
using namespace std::ranges::any_view_options;
using MyView3 = std::ranges::any_view<Foo,
<std::contiguous_iterator_tag>,
iterator_concept<Foo>,
reference_type<true>,
sized<true>>; borrowed
The benefits of this approach are
An implementation of this approach would look like this: link
However, we believe that this overcomplicates the design.
In Wrocław meeting, one feedback we had was to explore the options to have “single expansion point”, i.e not to have too many template parameters
struct default_range_traits {};
template <class Element, class RangeTraits = default_range_traits>
class any_view;
In the RangeTraits
, the user can
define aliases to customize
iterator_concept
,
reference_type
etc, and define static constexpr bool
variables to customize sized
,
move_only
etc. If an alias or static constexpr bool
variable is missing, the default type or value will be used.
With this design, the two main use cases would still work
using MyView1 = any_view<Foo>; // should be an input_range where the range_reference_t is Foo&
using MyView2 = any_view<const Foo>; // should be an input_range where the range_reference_t is const Foo&
If the default options do not work, users can specify the options in this way:
struct MyTraits {
using iterator_concept = std::contiguous_iterator_tag;
using reference = int;
static constexpr bool sized = true;
static constexpr move_only = true;
};
using MyView3 = any_view<int, MyTraits>;
The benefits of this approach are
An implementation of this approach would look like this: link
However, every time an user needs to customize anything, they need to
define a
struct
,
which is verbose and inconvenient.
RangeTraits
If we decided to go with this alternative, we could have a utility that deduces the traits from another range.
template <class Range>
struct range_traits {
using iterator_concept = /* see-below */;
using reference_type = range_reference_t<Range>;
using value_type = range_value_t<Range>;
using rvalue_reference_type = range_rvalue_reference_t<Range>;
using difference_type = range_difference_t<Range>;
static constexpr bool sized = sized_range<Range>;
static constexpr bool move_only = !copyable<decay_t<Range>>;
static constexpr bool borrowed = borrowed_range<Range>;
};
// MyView4 is a contiguous, sized, copyable, non-borrowed int& range
using MyView4 = any_view<int, range_traits<std::vector<int>>>;
// MyView5 is a contiguous, sized, copyable, non-borrowed const int& range
using MyView5 = any_view<const int, range_traits<const std::vector<int>>>;
An implementation of this approach would look like this: link
template <typename T>
struct type_t {
using type = T;
};
template <typename T>
inline constexpr type_t<T> type{};
template <class Ref,
class IterConcept = input_iterator_tag,
class Value = decay_t<Ref>,
class RValueRef = remove_reference_t<Ref>&&,
class Difference = ptrdiff_t>
struct any_view_options {
<Ref> reference_type;
type_t<IterConcept> iterator_concept = {};
type_tbool sized = false;
bool move_only = false;
bool borrowed = false;
<Value> value_type;
type_t<RValueRef> rvalue_reference_type;
type_t<Difference> difference_type;
type_t};
template <class Element, any_view_options options = {.reference_type = type<Element&>}>
class any_view;
This is inspired by Barry’s blog post. Thanks to designated initializers and generated CTAD, the user code is extremely readable
using MyView1 = any_view<Foo>; // should be an input_range where the range_reference_t is Foo&
using MyView2 = any_view<const Foo>; // should be an input_range where the range_reference_t is const Foo&
using MyView3 = any_view<int, {.reference_type = type<int&>,
.iterator_concept = type<std::contiguous_iterator_tag>,
.value_type = type<long>}>;
using MyView4 = any_view<int, {.reference_type = type<int&>,
.iterator_concept = type<std::contiguous_iterator_tag>,
.sized = true,
.borrowed = true,
.value_type = type<long>}>;
Each option is named and user can skip parameters if they want to use
the default. However, the user has to follow the same order of the
options that are defined in
any_view_options
.
An implementation of this approach would look like this: link
range_reference_t
?If the first template parameter is
Ref
, we have:
template <class Ref,
= any_view_options::input,
any_view_options Opts class Value = remove_cvref_t<Ref>>
For a range with a reference to
T
, one would write
<T&> any_view
And for a
const
reference to T
, one would write
<const T&> any_view
However, it is possible that the user uses any_view<string>
without realizing that they specified the reference type and they now
make a copy of the string
every time
when the iterator is dereferenced.
any_view_options
range-v3
uses the name category
for the
category enumeration type. However, the authors believe that the name
std::ranges::category
is too general and it should be reserved for more general purpose
utility in ranges library. Therefore, the authors recommend a more
specific name: any_view_options
.
constexpr
SupportWe do not require
constexpr
in
order to allow efficient implementations using e.g. SBO. There is no
way, with the current working draft, to construct an object of different
type on a unsigned char[N]
or std::byte[N]
buffer in
constexpr
context.
view
SupportMove-only view
is worth
supporting as we generally support them in
ranges
. We propose to have a
configuration template parameter any_view_options::move_only
to make the any_view
conditionally
move-only. This removes the need to have another type
move_only_any_view
as we did for
move_only_function
.
We also propose that by default,
any_view
is copyable and to make it
move-only, the user needs to explicitly provide this template parameter
any_view_options::move_only
.
iterator
SupportIn this proposal, any_view::iterator
is an exposition-only type. It is not worth making this
iterator
configurable. If the
iterator
is only
input_iterator
, we can also make it
a move-only iterator. There is no need to make it copyable. Existing
algorithms that take “input only” iterators already know that they
cannot copy them.
any_view_options::contiguous
Needed ?contiguous_range
is still useful
to support even though we have already
std::span
.
But span
is non-owning and
any_view
owns the underlying
view
.
any_view
const-iterable?We cannot make any_view
unconditionally const-iterable. If we did,
views
with cache-on-begin, like
filter_view
and
drop_while_view
could no longer be
put into an any_view
.
One option would be to make
any_view
conditionally
const-iterable, via a configuration template parameter. However, this
would make the whole interface much more complicated, as each
configuration template parameter would need to be duplicated. Indeed,
associated types like Ref
and
RValueRef
can be different between
const
and
non-const
iterators.
For simplicity, the authors propose to make
any_view
unconditionally
non-const-iterable.
common_range
supportUnconditionally making any_view
a
common_range
is not an option. This
would exclude most of the Standard Library
view
s. Adding a configuration
template parameter to make any_view
conditionally common_range
is
overkill. After all, if users need
common_range
, they can use my_any_view | views::common
.
Furthermore, supporting this turns out to add substantial complexity in
the implementation. The authors believe that adding
common_range
support is not worth
the added complexity.
borrowed_range
supportHaving support for borrowed_range
is simple enough:
enable_borrowed_range
if the
template parameter is set to
true
Therefore, we recommend conditional support for
borrowed_range
. However, since
borrowed_range
is not a very useful
concept in general, this design point is open for discussion.
any_view
We propose providing the strong exception safety guarantee in the
following operations: swap, copy-assignment, move-assignment and
move-construction. This means that if the operation fails, the two
any_view
objects will be in their
original states. If the underlying view’s move constructor (or
move-assignment operator) is not
noexcept
,
the only way to achieve the strong exception safety guarantee is to
avoid calling these operations altogether, which requires
any_view
to hold its underlying
object on the heap so it can implement these operations by swapping
pointers. This means that any implementation of
any_view
will have an empty state,
and a “moved-from” any_view
will be
in that state.
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. This drawback is shared by
all C++ types that live at ABI boundaries, and while we don’t think this
impacts the livelihood of any_view
,
this evolutionary challenge should be kept in mind by WG21.
One of the major concerns of using type erasure is the performance
cost of indirect function calls and their impact on the ability for the
compiler to perform optimizations. With
any_view
, every iteration will have
three indirect function calls:
++it;
!= last;
it *it;
While it may at first seem prohibitive, it is useful to remember the
context in which any_view
is used
and what the alternatives to it are.
The following benchmarks were compiled with clang 20 with libc++, with -O3, run on APPLE M4 MAX CPU with 16 cores. We have done the same benchmarks on a 8 core Intel CPU and they have very similar results.
vector
vs
any_view
Naively, one would be tempted to benchmark the cost of iterating over
a
std::vector
and to compare it with the cost of iterating over
any_view
. For example, the following
code:
::vector v = std::views::iota(0, state.range(0)) | std::ranges::to<std::vector>();
stdfor (auto _ : state) {
for (auto i : v) {
::DoNotOptimize(i);
benchmark}
}
vs
::vector v = std::views::iota(0, state.range(0)) | std::ranges::to<std::vector>();
std::ranges::any_view<int&> av(std::views::all(v));
stdfor (auto _ : state) {
for (auto i : av) {
::DoNotOptimize(i);
benchmark}
}
Benchmark Time Time vector Time any_view
-----------------------------------------------------------------------------------------
[BM_vector vs. BM_AnyView]/1024 +7.3364 237 1975
[BM_vector vs. BM_AnyView]/2048 +7.7186 464 4042
[BM_vector vs. BM_AnyView]/4096 +7.7098 920 8011
[BM_vector vs. BM_AnyView]/8192 +7.6034 1835 15790
[BM_vector vs. BM_AnyView]/16384 +7.7470 3654 31966
[BM_vector vs. BM_AnyView]/32768 +8.1498 7300 66796
[BM_vector vs. BM_AnyView]/65536 +8.0808 14602 132599
[BM_vector vs. BM_AnyView]/131072 +8.0281 29189 263523
[BM_vector vs. BM_AnyView]/262144 +7.4773 58497 495899
OVERALL_GEOMEAN +8.6630 0 0
We can see that any_view
is 8.6
times slower on iteration than
std::vector
.
However, this benchmark is not a realistic use case. No one would create
a vector
, immediately create a type
erased wrapper any_view
that wraps
it and then iterate through it. Similarly, no one would create a lambda,
immediately create a std::function
and
then call it.
any_view
Since any_view
is intended to be
used at an ABI boundary, a more realistic benchmark would separate the
creation of the view in a different TU. Furthermore, most uses of
any_view
are expected to be with
non-trivial view pipelines, not with e.g. a raw
std::vector
.
As the pipeline increases in complexity, the relative cost of using
any_view
becomes smaller and
smaller.
Let’s consider the following example, where we create a moderately
complex pipeline and pass it across an ABI boundary either with a
any_view
or with the pipeline’s
actual type:
// header file
struct Widget {
::string name;
stdint size;
};
struct UI {
::vector<Widget> widgets_;
std::ranges::transform_view<complicated...> getWidgetNames();
std};
// cpp file
::ranges::transform_view<complicated...> UI::getWidgetNames() {
stdreturn widgets_ | std::views::filter([](const Widget& widget) {
return widget.size > 10;
}) |
::views::transform(&Widget::name);
std}
And this is what we are going to measure:
::UI ui = {...};
libfor (auto _ : state) {
for (auto& name : ui.getWidgetNames()) {
::DoNotOptimize(name);
benchmark}
}
In the any_view
case, we simply
replace std::ranges::transform_view<complicated...>
by std::ranges::any_view
and we measure the same thing.
Benchmark Time Time complicated Time any_view
-----------------------------------------------------------------------------------------------------
[BM_RawPipeline vs. BM_AnyViewPipeline]/1024 +0.8536 1315 2438
[BM_RawPipeline vs. BM_AnyViewPipeline]/2048 +0.8162 2713 4928
[BM_RawPipeline vs. BM_AnyViewPipeline]/4096 +0.6976 5637 9570
[BM_RawPipeline vs. BM_AnyViewPipeline]/8192 +0.7154 11539 19795
[BM_RawPipeline vs. BM_AnyViewPipeline]/16384 +0.6611 23475 38994
[BM_RawPipeline vs. BM_AnyViewPipeline]/32768 +0.6379 47792 78278
[BM_RawPipeline vs. BM_AnyViewPipeline]/65536 +0.6174 96976 156851
[BM_RawPipeline vs. BM_AnyViewPipeline]/131072 +0.6087 197407 317560
[BM_RawPipeline vs. BM_AnyViewPipeline]/262144 +0.5882 399623 634694
OVERALL_GEOMEAN +0.6862 0 0
This benchmark shows that
any_view
is about 68% slower on
iteration, which is much better than the previous naive benchmark.
However, note that this benchmark is still not very realistic. Writing
down the type of the view pipeline causes an implementation detail (how
the pipeline is implemented) to leak into the API and the ABI of this
class, and increases header dependencies, which defeats the purpose of
hiding implementation into a separate translation unit.
As a result, most people would instead copy the results of the
pipeline into a container and return that from getWidgetNames()
,
for example as a std::vector<std::string>
.
This leads us to our last benchmark, which we believe is much more
representative of what people would do in the wild.
vector<string>
vs any_view
As mentioned above, various concerns that are not related to
performance like readability or API/ABI stability mean that the previous
benchmarks are not really representative of what happens in the real
world. Instead, people in the wild tend to use owning containers like
std::vector
as a type erasure tool for lack of a better tool. Such code would look
like this:
// header file
struct UI {
::vector<Widget> widgets_;
std::vector<std::string> getWidgetNames() const;
std};
// cpp file
::vector<std::string> UI::getWidgetNames() const {
std::vector<std::string> results;
stdfor (const Widget& widget : widgets_) {
if (widget.size > 10) {
.push_back(widget.name);
results}
}
return results;
}
Benchmark Time Time vector Time any_view
---------------------------------------------------------------------------------------------------
[BM_VectorCopy vs. BM_AnyViewPipeline]/1024 -0.7042 8243 2438
[BM_VectorCopy vs. BM_AnyViewPipeline]/2048 -0.7226 17764 4928
[BM_VectorCopy vs. BM_AnyViewPipeline]/4096 -0.7379 36516 9570
[BM_VectorCopy vs. BM_AnyViewPipeline]/8192 -0.7927 95467 19795
[BM_VectorCopy vs. BM_AnyViewPipeline]/16384 -0.7893 185104 38994
[BM_VectorCopy vs. BM_AnyViewPipeline]/32768 -0.7890 371035 78278
[BM_VectorCopy vs. BM_AnyViewPipeline]/65536 -0.8079 816621 156851
[BM_VectorCopy vs. BM_AnyViewPipeline]/131072 -0.8121 1690305 317560
[BM_VectorCopy vs. BM_AnyViewPipeline]/262144 -0.8228 3581632 634694
OVERALL_GEOMEAN -0.7723 0 0
With this more realistic use case, we can see that
any_view
is 3 times faster. For the
returning vector
case, we have some
variations of the implementations to produce the vector, including
reserve
the maximum possible size,
or use the range pipelines with
ranges::to
.
They all have extremely similar results. In our benchmark, 10% of the
Widget
s were filtered out by the
filter pipeline and the name
string’s length is randomly 0-30. So some of
string
s are in the SBO and some are
allocated on the heap. We maintain that this code pattern is very common
in the wild: making the code simple and clean at the cost of copying
data, even though most of the callers don’t actually need a copy of the
data at all.
In conclusion, we believe that while it’s easy to craft benchmarks
that make any_view
look bad
performance-wise, in reality this type can often lead to better
performance by sidestepping the need to own the data being iterated
over.
Furthermore, by putting this type in the Standard library, we would
make it possible for implementers to use optimizations like selective
devirtualization of some common operations like
for_each
to achieve large
performance gains in specific cases.
vector
vs
any_view
Another very common use case is that library authors provide an API
that takes a range of elements. The library authors would like to hide
implementation details in its own library to reduce the header
dependencies and avoid leaking implementation details. Due to the lack
of type erasure utilities, typically the API takes a
vector
, even though the
implementation only needs to iterate once over the elements.
This is the benchmark we are measuring.
// algo.hpp
int algo1(const std::vector<std::string>& strings);
int algo2(std::ranges::any_view<std::string> strings);
// algo.cpp
int algo1(const std::vector<std::string>& strings) {
int result = 0;
for (const auto& str : strings) {
if (str.size() > 6) {
+= str.size();
result }
}
return result;
}
int algo2(std::ranges::any_view<std::string> strings)
{
int result = 0;
for (const auto& str : strings) {
if (str.size() > 6) {
+= str.size();
result }
}
return result;
}
With the vector
version, the user
needs to create a temporary vector
if they do not have it at the first place. So in our benchmark, we are
measuring
for (auto _ : state) {
::vector<std::string> widget_names;
std.reserve(ui.widgets_.size());
widget_namesfor (const auto& widget : ui.widgets_) {
.push_back(widget.name);
widget_names}
auto res = lib::algo1(widget_names);
::DoNotOptimize(res);
benchmark}
With the any_view
, we can simply
pass in a transform_view
for (auto _ : state) {
auto res = lib::algo2(ui.widgets_ | std::views::transform(&Widget::name));
::DoNotOptimize(res);
benchmark}
And here is the result:
Benchmark Time Time vector Time any_view
----------------------------------------------------------------------------------------------
[BM_algo_vector vs. BM_algo_AnyView]/1024 -0.8098 9615 1829
[BM_algo_vector vs. BM_algo_AnyView]/2048 -0.8169 20651 3782
[BM_algo_vector vs. BM_algo_AnyView]/4096 -0.8188 41035 7436
[BM_algo_vector vs. BM_algo_AnyView]/8192 -0.8275 87227 15044
[BM_algo_vector vs. BM_algo_AnyView]/16384 -0.8315 182922 30818
[BM_algo_vector vs. BM_algo_AnyView]/32768 -0.8407 381383 60771
[BM_algo_vector vs. BM_algo_AnyView]/65536 -0.8425 793920 125004
[BM_algo_vector vs. BM_algo_AnyView]/131072 -0.8656 1733654 232982
[BM_algo_vector vs. BM_algo_AnyView]/262144 -0.8640 3592842 488714
OVERALL_GEOMEAN -0.8364 0 0
We can see the any_view
version
is 4 times faster. This is a very common pattern in the real world code.
vector
has been used in API
boundaries as a type-erasure tool.
any_view
has been implemented in
[range-v3], with equivalent semantics as
proposed here. The authors also implemented a version that directly
follows the proposed wording below without any issue [ours].
any_view
.