From 4dfde1b62a3a7c9adad4c699509b9813458372e5 Mon Sep 17 00:00:00 2001
From: huixie90 This paper proposes a new type-erased
- This paper proposes a new type-erased view:
+ Since being merged into C++20, the Ranges library has been enjoying a
-contribution of ever richer set of expressive
-Contents
any_view_category
1.1 R0<
2 Abstract
-view
:
-any_view
.3 Motivation and Examples
+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);
+ }
3 Motivation
view
s. For example,// in MyClass.hpp
-class MyClass{
-::unordered_map<Key, Widget> widgets_;
- stdpublic:
-auto getWidgets () {
- return widgets_ | std::views::values
- | std::views::filter(myFilter);
- }
-
-// other members
- };
// in MyClass.hpp
+class MyClass {
+::unordered_map<Key, Widget> widgets_;
+ stdpublic:
+auto getWidgets() {
+ return widgets_ | std::views::values
+ | std::views::filter(myFilter);
+ }
+
+// other members
+ };
While such use of ranges is exceedingly convenient, and indeed is the recommended and the intended use of the library, prudence is advisable when allowing such range definitions to “leak” away into the interface, @@ -584,11 +587,11 @@
::ranges::filter_view<
- std::ranges::elements_view<
- std::ranges::ref_view<std::unordered_map<Key, Widget>>,
- std1>,
- ::getWidgets()::<lambda(const auto:11&)> > MyClass
::ranges::filter_view<
+ std::ranges::elements_view<
+ std::ranges::ref_view<std::unordered_map<Key, Widget>>,
+ std1>,
+ ::getWidgets()::<lambda(const auto:11&)> > MyClass
Already hard to spell once, this expression template type is even harder to maintain against any evolution of the implementation of its business logic.
@@ -622,7 +625,7 @@Designing a type like
any_view
raises a lot of
questions.
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;
template<
+class Value
+ class Traversal
+ , class Reference
+ , class Difference
+ , class Buffer = any_iterator_default_buffer
+ , >
+class any_range;
It asks users to provide
range_reference_t
,
range_value_t
and
@@ -713,19 +716,19 @@
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;
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
.
@@ -740,81 +743,88 @@
This paper proposes the following interface:
-enum class any_view_category
-{
-= 0,
- none = 1,
- input = 3,
- forward = 7,
- bidirectional = 15,
- random_access = 31,
- contiguous = contiguous,
- mask = 32,
- sized = 64,
- borrowed = 128
- move_only_view };
-
-template <class Ref,
-= any_view_category::input,
- any_view_category Cat class Value = decay_t<Ref>,
- class RValueRef = add_rvalue_reference_t<remove_reference_t<Ref>>,
- class Diff = ptrdiff_t>
- class any_view;
enum class any_view_category
+{
+= 0,
+ none = 1,
+ input = 3,
+ forward = 7,
+ bidirectional = 15,
+ random_access = 31,
+ contiguous = contiguous,
+ mask = 32,
+ sized = 64,
+ borrowed = 128
+ move_only_view };
+
+template <class Ref,
+= any_view_category::input,
+ any_view_category Cat class Value = decay_t<Ref>,
+ class RValueRef = add_rvalue_reference_t<remove_reference_t<Ref>>,
+ class Diff = ptrdiff_t>
+ class any_view {
+// TODO: Include a rough synopsis for the class. Pseudo-code like `// only when sized_range` is OK.
+ // This part can be removed/simplified once you have actual wording.
+
+size_t size() const; // only when Cat includes `any_view_category::sized`
+
+using iterator = /* exposition-only */;
+ using sentinel = /* exposition-only */;
+
+// etc...
+ };
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, std::ranges::any_view_category::bidirectional | std::ranges::any_view_category::sized>;
using MyView = std::ranges::any_view<Widget, std::ranges::any_view_category::bidirectional | std::ranges::any_view_category::sized>;
Ref
or
Value
?If the first template parameter is
-Ref
,
template <class Ref,
-= any_view_category::input,
- any_view_category Cat class Value = decay_t<Ref>>
Ref
, we have:
+template <class Ref,
+= any_view_category::input,
+ any_view_category Cat class Value = decay_t<Ref>>
For a range with a reference to
int
, one would write
<int&> any_view
<int&> any_view
And for a const
reference to
int
, one would write
<const int&> any_view
<const int&> any_view
In case of a generator range, e.g a
transform_view
which generates
pr-value int
, the usage would
be
<int> any_view
<int> 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.
If the first template parameter is
-Value
,
template <class Value,
-= any_view_category::input,
- any_view_category Cat class Ref = Value&>
Instead, if the first template parameter is
+Value
, we have:
template <class Value,
+= any_view_category::input,
+ any_view_category Cat class Ref = Value&>
For a range with a reference to
int
, it would be less
verbose
<int> any_view
<int> any_view
However, in order to have a
const
reference to
int
, one would have to
explicitly specify the Value
,
the any_view_category and finally the
Ref
, i.e.
<int, any_view_category::input, const int&> any_view
<int, any_view_category::input, const int&> any_view
This is a bit verbose. In the case of a generator range, one would need to do the same:
-<int, any_view_category::input, int> any_view
Even though Option 1 is less verbose in few cases, it might create -unnecessary copies without user realizing it. The author recommends that -Option 2 is preferable.
+<int, any_view_category::input, int> any_view
Author Recommendation: Even though the first option +is less verbose in few cases, it might create unnecessary copies without +user realizing it. The author recommends the second option.
any_view_category
range-v3
uses the name
@@ -958,9 +968,9 @@
any_view
, every iteration will
have three indirect function calls:
-++it;
-!= last;
- it *it;
++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.
::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}
- }
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}
+ }
-O0
Benchmark Time Time vector Time any_view
-----------------------------------------------------------------------------------------------
[BM_vector vs. BM_AnyView]/1024 +3.4488 10423 46371
@@ -999,7 +1009,7 @@ 6.12.1.1<
[BM_vector vs. BM_AnyView]/131072 +3.4295 1335405 5915230
[BM_vector vs. BM_AnyView]/262144 +3.4320 2665004 11811264
OVERALL_GEOMEAN +3.4278 0 0
-O2
Benchmark Time Time vector Time any_view ------------------------------------------------------------------------------------------- [BM_vector vs. BM_AnyView]/1024 +14.8383 315 4991 @@ -1038,36 +1048,36 @@
or with the pipeline’s actual type: -any_view
+// 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}
// 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} - }
::UI ui = {...}; + libfor (auto _ : state) { + for (auto& name : ui.getWidgetNames()) { + ::DoNotOptimize(name); + benchmark} + }
In the
-any_view
case, we simply replacestd::ranges::transform_view<complicated...>
bystd::ranges::any_view
and we measure the same thing.6.12.2.1 -O0
+-O0
-Benchmark Time Time complicated Time any_view ------------------------------------------------------------------------------------------------------------ [BM_RawPipeline vs. BM_AnyViewPipeline]/1024 +0.4290 78469 112130 @@ -1080,7 +1090,7 @@
6.12.2. [BM_RawPipeline vs. BM_AnyViewPipeline]/131072 +0.4170 10142694 14372118 [BM_RawPipeline vs. BM_AnyViewPipeline]/262144 +0.4358 20386564 29270816 OVERALL_GEOMEAN +0.4120 0 0
6.12.2.2 -O2
+-O2
Benchmark Time Time complicated Time any_view ------------------------------------------------------------------------------------------------------------ [BM_RawPipeline vs. BM_AnyViewPipeline]/1024 +0.8066 3504 6330 @@ -1118,20 +1128,20 @@
as a type erasure tool for lack of a better tool. Such code would look like this: -std::vector
-// header file -struct UI { -::vector<Widget> widgets_; - std::vector<std::string> getWidgetNames() const; - std}; - -// cpp file -::vector<std::string> UI::getWidgetNames() const { - stdreturn widgets_ | std::views::filter([](const Widget& widget) { - return widget.size > 10; - }) | - ::views::transform(&Widget::name) | std::ranges::to<std::vector>(); - std}
6.12.3.1 -O0
++// header file +struct UI { +::vector<Widget> widgets_; + std::vector<std::string> getWidgetNames() const; + std}; + +// cpp file +::vector<std::string> UI::getWidgetNames() const { + stdreturn widgets_ | std::views::filter([](const Widget& widget) { + return widget.size > 10; + }) | + ::views::transform(&Widget::name) | std::ranges::to<std::vector>(); + std}
-O0
-Benchmark Time Time vector<string> Time any_view -------------------------------------------------------------------------------------------------------------- [BM_VectorCopy vs. BM_AnyViewPipeline]/1024 -0.5376 238558 110316 @@ -1144,7 +1154,7 @@
6.12.3. [BM_VectorCopy vs. BM_AnyViewPipeline]/131072 -0.4792 27501856 14321826 [BM_VectorCopy vs. BM_AnyViewPipeline]/262144 -0.4838 55950048 28883803 OVERALL_GEOMEAN -0.4917 0 0
6.12.3.2 -O2
+-O2
Benchmark Time Time vector<string> Time any_view -------------------------------------------------------------------------------------------------------------- [BM_VectorCopy vs. BM_AnyViewPipeline]/1024 -0.8228 35350 6264