第8章 视图类型详解
# 第8章 视图类型详解
本章将详细讨论C++20标准库引入的所有视图类型。首先会概述所有视图类型,并讨论视图的通用特性,例如C++标准库提供的公共基类。随后,将按视图的通用特性分组,分多个部分讨论C++20的各种视图类型:作为视图管道初始构建块的视图(使用现有值或自行生成值)、过滤和转换视图、变异视图,以及处理多个范围的视图。每种视图类型的描述均以其最重要特性的概述开始。
注意:下一章关于
std::span
的内容将讨论跨度视图的更多细节(历史上,跨度不属于范围库,但它们也是视图)。
# 8.1 所有视图概述
C++20提供了大量不同的视图类型。其中一些包裹现有数据源的元素,一些自行生成值,还有许多对元素或其值进行操作(过滤或转换)。本章将简要概述C++20中所有可用的视图类型。需要注意的是,几乎所有这些类型都有辅助的范围适配器/工厂,允许程序员通过调用函数或使用管道操作符来创建视图。通常应优先使用这些适配器和工厂,而非直接初始化视图,因为它们能执行额外优化(例如在不同视图中选择最佳方案)、在更多场景下工作、双重检查需求,并且更易于使用。
# 8.1.1 包裹和生成视图概述
“包裹和生成视图”表列出了只能作为管道源元素的标准视图。这些视图可能是:
- 包裹视图:操作外部资源(如容器或从输入流读取的元素)的元素序列。
- 工厂视图:自行生成元素的视图。
类型 | 适配器/工厂 | 效果 |
---|---|---|
std::ranges::ref_view | all(rg) | 对范围的引用 |
std::ranges::owning_view | all(rg) | 包含范围的视图 |
std::ranges::subrange | counted(beg,sz) | 起始迭代器和哨兵 |
std::span | counted(beg,sz) | 指向连续内存的起始迭代器和大小 |
std::ranges::iota_view | iota(val) iota(val, endVal) | 递增数值的生成器 |
std::ranges::single_view | single(val) | 仅包含一个值的视图 |
std::ranges::empty_view | empty<T> | 无元素的视图 |
std::ranges::basic_istream_view | - | 从流中读取元素 |
std::ranges::istream_view<> | istream<T>(strm) | 从字符流读取 |
std::ranges::wistream_view<> | - | 从wchar_t 流读取 |
std::basic_string_view | - | 字符的只读视图 |
std::string_view | - | char 序列视图 |
std::u8string_view | - | char8_t 序列视图 |
std::u16string_view | - | char16_t 序列视图 |
std::u32string_view | - | char32_t 序列视图 |
std::wstring_view | - | wchar_t 序列视图 |
表8.1 包裹和生成视图
该表同时列出了视图类型及其对应的范围适配器/工厂名称(如有)。对于C++17引入的std::basic_string_view<>
类型,C++提供了别名类型std::string_view
、std::u8string_view
、std::u16string_view
、std::u32string_view
和std::wstring_view
。对于std::basic_istream_view<>
类型,C++提供了别名类型std::istream_view
和std::wistream_view
。
注意:视图类型使用不同的命名空间:
span
和string_view
位于std
命名空间。- 所有范围适配器和工厂位于
std::views
命名空间(该命名空间是std::ranges::views
的别名)。- 其他所有视图类型位于
std::ranges
命名空间。
通常应优先使用范围适配器和工厂。例如,应始终优先使用适配器std::views::all()
,而非直接使用类型std::ranges::ref_view<>
和std::ranges::owning_view<>
。但在某些情况下(如必须从迭代器对创建视图),则需直接初始化std::ranges::subrange
。
# 8.1.2 适配视图概述
“适配视图”表列出了以某种方式适配给定范围元素的标准视图(过滤元素、修改元素值、改变元素顺序或组合/创建子范围)。它们尤其适用于视图管道内部。
类型 | 适配器 | 效果 |
---|---|---|
std::ranges::take_view | take(num) | 前(最多)num 个元素 |
std::ranges::take_while_view | take_while(pred) | 所有满足谓词的起始元素 |
std::ranges::drop_view | drop(num) | 除前num 个元素外的所有元素 |
std::ranges::drop_while_view | drop_while(pred) | 除满足谓词的起始元素外的所有元素 |
std::ranges::filter_view | filter(pred) | 所有满足谓词的元素 |
std::ranges::transform_view | transform(func) | 所有元素的转换后的值 |
std::ranges::elements_view | elements<idx> | 所有元素的第idx 个成员/属性 |
std::ranges::keys_view | keys | 所有元素的第一个成员 |
std::ranges::values_view | values | 所有元素的第二个成员 |
std::ranges::reverse_view | reverse | 所有元素的逆序 |
std::ranges::split_view | split(sep) | 将范围分割为多个子范围 |
std::ranges::lazy_split_view | lazy_split(sep) | 将输入或常量范围延迟分割为多个子范围 |
std::ranges::join_view | join | 多个范围的所有元素 |
std::ranges::common_view | common() | 迭代器和哨兵类型相同的所有元素 |
表8.2 适配视图
同样应优先使用范围适配器而非直接使用视图类型。例如,应始终优先使用适配器std::views::take()
而非类型std::ranges::take_view<>
,因为适配器可能在能直接跳转到底层范围第n
个元素时完全不创建取前视图。另一个例子是,应始终优先使用适配器std::views::common()
而非类型std::ranges::common_view<>
,因为只有适配器允许传递已普通化的范围(直接使用它们)。用common_view
包裹已普通化的范围会导致编译时错误。
# 8.2 视图的基类和命名空间
所有标准视图均派生自类std::ranges::view_interface<viewType>
。该类模板基于派生视图类型的begin()
和end()
定义,引入了多个基本成员函数。“std::ranges::view_interface<>
的操作”表列出了该类为视图提供的API。
操作 | 效果 | 提供条件 |
---|---|---|
r.empty() | 返回r 是否为空(begin() == end() ) | 至少前向迭代器 |
if (r) | r 非空时为true | 至少前向迭代器 |
r.size() | 返回元素数量 | 能计算begin 和end 的差值 |
r.front() | 返回第一个元素 | 至少前向迭代器 |
r.back() | 返回最后一个元素 | 至少双向迭代器且end() 与begin() 类型相同 |
r[idx] | 返回第n 个元素 | 至少随机访问迭代器 |
r.data() | 返回元素内存的原始指针 | 元素位于连续内存中 |
表8.3 std::ranges::view_interface<>
的操作
view_interface<>
类还为每个派生类型将std::ranges::enable_view<>
初始化为true
,这意味着这些类型满足std::ranges::view
概念。定义自定义视图类型时,应将其派生自view_interface<>
并传递自身类型作为参数。例如:
template<typename T>
class MyView : public std::ranges::view_interface<MyView<T>> {
public:
... begin() ...;
... end() ...;
...
};
2
3
4
5
6
7
基于begin()
和end()
的返回类型,若满足可用性前提条件,你的类型将自动提供表中列出的成员函数。这些成员函数的const
版本要求视图类型的const
版本是有效范围。
C++23将添加成员cbegin()
和cend()
,它们映射到std::ranges::cbegin()
和std::ranges::cend()
(通过http://wg21.link/p2278r4 (opens new window)添加)。
# 8.2.2 为什么范围适配器/工厂有自己的命名空间
范围适配器和工厂有自己的命名空间std::ranges::views
,并为其定义了命名空间别名std::views
:
namespace std {
namespace views = ranges::views;
}
2
3
这样,我们可以使用可能在其他命名空间(甚至是另一个标准命名空间)中使用的视图名称。
因此,在使用视图时必须对其进行限定:
std::ranges::views::reverse // 完全限定
std::views::reverse // 快捷方式
2
通常,无法在不限定的情况下使用视图。参数依赖查找(ADL)不会生效,因为std::views
命名空间中没有定义范围(容器或视图):
std::vector<int> v;
...
take(v, 3) | drop(2); // 错误:找不到视图(可能找到不同的符号)
2
3
即使视图生成的对象不属于视图命名空间(它们位于std::ranges
中),这意味着在使用视图时仍需对其进行限定:
std::vector<int> values{0, 1, 2, 3, 4};
auto v1 = std::views::all(values);
auto v2 = take(v1, 3); // 错误
auto v3 = std::views::take(v1, 3); // 正确
2
3
4
需要注意的重要一点是:永远不要使用using
声明来省略范围适配器的限定:
using namespace std::views; // 不要这样做
以composers示例为例,如果我们仅过滤出定义了本地values
对象的值,就会遇到麻烦:
std::vector<int> values;
...
std::map<std::string, int> composers{ ... };
using namespace std::views; // 不要这样做
for (const auto& elem : composers | values) { // 错误:找到错误的values
...
}
2
3
4
5
6
7
8
在这个示例中,我们会使用本地的vector
对象values
而不是视图。这里你很幸运,因为会得到一个编译时错误。如果不幸运,不限定的视图可能会找到不同的符号,导致未定义行为,甚至可能使用或覆盖其他对象的内存。
# 8.3 引用外部元素的源视图
本节讨论C++20中创建引用现有外部值(通常作为单个范围参数、起始迭代器和哨兵,或起始迭代器和计数传递)的视图的所有特性。
# 8.3.1 子范围
类型:std::ranges::subrange<> 内容:从传递的起始到结束的所有元素 工厂: std::views::counted() 反转子范围上的 std::views::reverse() 元素类型:传递的迭代器的值类型 要求:至少输入迭代器 类别:与传递的迭代器相同 是否为有大小范围:如果传递公共随机访问迭代器或大小提示 是否为公共范围:如果基于公共迭代器 是否为借用范围:始终是 缓存:无 是否可 const 迭代:如果传递的迭代器可复制常量性传播:从不 |
---|
类模板std::ranges::subrange<>
定义了一个视图,指向通常作为起始迭代器和哨兵(结束迭代器)对传递的范围的元素。不过,你也可以传递单个范围对象,间接传递起始迭代器和计数。该视图内部通过存储起始(迭代器)和结束(哨兵)来表示元素。
子范围视图的主要用例是将起始迭代器和哨兵(结束迭代器)对转换为一个对象。例如:
std::vector<int> coll{0, 8, 15, 47, 11, -1, 13};
std::ranges::subrange s1{std::ranges::find(coll, 15),
std::ranges::find(coll, -1)};
print(coll); // 15 47 11
2
3
4
你可以使用特殊的结束值哨兵来初始化子范围:
std::ranges::subrange s2{coll.begin() + 1, EndValue<-1>{}};
print(s2); // 8 15 47 11
2
这两种方式都特别有助于将迭代器对转换为范围/视图,以便范围适配器等处理元素:
void foo(auto beg, auto end) {
// 初始化视图,排除前五个元素(如果有的话):
auto v = std::ranges::subrange{beg, end} | std::views::drop(5);
...
}
2
3
4
5
下面是一个完整示例,演示了子范围的使用:
// ranges/subrange.cpp
#include <iostream>
#include <string>
#include <unordered_map>
#include <ranges>
void printPairs(auto&& rg) {
for (const auto& [key, val] : rg) {
std::cout << key << " : " << val << " ";
}
std::cout << "\n";
}
int main() {
// 英德词典:
std::unordered_multimap<std::string, std::string> dict = {
{"strange", "fremd"},
{"smart", "klug"},
{"car", "Auto"},
{"smart", "raffiniert"},
{"trait", "Merkmal"},
{"smart", "elegant"},
};
// 获取所有"smart"翻译的起始和结束迭代器:
auto [beg, end] = dict.equal_range("smart");
// 创建子范围视图以打印所有翻译:
printPairs(std::ranges::subrange(beg, end));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
该程序的输出如下:
smart:klug smart:elegant smart:raffiniert
在equal_range()
为我们提供了字典中所有键为"smart"的元素的起始和结束迭代器后,我们使用子范围将这两个迭代器转换为视图(这里是字符串对的视图):
std::ranges::subrange(beg, end)
然后,我们可以将该视图传递给printPairs()
来遍历元素并打印它们。
需要注意的是,只要起始和结束迭代器保持有效,子范围的大小可能会改变,例如元素在起始和结束之间插入或删除:
std::list coll{1, 2, 3, 4, 5, 6, 7, 8};
auto v1 = std::ranges::subrange(coll.begin(), coll.end());
print(v1); // 1 2 3 4 5 6 7 8
coll.insert(++coll.begin(), 0);
coll.push_back(9);
print(v2); // 1 0 2 3 4 5 6 7 8 9
2
3
4
5
6
7
# 子范围的范围工厂
没有从起始(迭代器)和结束(哨兵)初始化子范围的范围工厂。但是,有一个范围工厂可以从起始和计数创建子范围:
std::views::counted(beg, sz)
std::views::counted()
从非连续范围的起始迭代器beg
开始,创建包含前sz
个元素的子范围(对于连续范围,counted()
会创建一个span
视图)。
当std::views::counted()
创建子范围时,子范围的起始为std::counted_iterator
,结束为std::default_sentinel_t
类型的虚拟哨兵。这意味着:
std::views::counted(rg.begin(), 5);
等效于:
std::ranges::subrange{std::counted_iterator{rg.begin(), 5}, std::default_sentinel};
这使得即使在插入或删除元素时,子范围的计数也能保持稳定:
std::list coll{1, 2, 3, 4, 5, 6, 7, 8};
auto v2 = std::views::counted(coll.begin(), coll.size());
print(v2); // 1 2 3 4 5 6 7 8
coll.insert(++coll.begin(), 0);
coll.push_back(9);
print(v2); // 1 0 2 3 4 5 6 7
2
3
4
5
6
7
更多细节请参考std::views::counted()
和std::counted_iterator
类型的描述。
# 8.3.2 引用视图(ref_view)
类型: std::ranges::ref_view<> 内容: 一个范围的所有元素 适配器: std::views::all() 和所有其他作用于左值的适配器元素类型: 与传递的范围相同 要求: 至少是输入范围 类别: 与传递的范围相同 是否为sized范围: 如果作用于sized范围 是否为common范围: 如果作用于common范围 是否为borrowed范围: 始终是 缓存: 无 是否可const迭代: 如果作用于可const迭代的范围 是否传播const性: 从不 |
---|
类模板std::ranges::ref_view<>
定义了一个简单引用某个范围的视图。通过这种方式,按值传递该视图的效果类似于按引用传递原始范围。
这种效果类似于C++11引入的std::reference_wrapper<>
类型,它通过std::ref()
和std::cref()
将引用转换为一等对象。但ref_view
的优势在于它仍然可以直接作为范围使用。
ref_view
的主要用例是将容器转换为一个轻量级对象,该对象的复制成本很低。例如:
void foo(std::ranges::input_range auto coll) // 注意:按值接收范围
{
for (const auto& elem : coll) {
// ...
}
}
std::vector<std::string> coll{ ... };
2
3
4
5
6
7
8
foo(coll); | // 复制coll |
---|---|
foo(std::ranges::ref_view{coll}); | // 按引用传递coll |
将容器传递给通常需要按值接收参数的协程时,可能会用到这种技术。
注意,只能为左值(具有名称的范围)创建引用视图:
std::vector coll{0, 8, 15};
// ...
std::ranges::ref_view v1{coll}; // 正确,引用coll
std::ranges::ref_view v2{std::move(coll)}; // 错误
std::ranges::ref_view v3{std::vector{0, 8, 15}}; // 错误
2
3
4
5
对于右值,必须使用拥有型视图。以下是一个使用引用视图的完整示例:
// ranges/refview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void printByVal(std::ranges::input_range auto coll) // 注意:按值接收范围
{
for (const auto& elem : coll) {
std::cout << elem << ' ';
}
std::cout << '\n';
}
int main() {
std::vector<std::string> coll{ "love ", "of ", "my ", "life "};
printByVal(coll); // 复制coll
printByVal(std::ranges::ref_view{coll}); // 按引用传递coll
printByVal(std::views::all(coll)); // 同上(使用范围适配器)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 8.3.3 拥有视图(owning_view)
类型:std::ranges::owning_view<> 内容: 移动范围的所有元素 适配器: std::views::all() 和其他作用于右值的适配器元素类型: 与传递范围相同 要求: 至少输入范围 类别: 与传递范围相同 是否为可获取大小范围: 如果传递范围是 是否为公共范围: 如果传递范围是 是否为借用范围: 如果传递范围是 缓存: 无 是否为常量可迭代范围: 如果传递范围是 传播常量性: 总是 |
---|
类模板std::ranges::owning_view<>
定义了一个拥有另一个范围元素所有权的视图。这是目前唯一可能拥有多个元素的视图。不过,构造该视图的成本仍然很低,因为初始范围必须是一个右值(临时对象或用std::move()
标记的对象)。构造函数会将该范围移动到视图的内部成员中。
例如:
std::vector vec{0, 8, 15};
std::ranges::owning_view v1{std::move(vec)}; // 正确
print(v1); // 0 8 15
print(vec); // 未指定值(已被移动)
std::array<std::string, 3> arr{ "tic " , "tac " , "toe "};
std::ranges::owning_view v2{std::move(arr)}; // 正确
print(v2); // "tic" "tac" "toe"
print(arr); // "" "" ""
2
3
4
5
6
7
8
9
这是C++20中唯一完全不支持复制的标准视图。你只能移动它。例如:
std::vector coll{0, 8, 15};
std::ranges::owning_view v0{std::move(coll)};
auto v4 = std::move(v0); // 正确(v0中的范围移动到v4)
2
3
拥有视图的主要用例是创建一个不依赖原始范围生命周期的视图。例如:
void foo(std::ranges::view auto coll) // 注意:按值接受范围
{
for (const auto& elem : coll) {
...
}
}
std::vector<std::string> coll{ ... };
foo(std::ranges::owning_view{std::move(coll)}); // 正确:将coll作为视图移动
2
3
4
5
6
7
8
9
# 拥有视图的范围适配器
拥有视图也可以(通常应该)通过范围工厂创建:std::views::all(rg)
。如果传递的rg
是右值且不是视图,std::views::all()
会创建一个拥有视图(如果rg
已经是视图,则返回rg
;如果是左值,则返回引用视图)。
此外,几乎所有其他范围适配器在传递一个非视图的右值时,也会创建拥有视图。
例如,可以按如下方式调用foo()
:
foo(std::views::all(std::move(coll))); // 将coll作为视图移动
要创建拥有视图,适配器all()
需要右值(如果是左值,all()
会创建引用视图)。以下是一个完整示例:
// ranges/owningview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
auto getColl() {
return std::vector<std::string>{ "you " , "don't " , "fool " , "me "};
}
int main() {
std::ranges::owning_view v1 = getColl(); // 拥有字符串向量的视图
auto v2 = std::views::all(getColl()); // 同上
static_assert(std::same_as<decltype(v1), decltype(v2)>);
// 迭代拥有视图的drop视图:
for (const auto& elem : getColl() | std::views::drop(1)) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里,v1
和v2
的类型都是std::ranges::owning_view<std::vector<std::string>>>
。程序末尾的基于范围的for
循环演示了创建拥有视图的常用方式:将临时容器传递给范围适配器时会间接创建拥有视图。表达式getColl() | std::views::drop(1)
会创建一个std::ranges::drop_view<std::ranges::owning_view<std::vector<std::string>>>
。
# 拥有视图的特殊特性
拥有视图将传递的范围移动到自身内部。因此,如果它拥有的范围是借用范围,那么拥有视图也是借用范围。
# 拥有视图的接口
表“std::ranges::owning_view<>
类的操作”列出了拥有视图的API。只有当迭代器可默认初始化时,才提供默认构造函数。
操作 | 效果 |
---|---|
owning_view r{} owning_view r{rg} r.begin() r.end() r.empty() if (r) r.size() r.front() r.back() r[idx] r.data() r.base() | 创建空的拥有视图 创建拥有 rg 元素的拥有视图返回起始迭代器 返回哨兵(结束迭代器) 返回 r 是否为空(如果范围支持)如果 r 不为空则返回true (如果定义了empty() )返回元素数量(如果是可获取大小范围) 返回第一个元素(如果支持前向访问) 返回最后一个元素(如果支持双向且公共) 返回第 n 个元素(如果支持随机访问)返回元素内存的原始指针(如果元素在连续内存中) 返回 r 所拥有范围的引用 |
# 8.3.4 公共视图(common_view)
类型:std::ranges::common_view<> 内容: 具有统一迭代器类型的范围的所有元素 适配器: std::views::common() 元素类型: 与传递范围相同 要求: 非公共的至少前向范围(适配器接受公共范围) 类别: 通常为前向(如果是可获取大小的连续范围则为连续) 是否为可获取大小范围: 如果传递范围是 是否为公共范围: 总是 是否为借用范围: 如果传递范围是 缓存: 无 是否为常量可迭代范围: 如果传递范围是 传播常量性: 仅当传递范围是右值时 |
---|
类模板std::ranges::common_view<>
是一个将范围的起始和结束迭代器类型统一的视图,以便能够将它们传递给需要相同迭代器类型的代码(例如容器的构造函数或传统算法)。
注意,视图的构造函数要求传递的范围不是公共范围(迭代器类型不同)。例如:
std::list<int> lst{1, 2, 3, 4, 5, 6, 7, 8, 9};
auto take5 = std::ranges::take_view{lst, 5}; // 不生成公共视图
auto v2 = std::ranges::common_view{take5}; // 正确
2
3
# 公共视图的范围适配器
公共视图也可以(通常应该)通过范围适配器创建:std::views::common(rg)
。std::views::common()
会创建一个将rg
视为公共范围的视图。如果rg
已经是公共范围,则返回std::views::all(rg)
。
例如:
std::list<int> lst{1, 2, 3, 4, 5, 6, 7, 8, 9};
auto v1 = std::views::common(lst); // 正确
auto take5 = std::ranges::take_view{lst, 5}; // 不生成公共视图
auto v2 = std::views::common(take5); // v2是公共视图
std::vector<int> coll{v2.begin(), v2 .end()}; // 正确
2
3
4
5
需要注意的是,使用适配器从一个已经是通用的范围创建通用视图并不会导致编译错误。这正是推荐使用适配器而非直接初始化视图的关键原因。
以下是一个使用通用视图的完整示例程序:
//ranges/commonview.cpp
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::list<std::string> coll{ "You're ", "my ", "best ", "friend " };
auto tv = coll | std::views::take(3);
static_assert(! std::ranges::common_range<decltype(tv)>);
// 无法通过传递视图的begin和end初始化容器
std::ranges::common_view vCommon1{tv};
static_assert(std::ranges::common_range<decltype(vCommon1)>);
auto tvCommon = coll | std::views::take(3) | std::views::common;
static_assert(std::ranges::common_range<decltype(tvCommon)>);
std::vector<std::string> coll2{tvCommon.begin(), tvCommon.end()}; // 正常
print(coll);
print(coll2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在此示例中,我们首先创建了一个take
视图。如果该视图不是随机访问范围,它就不是通用的。由于它不通用,我们无法使用它的元素来初始化容器:
std::vector<std::string> coll2{tv.begin(), tv.end()}; // 错误:begin/end类型不同
使用通用视图后,初始化就可以正常工作。如果你不确定要统一的范围是否已经是通用的,请使用std::views::common()
。
通用视图的主要用例是将起始和结束迭代器类型不同的范围或视图传递给需要起始和结束迭代器类型相同的泛型代码。一个典型的例子是将范围的起始和结束迭代器传递给容器的构造函数或传统算法。例如:
std::list<int> lst {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto v1 = std::views::take(lst, 5); // 注意:begin()和end()的类型不同
std::vector<int> coll{v1.begin(), v1.end()}; // 错误:容器要求类型相同
auto v2 = std::views::common(std::views::take(lst, 5)); // 现在类型相同
std::vector<int> coll{v2.begin(), v2.end()}; // 正常
2
3
4
5
6
# 通用视图的接口
表8.7列出了std::ranges::common_view<>
类的API。
操作 | 效果 |
---|---|
common_view r{} common_view r{rg} | 创建一个引用默认构造范围的通用视图 创建一个引用范围 rg 的通用视图 |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是固定大小范围) 返回第一个元素(如果支持前向访问) |
r.back() r[idx] | 返回最后一个元素(如果是双向且通用) 返回第 n 个元素(如果是随机访问) |
r.data() r.base() | 返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用的范围的引用 |
默认构造函数仅在迭代器可默认初始化时提供。内部,common_view
使用std::common_iterator
类型的迭代器。
# 8.4 生成视图
本节讨论C++20中用于创建自身生成值(即不引用视图外部元素或值)的视图的所有特性。
# 8.4.1 Iota视图
类型: 内容: 工厂: 元素类型: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::iota_view<> 递增序列值的生成器 std::views::iota() 值 输入到随机访问(取决于起始值类型) 如果用结束值初始化则是 如果有限且结束值与值类型相同则是 始终是 无 始终是 从不(但元素不是左值) |
---|
类模板std::ranges::iota_view<>
是一个生成值序列的视图。这些值可以是整数类型,例如:
- 1, 2, 3 ...
- 'a', 'b', 'c' ...
也可以使用operator++
生成指针或迭代器序列。序列可以是有限的或无限的(无界的)。
iota视图的主要用例是提供一个遍历值序列的视图。例如:
std::ranges::iota_view v1{1, 100}; // 值序列:1, 2, ... 99
for (auto val : v1) {
std::cout << val << "\n"; // 打印这些值
}
2
3
4
# Iota视图的范围工厂
iota视图也可以(通常应该)通过范围工厂创建:
std::views::iota(val)
std::views::iota(val, endVal)
2
例如:
for (auto val : std::views::iota(1, 100)) { // 遍历值1, 2, ... 99
std::cout << val << "\n"; // 打印这些值
}
2
3
以下是使用iota视图的完整示例程序:
[`ranges/iotaview.cpp`]
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
int num = 0;
for (const auto& elem : coll) {
std::cout << elem << " ";
if (++num > 30) { // 仅打印最多30个值
std::cout << " ... ";
break;
}
}
std::cout << "\n";
}
int main() {
std::ranges::iota_view<int> iv0; // 0 1 2 3 ...
print(iv0);
std::ranges::iota_view iv1{-2}; // -2 -1 0 1 ...
print(iv1);
std::ranges::iota_view iv2{10, 20}; // 10 11 12 ... 19
print(iv2);
auto iv3 = std::views::iota(1);
print(iv3);
auto iv4 = std::views::iota('a', 'z' + 1);
print(iv4);
std::vector coll{0, 8, 15, 47, 11};
for (auto p : std::views::iota(coll.begin(), coll.end())) { // 迭代器序列
std::cout << *p << " "; // 0 8 15 47 11
}
std::cout << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
该程序的输出如下:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ...
-2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ...
10 11 12 13 14 15 16 17 18 19
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ...
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 8 15 47 11
2
3
4
5
6
注意,从'a'到'z'的输出取决于平台的字符集。
# Iota视图的特殊特性
由于迭代器存储当前值,iota视图是借用范围(其迭代器的生命周期不依赖于视图)。如果iota视图是有限的且结束值与起始值类型相同,则该视图是通用范围。
如果iota视图是无限的或结束值类型与起始值类型不同,则它不是通用范围。你可能需要使用通用视图来统一起始迭代器和哨兵(结束迭代器)的类型。
# Iota视图的接口
表8.8列出了std::ranges::iota_view<>
类的API。
操作 | 效果 |
---|---|
iota_view<Type> v iota_view v{begVal} | 创建一个以Type 默认值开始的无限序列创建一个以 begVal 开始的无限序列 |
iota_view v{begVal, endVal} iota_view v{beg, end} | 创建一个从begVal 到endVal 前一个值的序列辅助构造函数以支持子视图 |
v.begin() v.end() | 返回起始迭代器(指向起始值) 返回哨兵(结束迭代器),无限时为 std::unreachable_sentinel |
v.empty() if (v) | 返回v 是否为空如果 v 不为空则为true |
v.size() v.front() | 返回元素数量(如果有限且可计算) 返回第一个元素 |
v.back() v[idx] | 返回最后一个元素(如果有限且通用) 返回第 n 个元素 |
当按如下方式声明iota视图时:
std::ranges::iota_view v1{1, 100}; // 值序列:1, 2, ... 99
v1
的类型推导为std::ranges::iota_view<int, int>
,它创建一个提供基本API的对象,其begin()
和end()
返回指向这些值的迭代器。迭代器在内部存储当前值,并在递增时增加该值:
std::ranges::iota_view v1{1, 100};
auto pos = v1.begin();
std::cout << *pos; // 打印当前值(此处为1)
++pos;
std::cout << *pos; // 打印当前值(此处为2)
2
3
4
5
因此,值类型必须支持operator++
。出于这个原因,像bool
或std::string
这样的值类型不可用。
注意,该迭代器的类型由iota_view
的实现决定,这意味着在使用时必须使用auto
。或者,可以使用std::ranges::iterator_t<>
声明pos
:
std::ranges::iterator_t<decltype(v1)> pos = v1.begin();
对于迭代器范围,值范围是半开区间,结束值不包含在内。要包含结束值,可能需要递增结束值:
std::ranges::iota_view letters{'a', 'z' + 1}; // 值:'a', 'b', ... 'z'
for (auto c : letters) {
std::cout << c << " "; // 打印这些值/字母
}
2
3
4
注意,此循环的输出取决于字符集。只有当小写字母的值是连续的(如ASCII、ISO-Latin-1或UTF-8),视图才会仅遍历小写字母。通过使用char8_t
,可以在UTF-8字符中可移植地确保这一点:
std::ranges::iota_view letters{u8'a', u8'z' + 1}; // UTF-8中从a到z的值
如果不传递结束值,视图是无限的,生成无穷尽的值序列:
std::ranges::iota_view v2{10L}; // 无限范围:10L, 11L, 12L, ...
std::ranges::iota_view<int> v3; // 无限范围:0, 1, 2, ...
2
v3
的类型是std::ranges::iota_view<int, std::unreachable_sentinel_t>
,意味着end()
返回std::unreachable_sentinel
。
无限iota视图是无界的。当迭代器表示最大值并递增到下一个值时,它会调用operator++
,这在形式上是未定义行为(实际中通常会溢出,使下一个值为该类型的最小值)。如果视图的结束值永远不匹配,其行为相同。
# 使用Iota视图遍历指针和迭代器
你可以使用迭代器或指针初始化iota视图。此时,iota视图将第一个值作为起始,最后一个值作为结束,但元素是迭代器/指针而非值。
这可用于处理指向范围中所有元素的迭代器:
std::list<int> coll{2, 4, 6, 8, 10, 12, 14};
...
// 将每个元素的迭代器传递给foo():
for (const auto& itorElem : std::views::iota(coll.begin(), coll.end())) {
std::cout << *itorElem << "\n";
}
2
3
4
5
6
你还可以利用此特性用指向集合coll
所有元素的迭代器初始化容器:
std::ranges::iota_view itors{coll.begin(), coll.end()};
std::vector<std::ranges::iterator_t<decltype(coll)>> refColl{itors.begin(), itors.end()};
2
注意,如果省略refColl
元素类型的指定,必须使用圆括号。否则会用两个迭代器元素初始化向量:
std::vector refColl(itors.begin(), itors.end());
提供此构造函数的主要原因是支持能创建iota视图子视图的泛型代码,例如drop
视图:
// 从视图中删除第一个元素的泛型函数:
auto dropFirst = [] (auto v) {
return decltype(v){++v.begin(), v.end()};
};
std::ranges::iota_view v1{1, 9};
auto v2 = dropFirst(v1);
2
3
4
5
6
7
# 8.4.2 Single视图
类型: 内容: 工厂: 元素类型: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::single_view<> 单元素范围生成器 std::views::single() 引用 连续 始终为1 始终是 从不 无 始终是 始终是 |
---|
类模板std::ranges::single_view<>
是一个拥有单个元素的视图。除非值类型为const
,否则你甚至可以修改该值。
整体效果是,single视图的行为类似于一个不分配堆内存的轻量级单元素集合。
single视图的主要用例是调用泛型代码处理恰好包含一个元素/值的轻量级视图:
std::ranges::single_view<int> v1{42};
for (auto val : v1) {
...
}
2
3
4
# Single视图的范围工厂
single视图也可以(通常应该)通过范围工厂创建:
std::views::single(val)
例如:
for (auto val : std::views::single(42)) {
...
}
2
3
以下是使用single视图的完整示例程序:
[`ranges/singleview.cpp`]
#include <iostream>
#include <string>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::ranges::single_view<double> sv0;
std::ranges::single_view sv1{42};
auto sv2 = std::views::single('x');
auto sv3 = std::views::single("ok ");
std::ranges::single_view<std::string> sv4{"ok "};
print(sv0);
print(sv1);
print(sv2);
print(sv3);
print(sv4);
print(std::ranges::single_view{42});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
该程序的输出如下:
0
42
x
ok
ok
42
2
3
4
5
6
# Single视图的特殊特性
single视图符合以下概念:
std::ranges::contiguous_range
std::ranges::sized_range
视图始终是通用范围,且永远不是借用范围(其迭代器的生命周期依赖于视图)。
# Single视图的接口
表8.9列出了std::ranges::single_view<>
类的API。
操作 | 效果 |
---|---|
single_view<Type> v single_view v{val} | 创建一个元素为Type 默认初始化的视图创建一个元素为 val 的视图 |
single_view<Type> v{std::in_place, arg1, arg2, ...} | 创建一个用arg1, arg2, ... 初始化元素的视图 |
v.begin() v.end() | 返回指向元素的原始指针 返回指向元素之后位置的原始指针 |
v.empty() if (v) | 始终为false 始终为 true |
v.size() v.front() | 返回1 返回当前值的引用 |
v.back() v[idx] | 返回当前值的引用 返回索引0处的当前值 |
r.data() | 返回元素的原始指针 |
如果调用构造函数时未传递初始值,single视图中的元素会被值初始化。这意味着它会使用Type
的默认构造函数,或以0、false
或nullptr
初始化值。
要初始化需要多个初始化参数的对象的single视图,有两种方式:
- 传递已初始化的对象:
std::ranges::single_view sv1{std::complex{1,1}};
auto sv2 = std::views::single(std::complex{1,1});
2
- 在
std::in_place
参数后传递初始值:
std::ranges::single_view<std::complex<int>> sv4{std::in_place, 1, 1};
注意,对于非const
的single视图,可以修改“元素”的值:
std::ranges::single_view v2{42};
std::cout << v2.front() << "\n"; // 输出42
++v2.front(); // 正常,修改视图的值
std::cout << v2.front() << "\n"; // 输出43
2
3
4
若要防止修改,可将元素类型声明为const
:
std::ranges::single_view<const int> v3{42};
++v3.front(); // 错误
2
接受值的构造函数会将该值复制或移动到视图中。这意味着视图不能引用初始值(声明元素类型为引用无法编译)。
由于begin()
和end()
返回相同的值,视图始终是通用范围。由于迭代器引用视图中存储的值以便修改,single视图不是借用范围(其迭代器的生命周期依赖于视图)。
# 8.4.3 Empty视图
类型: 内容: 工厂: 元素类型: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::empty_view<> 无元素范围生成器 std::views::empty<> 引用 连续 始终为0 始终是 始终是 无 始终是 从不 |
---|
类模板std::ranges::empty_view<>
是一个没有元素的视图。但必须指定元素类型。
empty视图的主要用例是调用泛型代码处理没有元素的轻量级视图,且类型系统明确知道它永远不会有元素:
std::ranges::empty_view<int> v1;
for (auto val : v1) {
... // 永远不会执行
}
2
3
4
# Empty视图的范围工厂
empty视图也可以(通常应该)通过范围工厂创建,这是一个变量模板,意味着需要为类型声明模板参数但无需调用参数:
std::views::empty<type>
例如:
for (auto val : std::views::empty<int>) {
... // 永远不会执行
}
2
3
以下是使用empty视图的完整示例程序:
[`ranges/emptyview.cpp`]
#include <iostream>
#include <string>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
if (coll.empty()) {
std::cout << "<empty>\n";
}
else {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
}
int main() {
std::ranges::empty_view<double> ev0;
auto ev1 = std::views::empty<int>;
print(ev0);
print(ev1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
该程序的输出如下:
<empty>
<empty>
2
# Empty视图的特殊特性
empty视图的行为类似于空向量。实际上,它符合以下概念:
std::ranges::contiguous_range
(隐含std::ranges::random_access_range
)std::ranges::sized_range
begin()
和end()
始终返回nullptr
,意味着该范围也是通用范围和借用范围(其迭代器的生命周期不依赖于视图)。
# 空视图的接口
表“std::ranges::empty_view<>
类的操作”列出了空视图的API。你可能想知道为什么空视图会提供front()
、back()
和operator[]
,而这些操作总是具有未定义行为,调用它们总是会导致致命的运行时错误。但是请记住,泛型代码在调用front()
、back()
或operator[]
之前,总是必须检查(或知道)存在元素,这意味着这样的泛型代码永远不会调用这些成员函数。即使对于空视图,这样的代码也能编译。
操作 | 效果 |
---|---|
empty_view<Type> v v.begin() v.end() v.empty() if (v) v.size() v.front() v.back() v[idx] r.data() | 创建一个没有Type 类型元素的视图 返回一个用 nullptr 初始化的指向元素类型的原始指针 返回一个用 nullptr 初始化的指向元素类型的原始指针 返回 true 始终为 false 返回 0 总是具有未定义行为(致命的运行时错误) 总是具有未定义行为(致命的运行时错误) 总是具有未定义行为(致命的运行时错误) 返回一个用 nullptr 初始化的指向元素类型的原始指针 |
表8.10 std::ranges::empty_view<>
类的操作
例如,你可以将空视图传递给如下代码,它将能够编译:
void foo(std::ranges::random_access_range auto&& rg) {
std::cout << "sortFirstLast(): \n " ;
std::ranges::sort(rg);
if (! std::ranges::empty(rg)) {
std::cout << " first : " << rg.front() << "\n";
std::cout << " last : " << rg.back() << "\n";
}
}
foo(std::ranges::empty_view<int>{}); // 没问题
2
3
4
5
6
7
8
9
# 8.4.4 输入流视图(IStream View)
类型:std::ranges::basic_istream_view<> std::ranges::istream_view<> std::ranges::wistream_view<> 内容: 从流中读取元素生成的范围 工厂函数: std::views::istream<>() 元素类型: 引用 类别: 输入 是否为可获取大小范围: 从不 是否为公共范围: 从不 是否为借用范围: 从不 缓存: 无 是否为常量可迭代范围: 从不 传播常量性: — |
---|
类模板std::ranges::basic_istream_view<>
是一个从输入流(如标准输入、文件或字符串流)中读取元素的视图。
与流类型通常的情况一样,该类型针对字符类型是泛型的,并为char
和wchar_t
提供了特化版本:
- 类模板
std::ranges::istream_view<>
是一个使用char
类型字符从输入流中读取元素的视图。 - 类模板
std::ranges::wistream_view<>
是一个使用wchar_t
类型字符从输入流中读取元素的视图。
例如:
std::istringstream myStrm{ "0 1 2 3 4 "};
for (const auto& elem : std::ranges::istream_view<int>{myStrm}) {
std::cout << elem << "\n";
}
2
3
4
# 输入流视图的范围工厂函数
输入流视图也可以(通常应该)通过范围工厂函数创建。工厂函数会根据传递范围的字符类型,将其参数传递给std::ranges::basic_istream_view
构造函数:std::views::istream<Type>(rg)
。
例如:
std::istringstream myStrm{ "0 1 2 3 4 "};
for (const auto& elem : std::views::istream<int>(myStrm)) {
std::cout << elem << "\n";
}
2
3
4
或者:
std::wistringstream mywstream{L "1.1 2.2 3.3 4.4 "};
auto vw = std::views::istream<double>(mywstream);
2
vw
的初始化等同于以下两种初始化方式:
auto vw2 = std::ranges::basic_istream_view<double , wchar_t>{myStrm};
auto vw3 = std::ranges::wistream_view<double>{myStrm};
2
下面是一个使用输入流视图的完整示例程序:
// ranges/istreamview.cpp
#include <iostream>
#include <string>
#include <sstream>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::string s{ "2 4 6 8 Motorway 1977 by Tom Robinson "};
std::istringstream mystream1{s};
std::ranges::istream_view<std::string> vs{mystream1};
print(vs);
std::istringstream mystream2{s};
auto vi = std::views::istream<int>(mystream2);
print(vi);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
程序输出如下:
2 4 6 8 Motorway 1977 by Tom Robinson
2 4 6 8
2
程序对用字符串s
初始化的字符串流进行了两次迭代:
- 输入流视图
vs
迭代从输入字符串流mystream1
中读取的所有字符串,它打印出s
的所有子字符串。 - 输入流视图
vi
迭代从输入字符串流mystream2
中读取的所有int
类型值,当读到Motorway
时停止读取。
# 输入流视图与常量(const)
注意,你不能对常量输入流视图进行迭代。例如:
void printElems(const auto& coll) {
for (const auto elem& e : coll) {
std::cout << elem << "\n";
}
}
std::istringstream myStrm{ "0 1 2 3 4 "};
printElems(std::views::istream<int>(myStrm)); // 错误
2
3
4
5
6
7
8
问题在于,begin()
仅为非常量输入流视图提供,因为值的处理分两步(begin()/++
,然后是operator*
),并且在此期间值存储在视图中。
通过以下示例可以说明修改视图的原因,该示例使用底层方式迭代视图的“元素”来从输入流视图中读取字符串:
std::istringstream myStrm{ "stream with very-very-very-long words "};
auto v = std::views::istream<std::string>(myStrm);
for (auto pos = v.begin(); pos != v.end(); ++pos) {
std::cout << *pos << "\n";
}
2
3
4
5
这段代码的输出如下:
stream with
very-very-very-long words
2
当迭代器pos
遍历视图va
时,它使用begin()
和++
从输入流myStrm
中读取字符串。这些字符串必须存储在内部,以便可以通过operator *
使用。如果我们将字符串存储在迭代器中,迭代器可能需要为字符串分配内存,并且复制迭代器时必须复制该内存。然而,复制迭代器不应开销过大。因此,视图将字符串存储在视图中,这导致在迭代时视图被修改。
因此,在泛型代码中,你必须使用万能/转发引用来支持这个视图:
void printElems(auto&& coll) {
...
}
2
3
# 输入流视图的接口
表“std::ranges::basic_istream_view<>
类的操作”列出了输入流视图的API。
操作 | 效果 |
---|---|
istream_view<Type> v{strm} v.begin() v.end() | 创建一个从strm 读取Type 类型值的输入流视图 读取第一个值并返回起始迭代器 返回 std::default_sentinel 作为结束迭代器 |
表8.11 std::ranges::basic_istream_view<>
类的操作
你所能做的就是使用迭代器逐个读取值,直到到达流的末尾。它没有提供其他成员函数(如empty()
或front()
)。
注意,C++20标准中输入流视图的原始规范与其他视图存在一些不一致之处,后来进行了修正(见http://wg21.link/p2432 )。通过这次修正,才有了当前的行为:
- 你可以完全指定所有模板参数:
std::ranges::basic_istream_view<int , char, std::char_traits<char>> v1{myStrm};
- 最后一个模板参数有一个可用的默认值,这意味着你可以使用:
std::ranges::basic_istream_view<int , char> v2{myStrm}; // 没问题
- 但是,你不能跳过更多参数:
std::ranges::basic_istream_view<int> v3{myStrm}; // 错误
- 不过,输入流视图遵循通常的约定,对于
char
和wchar_t
类型的流,有不带basic_
前缀的特殊类型:
std::ranges::istream_view<int> v4{myStrm};// 对于char类型的流没问题
std::wistringstream mywstream{L "0 1 2 3 4 "};
std::ranges::wistream_view<int> v5{mywstream};// 对于wchar_t类型的流没问题
2
3
- 并且提供了相应的范围工厂函数:
auto v6 = std::views::istream<int>(myStrm); // 没问题
# 8.4.5 字符串视图(String View)
类型: std::basic_string_view<> std::string_view std::u8string_view std::u16string_view std::u32string_view std::wstring_view 内容: 字符序列的所有字符 工厂函数: – 元素类型: const 引用类别: 连续 是否为sized范围: 始终是 是否为common范围: 始终是 是否为borrowed范围: 始终是 缓存: 无 是否可const迭代: 始终是 是否传播const性: 元素始终为 const |
---|
类模板std::basic_string_view<>
及其特化版本std::string_view
、std::u16string_view
、std::u32string_view
和std::wstring_view
是C++17标准库中已有的视图类型。但C++20新增了针对UTF-8字符的特化版本std::u8string_view
。
字符串视图不遵循视图的某些常规约定:
- 视图定义在命名空间
std
(而非std::ranges
)中。 - 视图有自己的头文件
<string_view>
。 - 视图没有创建视图对象的范围适配器/工厂函数。
- 视图仅提供对元素/字符的只读访问。
- 视图提供
cbegin()
和cend()
成员函数。 - 视图不支持转换为
bool
。例如:
for (char c : std::string_view{ "hello" }) {
std::cout << c << ' ';
}
std::cout << '\n';
2
3
4
该循环的输出为:h e l l o
。
# 字符串视图的特殊特性
迭代器不直接引用视图,而是引用底层字符序列。因此,字符串视图是借用范围。但需注意,当底层字符序列不再存在时,迭代器仍可能悬空。
# 字符串视图的视图特定接口
下表列出了字符串视图API中与视图相关的部分:
操作 | 效果 |
---|---|
string_view sv string_view sv{s} v.begin() v.end() sv.empty() sv.size() sv.front() sv.back() sv[idx] sv.data() ... | 创建空字符串视图 创建指向 s 的字符串视图返回起始迭代器 返回哨兵(结束迭代器) 判断 sv 是否为空返回字符数量 返回第一个字符 返回最后一个字符 返回第 n 个字符返回字符的原始指针或 nullptr 其他针对只读字符串的操作 |
表8.12. std::ranges::basic_string_view<>
类的视图操作
除了视图的常规接口,该类型还提供了字符串所有只读操作的完整API。更多细节可参考我的《C++17 - The Complete Guide》(http://www.cppstd17.com)。
# 8.4.6 跨度(Span)
类型: std::span<> 内容: 连续内存中范围的所有元素 工厂函数: std::views::counted() 元素类型: 引用 要求: 连续范围 类别: 连续 是否为sized范围: 始终是 是否为common范围: 始终是 是否为borrowed范围: 始终是 缓存: 无 是否可const迭代: 始终是 是否传播const性: 从不 |
---|
类模板std::span<>
是一个指向连续内存中元素序列的视图。其详细内容在本书专门章节讨论。此处仅介绍其作为视图的特性。
跨度的主要优势在于支持引用中间或末尾的n
个元素子范围。例如:
std::vector<std::string> vec{ "New York", "Tokyo", "Rio", "Berlin", "Sydney" };
// 对中间三个元素排序:
std::ranges::sort(std::span{vec}.subspan(1, 3));
// 打印最后三个元素:
print(std::span{vec}.last(3));
2
3
4
5
此示例将在后续关于跨度操作的章节中讨论。
# 跨度的特殊特性
迭代器不直接引用跨度,而是引用底层元素。因此,跨度是借用范围。但需注意,当底层字符序列不再存在时,迭代器仍可能悬空。
# 跨度的视图特定接口
下表列出了跨度API中与视图相关的部分:
操作 | 效果 |
---|---|
span sp span sp{s} sp.begin() sp.end() sp.empty() sp.size() sp.front() sp.back() sp[idx] sp.data() ... | 创建空跨度 创建指向 s 的跨度返回起始迭代器 返回哨兵(结束迭代器) 判断 sp 是否为空返回元素数量 返回第一个元素 返回最后一个元素 返回第 n 个元素返回元素的原始指针或 nullptr 其他跨度操作详见后续章节 |
表8.13. std::span<>
类的视图操作
更多细节请参考跨度操作章节。
# 8.5 过滤视图
本节讨论所有过滤给定范围或视图元素的视图。
# 8.5.1 取前视图
类型:std::ranges::take_view<> 内容:范围的前 num 个元素(最多)适配器: std::views::take() 元素类型:与传递的范围相同 要求:至少输入范围 类别:与传递的相同 是否为有大小范围:如果基于有大小范围 是否为公共范围:如果基于有大小的随机访问范围 是否为借用范围:如果基于借用视图或左值非视图 缓存:无 是否可 const 迭代:如果基于可const 迭代的范围常量性传播:仅当基于右值范围时 |
---|
类模板std::ranges::take_view<>
定义了一个视图,指向传递范围的前num
个元素。如果传递的范围元素不足,则视图指向所有元素。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& elem : std::ranges::take_view{rg, 5}) {
std::cout << elem << " ";
}
2
3
4
5
循环将输出:
1 2 3 4 5
# 取前视图的范围适配器
取前视图也可以(通常应该)通过范围适配器创建:
std::views::take(rg, n)
例如:
for (const auto& elem : std::views::take(rg, 5)) {
std::cout << elem << " ";
}
2
3
或者:
for (const auto& elem : rg | std::views::take(5)) {
std::cout << elem << " ";
}
2
3
注意,适配器并不总是生成take_view
:
- 如果传递的是空视图(
empty_view
),则直接返回该视图。 - 如果传递的是有大小的随机访问范围,可以直接用
begin() + num
初始化同类型范围,则返回该范围(适用于子范围、iota视图、字符串视图和span)。例如:
std::vector<int> vec;
// 使用构造函数:
std::ranges::take_view tv1{vec, 5}; // vector引用视图的取前视图
std::ranges::take_view tv2{std::vector<int>{}, 5}; // vector拥有视图的取前视图
std::ranges::take_view tv3{std::views::iota(1,9), 5}; // iota视图的取前视图
// 使用适配器:
auto tv4 = std::views::take(vec, 5); // vector引用视图的取前视图
auto tv5 = std::views::take(std::vector<int>{}, 5); // vector拥有视图的取前视图
auto tv6 = std::views::take(std::views::iota(1,9), 5); // 纯iota视图
2
3
4
5
6
7
8
9
取前视图内部存储传递的范围(可能通过all()
转换为视图)。因此,只有当传递的范围有效时,取前视图才有效(除非传递的是右值,此时内部使用拥有视图)。
如果底层范围是有大小的随机访问范围,迭代器是其迭代器或计数迭代器加默认哨兵。因此,只有传递有大小的随机访问范围时,该范围才是公共的。
以下是使用取前视图的完整示例程序:
// ranges/takeview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4, 1};
print(coll); // 1 2 3 4 1 2 3 4 1
print(std::ranges::take_view{coll, 5}); // 1 2 3 4 1
print(coll | std::views::take(5)); // 1 2 3 4 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
程序输出如下:
1 2 3 4 1 2 3 4 1
1 2 3 4 1
1 2 3 4 1
2
3
# 取前视图的特殊特性
注意,只有当底层范围是有大小且是公共范围时,取前视图才是公共的(迭代器和哨兵类型相同)。要统一类型,可能需要使用公共视图。
# 取前视图的接口
表8.14“std::ranges::take_view<>
类的操作”列出了取前视图的API。
操作 | 效果 |
---|---|
take_view r{} take_view r{rg, num} r.begin() r.end() r.empty() if (r) r.size() r.front() r.back() r[idx] r.data() r.base() | 创建指向默认构造范围的取前视图 创建指向范围 rg 前num 个元素的取前视图返回起始迭代器 返回哨兵(结束迭代器) 返回 r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() )返回元素个数(如果指向有大小范围) 返回第一个元素(如果支持前向) 返回最后一个元素(如果支持双向且公共) 返回第 n 个元素(如果支持随机访问)返回元素内存的原始指针(如果元素在连续内存中) 返回对 r 指向或拥有的范围的引用 |
表8.14 std::ranges::take_view<>
类的操作
# 8.5.2 取直到视图
类型:std::ranges::take_while_view<> 内容:范围中所有匹配谓词的前置元素 适配器: std::views::take_while() 元素类型:与传递的范围相同 要求:至少输入范围 类别:与传递的相同 是否为有大小范围:从不 是否为公共范围:从不 是否为借用范围:从不 缓存:无 是否可 const 迭代:如果基于可const 迭代的范围且谓词可处理const 值常量性传播:仅当基于右值范围时 |
---|
类模板std::ranges::take_while_view<>
定义了一个视图,指向传递范围中所有匹配特定谓词的前置元素。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& elem : std::ranges::take_while_view{rg, [](auto x) {
return x % 3 != 0;
}}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
循环将输出:
1 2
# 取直到视图的范围适配器
取直到视图也可以(通常应该)通过范围适配器创建。适配器将参数直接传递给std::ranges::take_while_view
构造函数:
std::views::take_while(rg, pred)
例如:
for (const auto& elem : std::views::take_while(rg, [](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
或者:
for (const auto& elem : rg | std::views::take_while([](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
传递的谓词必须是满足std::predicate
概念的可调用对象。这隐含了std::regular_invocable
概念,意味着谓词不应修改底层范围的传递值。不过,不修改值是语义约束,无法在编译时总是检查。因此,谓词至少应声明为按值或按const
引用接收参数。
取直到视图内部存储传递的范围(可能通过all()
转换为视图)。因此,只有当传递的范围有效时,取直到视图才有效(除非传递的是右值,此时内部使用拥有视图)。
迭代器只是传递范围的迭代器和一个特殊的内部哨兵类型。以下是使用取直到视图的完整示例程序:
// ranges/takewhileview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4, 1};
print(coll); // 1 2 3 4 1 2 3 4 1
auto less4 = [](auto v) { return v < 4; };
print(std::ranges::take_while_view{coll, less4}); // 1 2 3
print(coll | std::views::take_while(less4)); // 1 2 3
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
程序输出如下:
1 2 3 4 1 2 3 4 1
1 2 3
1 2 3
2
3
# 取直到视图的接口
表8.15“std::ranges::take_while_view<>
类的操作”列出了取直到视图的API。
操作 | 效果 |
---|---|
take_while_view r{} take_while_view r{rg, pred} r.begin() r.end() r.empty() if (r) r.size() r.front() r[idx] r.data() r.base() r.pred() | 创建指向默认构造范围的取直到视图 创建指向范围 rg 中满足pred 的前置元素的取直到视图返回起始迭代器 返回哨兵(结束迭代器) 返回 r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() )返回元素个数(如果指向有大小范围) 返回第一个元素(如果支持前向) 返回第 n 个元素(如果支持随机访问)返回元素内存的原始指针(如果元素在连续内存中) 返回对 r 指向或拥有的范围的引用返回对谓词的引用 |
表8.15 std::ranges::take_while_view<>
类的操作
# 8.5.3 丢弃视图
|类型:|std::ranges::drop_view<>
|
|---|
|内容:|范围中除前num
个元素外的所有元素|
|适配器:|std::views::drop()
|
|元素类型:|与传递的范围相同|
|要求:|至少输入范围|
|类别:|与传递的范围相同|
|是否为大小范围:|若是大小范围则是|
|是否为公共范围:|若是公共范围则是|
|是否为借用范围:|若是借用视图或左值非视图则是|
|缓存:|若非随机访问范围或非大小范围则缓存begin()
|
|是否支持const
迭代:|若是支持随机访问且为大小范围的const
可迭代范围则是|
|是否传播const
性:|仅当传递的是右值范围时|
类模板std::ranges::drop_view<>
定义了一个视图,它引用传递范围中除前num
个元素外的所有元素。其效果与取前视图相反。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13};
...
for (const auto& elem : std::ranges::drop_view{rg, 5}) {
std::cout << elem << " ";
}
2
3
4
5
循环将输出:
6 7 8 9 10 11 12 13
# 丢弃视图的范围适配器
丢弃视图也可以(且通常应该)通过范围适配器创建:
std::views::drop(rg, n)
例如:
for (const auto& elem : std::views::drop(rg, 5)) {
std::cout << elem << " ";
}
2
3
或:
for (const auto& elem : rg | std::views::drop(5)) {
std::cout << elem << " ";
}
2
3
注意,适配器并不总是返回drop_view
:
- 如果传递的是空视图,直接返回该视图。
- 如果传递的是大小随机访问范围(如
subrange
、iota view
、string view
和span
),适配器可能直接创建一个新范围(通过begin() + num
初始化)。
# 丢弃视图与缓存
为提高性能(实现均摊常数复杂度),丢弃视图会在视图中缓存begin()
的结果(除非范围仅是输入范围)。这意味着首次遍历丢弃视图的元素比后续遍历更耗时。因此,建议初始化一次丢弃视图并多次使用:
// 更好的方式:
auto v1 = coll | std::views::drop(5);
check(v1);
process(v1);
// 较差的方式:
check(coll | std::views::drop(5));
process(coll | std::views::drop(5));
2
3
4
5
6
7
8
# 丢弃视图与const
并非所有情况下都能遍历const
丢弃视图。只有当引用的范围是随机访问范围且为大小范围时才支持:
void printElems(const auto& coll) {
for (const auto& e : coll) {
std::cout << e << "\n";
}
}
std::vector vec{1, 2, 3, 4, 5};
std::list lst{1, 2, 3, 4, 5};
printElems(vec | std::views::drop(3)); // 可行
printElems(lst | std::views::drop(3)); // 错误
2
3
4
5
6
7
8
9
10
11
若要在泛型代码中支持此视图,需使用万能引用:
void printElems(auto&& coll) {
...
}
std::list lst{1, 2, 3, 4, 5};
printElems(lst | std::views::drop(3)); // 可行
2
3
4
5
# 丢弃视图的接口
“std::ranges::drop_view<>
类的操作”表列出了丢弃视图的API。
操作 | 效果 |
---|---|
drop_view r{} drop_view r{rg, num} | 创建引用默认构造范围的丢弃视图 创建引用 rg 中除前num 个元素外的所有元素的丢弃视图 |
r.begin() r.end() | 返回起始迭代器 返回哨兵(尾后迭代器) |
r.empty() if (r) | 返回r 是否为空(若范围支持)r 非空时为true (若empty() 定义) |
r.size() r.front() | 返回元素数量(若引用大小范围) 返回第一个元素(若支持前向迭代) |
r.back() r[idx] | 返回最后一个元素(若支持双向且公共) 返回第 n 个元素(若支持随机访问) |
r.data() r.base() | 返回元素内存的原始指针(若元素连续) 返回 r 引用或拥有的范围的引用 |
表8.16 std::ranges::drop_view<>
类的操作
# 8.5.4 丢弃-while视图
类型 | std::ranges::drop_while_view<> |
---|---|
内容 | 范围中除了满足某个谓词的起始元素之外的所有元素 |
适配器 | std::views::drop_while() |
元素类型 | 与传递的范围相同 |
要求 | 至少为输入范围 |
类别 | 与传递的范围相同 |
是否为大小范围 | 如果传递的是公共随机访问范围,则是 |
是否为公共范围 | 如果在公共范围上,则是 |
是否为借用范围 | 如果在借用视图或左值非视图上,则是 |
缓存 | 始终缓存begin() |
是否支持const 迭代 | 从不支持 |
是否传播const 性 | —(如果是const ,则无法调用begin() ) |
类模板std::ranges::drop_while_view<>
定义了一个视图,它会跳过传递范围中所有满足特定谓词的起始元素。它产生的元素与取-while视图相反。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& elem : std::ranges::drop_while_view{rg, [](auto x) {
return x % 3 != 0;
}}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
该循环将输出:
3 4 5 6 7 8 9 10 11 12 13
# 丢弃-while视图的范围适配器
丢弃-while视图也可以(并且通常应该)使用范围适配器创建。适配器只是将其参数传递给std::ranges::drop_while_view
构造函数:
std::views::drop_while(rg, pred)
例如:
for (const auto& elem : std::views::drop_while(rg, [](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
或者:
for (const auto& elem : rg | std::views::drop_while([](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
传递的谓词必须是一个满足std::predicate
概念的可调用对象。这意味着它满足std::regular_invocable
概念,即谓词不应修改底层范围传递的值。然而,不修改值是一个语义约束,并非总能在编译时检查。因此,谓词至少应声明按值或按常量引用接受参数。
丢弃-while视图在内部存储传递的范围(可能会像all()
那样转换为视图)。因此,只有在传递的范围有效时,它才有效(除非传递的是右值,这意味着在内部使用owning view
)。
begin
迭代器在首次调用begin()
时初始化并缓存,这总是需要线性时间。因此,重用丢弃-while视图比重新创建它更好。
下面是一个使用丢弃-while视图的完整示例程序:
// ranges/dropwhileview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4, 1};
print(coll);
auto less4 = [](auto v) { return v < 4; };
print(std::ranges::drop_while_view{coll, less4});
print(coll | std::views::drop_while(less4));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
该程序的输出如下:
// 1 2 3 4 1 2 3 4 1
// 4 1 2 3 4 1
// 4 1 2 3 4 1
2
3
# 丢弃-while视图和缓存
为了获得更好的性能(实现均摊常数复杂度),丢弃-while视图会在视图中缓存begin()
的结果(除非范围仅是输入范围)。这意味着对丢弃-while视图的元素进行首次迭代比后续迭代更耗时。
因此,最好初始化一次过滤视图并使用两次:
// 更好的方式:
auto v1 = coll | std::views::drop_while(myPredicate);
check(v1);
process(v1);
2
3
4
而不是初始化并使用两次:
// 较差的方式:
check(coll | std::views::drop_while(myPredicate));
process(coll | std::views::drop_while(myPredicate));
2
3
当过滤视图引用的范围被修改时,缓存可能会产生功能上的影响。例如:
// ranges/dropwhilecache.cpp
#include <iostream>
#include <vector>
#include <list>
#include <ranges>
void print(auto&& coll) {
for (const auto& elem : coll)
std::cout << elem << " ";
std::cout << "\n";
}
int main() {
std::vector vec{1, 2, 3, 4};
std::list lst{1, 2, 3, 4};
auto lessThan2 = [](auto v){
return v < 2;
};
auto vVec = vec | std::views::drop_while(lessThan2);
auto vLst = lst | std::views::drop_while(lessThan2);
// 在前面插入一个新元素(=> 0 1 2 3 4)
vec.insert(vec.begin(), 0);
lst.insert(lst.begin(), 0);
print(vVec);
print(vLst);
// 没问题:2 3 4
// 没问题:2 3 4
// 在前面插入更多元素(=> 0 98 99 -1 0 1 2 3 4)
vec.insert(vec.begin(), {0, 98, 99, -1});
lst.insert(lst.begin(), {0, 98, 99, -1});
print(vVec);
print(vLst);
// 糟糕:99 -1 0 1 2 3 4
// 糟糕:2 3 4
// 创建副本可以解决(随机访问范围除外):
auto vVec2 = vVec;
auto vLst2 = vLst;
// 糟糕:99 -1 0 1 2 3 4
// 没问题:98 99 -1 0 1 2 3 4
print(vVec2);
print(vLst2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
正式来说,将无效视图复制到向量会产生未定义行为,因为C++标准没有规定缓存的实现方式。由于向量重新分配会使所有迭代器失效,缓存的迭代器也会变为无效。然而,对于随机访问范围,视图通常缓存的是偏移量,而不是迭代器。这意味着即使begin
不再符合谓词,视图仍然有效,因为它仍然包含一个有效的范围。
作为一个经验法则,在底层范围被修改后,不要使用已经调用过begin()
的丢弃-while视图。
# 丢弃-while视图和const
请注意,你不能遍历const
丢弃-while视图。例如:
void printElems(const auto& coll) {
for (const auto& e : coll) {
std::cout << elem << "\n";
}
}
std::vector vec{1, 2, 3, 4, 5};
printElems(vec | std::views::drop_while(...)); // 错误
2
3
4
5
6
7
8
问题在于,begin()
仅为非const
丢弃-while视图提供,因为缓存迭代器会修改视图。
为了在泛型代码中支持这个视图,你必须使用万能引用/转发引用:
void printElems(auto&& coll) {
...
}
std::list lst{1, 2, 3, 4, 5};
printElems(vec | std::views::drop_while(...)); // 没问题
2
3
4
5
6
# 丢弃-while视图的接口
“std::ranges::drop_while_view<>
类的操作”表列出了丢弃-while视图的API。
操作 | 效果 |
---|---|
drop_while_view r{} drop_while_view r{rg, pred} | 创建一个引用默认构造范围的丢弃-while视图 创建一个引用 rg 中除了使pred 为true 的起始元素之外的所有元素的丢弃-while视图 |
r.begin() r.end() | 返回起始迭代器 返回哨兵(尾后迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)r 不为空时返回true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是大小范围) 返回第一个元素(如果支持前向迭代) |
r.back() r[idx] | 返回最后一个元素(如果支持双向且公共) 返回第 n 个元素(如果支持随机访问) |
r.data() r.base() r.pred() | 返回指向元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用返回对谓词的引用 |
表8.17 std::ranges::drop_while_view<>
类的操作
# 8.5.5 过滤视图
类型 | std::ranges::filter_view<> |
---|---|
内容 | 范围中所有满足某个谓词的元素 |
适配器 | std::views::filter() |
元素类型 | 与传递的范围相同 |
要求 | 至少为输入范围 |
类别 | 与传递的范围相同,但最多为双向范围 |
是否为大小范围 | 从不 |
是否为公共范围 | 如果在公共范围上,则是 |
是否为借用范围 | 从不 |
缓存 | 始终缓存begin() |
是否支持const 迭代 | 从不 |
是否传播const 性 | —(如果是const ,则无法调用begin() ) |
类模板std::ranges::filter_view<>
定义了一个视图,它仅遍历底层范围中满足特定谓词的元素,即过滤掉所有不满足谓词的元素。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& elem : std::ranges::filter_view{rg, [](auto x) {
return x % 3 != 0;
}}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
该循环将输出:
1 2 4 5 7 8 10 11 13
# 过滤视图的范围适配器
过滤视图也可以(并且通常应该)使用范围适配器创建。适配器只是将其参数传递给std::ranges::filter_view
构造函数:
std::views::filter(rg, pred)
例如:
for (const auto& elem : std::views::filter(rg, [](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
或者:
for (const auto& elem : rg | std::views::filter([](auto x) {
return x % 3 != 0;
})) {
std::cout << elem << " ";
}
2
3
4
5
传递的谓词必须是一个满足std::predicate
概念的可调用对象。这意味着它满足std::regular_invocable
概念,即谓词不应修改底层范围传递的值。然而,不修改值是一个语义约束,并非总能在编译时检查。因此,谓词至少应声明按值或按常量引用接受参数。
过滤视图比较特殊,你应该了解何时、何地以及如何使用它,以及使用它会产生的副作用。实际上,它对管道性能有显著影响,并且有时会以出人意料的方式限制对元素的写访问。
因此,使用过滤视图时要谨慎:
- 在管道中,应尽早使用它。
- 对在过滤器之前的昂贵转换要小心。
- 当写访问会破坏谓词时,不要使用它来对元素进行写访问。
过滤视图在内部存储传递的范围(可能会像all()
那样转换为视图)。因此,只有在传递的范围有效时,它才有效(除非传递的是右值,这意味着在内部使用owning view
)。
begin
迭代器在首次调用begin()
时初始化并通常会缓存,这总是需要线性时间。因此,重用过滤视图比重新创建它更好。
下面是一个使用过滤视图的完整示例程序:
// ranges/filterview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4, 1};
print(coll); // 1 2 3 4 1 2 3 4 1
auto less4 = [](auto v) { return v < 4; };
print(std::ranges::filter_view{coll, less4}); // 1 2 3 1 2 3 1
print(coll | std::views::filter(less4)); // 1 2 3 1 2 3 1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
程序输出如下:
1 2 3 4 1 2 3 4 1
1 2 3 1 2 3 1
1 2 3 1 2 3 1
2
3
# 过滤视图和缓存
为了提高性能(实现均摊的常数时间复杂度),过滤视图会在视图中缓存begin()
的结果(除非范围仅是输入范围)。这意味着对过滤视图的元素进行首次迭代比后续迭代的开销更大。
因此,最好初始化一次过滤视图并使用两次,例如:
// 更好的做法:
auto v1 = coll | std::views::filter(myPredicate);
check(v1);
process(v1);
2
3
4
而不是初始化并使用两次,例如:
// 较差的做法:
check(coll | std::views::filter(myPredicate));
process(coll | std::views::filter(myPredicate));
2
3
注意,对于具有随机访问功能的范围(例如数组、向量和双端队列),缓存的起始偏移量会随视图一起复制。否则,缓存的起始位置不会被复制。
当过滤视图引用的范围被修改时,缓存可能会产生功能上的影响。例如:
// ranges/filtercache.cpp
#include <iostream>
#include <vector>
#include <list>
#include <ranges>
void print(auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector vec{1, 2, 3, 4};
std::list lst{1, 2, 3, 4};
auto biggerThan2 = [](auto v){ return v > 2; };
auto vVec = vec | std::views::filter(biggerThan2);
auto vLst = lst | std::views::filter(biggerThan2);
// 在前端插入一个新元素(变为 0 1 2 3 4)
vec.insert(vec.begin(), 0);
lst.insert(lst.begin(), 0);
// 正确:3 4
// 正确:3 4
print(vVec);
print(vLst);
// 在前端插入更多元素(变为 98 99 0 -1 0 1 2 3 4)
vec.insert(vec.begin(), {98, 99, 0, -1});
lst.insert(lst.begin(), {98, 99, 0, -1});
print(vVec); // 糟糕:-1 3 4
print(vLst); // 糟糕:3 4
// 创建副本可以解决问题(随机访问范围除外):
auto vVec2 = vVec;
auto vLst2 = vLst;
// 糟糕:-1 3 4
// 正确:98 99 3 4
print(vVec2);
print(vLst2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
注意,从形式上讲,将无效视图复制到向量会产生未定义行为,因为C++标准并未规定缓存的实现方式。由于向量重新分配会使所有迭代器失效,缓存的迭代器也会变为无效。不过,对于随机访问范围,视图通常缓存的是偏移量,而不是迭代器。这意味着,尽管begin
现在指向第三个元素(无论它是否符合过滤条件),但视图仍然有效,因为它仍然包含一个有效的范围。
经验法则是,在底层范围被修改后,不要使用已经调用过begin()
的过滤视图。
# 修改元素时的过滤视图
使用过滤视图时,对写访问有一个重要的额外限制:必须确保修改后的值仍然满足传递给过滤器的谓词。
感谢蒂姆·宋(Tim Song)指出这一点。
为了理解原因,考虑以下程序:
// ranges/viewswrite.cpp
#include <iostream>
#include <vector>
#include <ranges>
namespace vws = std::views;
void print(const auto& coll) {
std::cout << "coll : ";
for (int i : coll) {
std::cout << i << " ";
}
std::cout << "\n";
}
int main() {
std::vector<int> coll{1, 4, 7, 10, 13, 16, 19, 22, 25};
// 用于获取coll中所有偶数元素的视图:
auto isEven = [](auto&& i) { return i % 2 == 0; };
auto collEven = coll | vws::filter(isEven);
print(coll);
// 修改coll中的偶数元素:
for (int& i : collEven) {
std::cout << " increment " << i << "\n";
i += 1; // 错误:未定义行为,因为过滤谓词被破坏
}
print(coll);
// 再次修改coll中的偶数元素:
for (int& i : collEven) {
std::cout << " increment " << i << "\n";
i += 1; // 错误:未定义行为,因为过滤谓词被破坏
}
print(coll);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
我们对集合中的偶数元素进行两次迭代并增加它们的值。一个缺乏经验的程序员可能会认为输出如下:
coll: 1 4 7 10 13 16 19 22 25
coll: 1 5 7 11 13 17 19 23 25
coll: 1 5 7 11 13 17 19 23 25
2
3
然而,程序实际输出如下:
coll: 1 4 7 10 13 16 19 22 25
coll: 1 5 7 11 13 17 19 23 25
coll: 1 6 7 11 13 17 19 23 25
2
3
在第二次迭代时,我们再次增加了之前第一个偶数元素的值。为什么呢?
第一次使用确保只处理偶数元素的视图时,一切正常。但是,视图缓存了第一个匹配谓词的元素的位置,这样begin()
就不必重新计算它。因此,当我们访问该位置的值时,过滤器不会再次应用谓词,因为它已经知道这是第一个匹配元素。所以,当我们进行第二次迭代时,过滤器返回了之前的第一个元素。然而,对于所有其他元素,我们必须再次进行检查,这意味着过滤器找不到更多元素了,因为它们现在都是奇数。
关于即使修改操作破坏了过滤谓词,单遍写访问是否应该是格式良好的,存在一些讨论。这是因为修改不应违反过滤谓词的要求使一些非常合理的示例无效,下面是其中一个例子:
感谢帕特里斯·罗伊(Patrice Roy)提供这个例子。
for (auto& m : collOfMonsters | filter(isDead)) {
m.resurrect(); // 当然,这是萨满的操作
}
2
3
这段代码通常可以编译并运行。然而,从形式上讲,我们又遇到了未定义行为,因为过滤器的谓词(“怪物必须是死的”)被破坏了。不过,对(死的)怪物进行任何其他“修改”都是可行的(例如,将它们“火化”)。
要破坏谓词,必须使用普通循环,例如:
for (auto& m : collOfMonsters) {
if (m.isDead()) {
m.resurrect(); // 当然,这是萨满的操作
}
}
2
3
4
5
# 过滤视图和const
注意,不能对const
过滤视图进行迭代。例如:
void printElems(const auto& coll) {
for (const auto elem& e : coll) {
std::cout << elem << "\n";
}
}
std::vector vec{1, 2, 3, 4, 5};
printElems(vec | std::views::filter(...)); // 错误
2
3
4
5
6
7
8
问题在于,begin()
仅为非const
过滤视图提供,因为缓存迭代器会修改视图。
为了在泛型代码中支持这种视图,必须使用万能/转发引用:
void printElems(auto&& coll) {
...
}
std::list lst{1, 2, 3, 4, 5};
printElems(vec | std::views::filter(...)); // 正确
2
3
4
5
6
# 管道中的过滤视图
在管道中使用过滤视图时,有几个问题需要考虑:
- 在管道中,应尽早使用过滤视图。
- 尤其要注意在过滤器之前进行开销较大的转换操作。
- 在修改元素时使用过滤器,要确保修改后元素仍然满足过滤器的谓词。详见下文。
原因是,过滤器视图前面的视图和适配器可能需要多次计算元素,一次是为了判断它们是否通过过滤,另一次是为了最终使用它们的值。
因此,像下面这样的管道:
rg | std::views::filter(pred) | std::views::transform(func)
比下面这种管道性能更好:
rg | std::views::transform(func) | std::views::filter(pred)
# 过滤视图的接口
下表列出了过滤视图的API。
过滤视图从不提供size()
、data()
或[]
运算符,因为它们既不是sized范围,也不提供随机访问功能。
操作 | 效果 |
---|---|
filter_view r{} | 创建一个指向默认构造范围的过滤视图 |
filter_view r{rg, pred} | 创建一个过滤视图,指向rg 中所有使pred 为真的元素 |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 判断r 是否为空(如果范围支持) 如果 r 不为空则为真(如果定义了empty() ) |
r.front() | 返回第一个元素(如果支持前向访问) |
r.back() r.base() r.pred() | 返回最后一个元素(如果是双向且common范围) 返回 r 所引用或拥有范围的引用 返回谓词的引用 |
表8.18 std::ranges::filter_view<>
类的操作
# 8.6 转换视图
本节讨论所有对迭代的元素值进行修改后再生成结果的视图。
# 8.6.1 转换视图(Transform View)
类型: | std::ranges::transform_view<> |
---|---|
内容: | 底层范围中所有元素经过转换后的值 |
适配器: | std::views::transform() |
元素类型: | 转换操作的返回类型 |
要求: | 至少是输入范围 |
类别: | 与传递的范围相同,但最多为随机访问范围 |
是否为sized范围: | 如果作用于sized范围,则是 |
是否为common范围: | 如果作用于common范围,则是 |
是否为borrowed范围: | 从不 |
缓存: | 无 |
是否可const迭代: | 如果作用于可const迭代的范围且转换操作适用于const值,则是 |
是否传播const性: | 仅当作用于右值范围时 |
类模板std::ranges::transform_view<>
定义了一种视图,它对底层范围的所有元素应用传递的转换操作后,生成转换后的结果。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
// ...
// 打印所有元素的平方值:
for (const auto& elem : std::ranges::transform_view{rg, [](auto x) {
return x * x;
}}) {
std::cout << elem << ' ';
}
2
3
4
5
6
7
8
上述循环将输出:
1 4 9 16 25 36 49 64 81 100 121 144 169
# 转换视图的范围适配器
转换视图也可以(并且通常应该)使用范围适配器创建。该适配器只是将其参数传递给std::ranges::transform_view
构造函数。
std::views::transform(rg, func)
例如:
for (const auto& elem : std::views::transform(rg, [](auto x) {
return x * x;
})) {
std::cout << elem << ' ';
}
2
3
4
5
或者:
for (const auto& elem : rg | std::views::transform([](auto x) {
x * x;
})) {
std::cout << elem << ' ';
}
2
3
4
5
以下是一个使用转换视图的完整示例程序:
// ranges/transformview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <cmath> //for std::sqrt()
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << ' ';
}
std::cout << '\n';
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4, 1};
print(coll);
auto sqrt = [] (auto v) { return std::sqrt(v); };
print(std::ranges::transform_view{coll, sqrt});
print(coll | std::views::transform(sqrt));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
程序输出如下:
1 2 3 4 1 2 3 4 1
1 1.41421 1.73205 2 1 1.41421 1.73205 2 1
1 1.41421 1.73205 2 1 1.41421 1.73205 2 1
2
3
转换操作必须是一个满足std::regular_invocable
概念的可调用对象。这意味着转换操作不应修改底层范围传递的值。然而,不修改值是一种语义约束,并不总是能在编译时检查出来。因此,可调用对象至少应声明按值或按const
引用接受参数。
转换视图生成的元素具有转换操作的返回类型。因此,在我们的示例中,转换视图生成的元素类型为double
。
转换操作的返回类型甚至可以是引用。例如:
// ranges/transformref.cpp
#include <iostream>
#include <vector>
#include <utility>
#include <ranges>
void printPairs(const auto& collOfPairs) {
for (const auto& elem : collOfPairs) {
std::cout << elem.first << '/' << elem.second << ' ';
}
std::cout << '\n';
}
int main() {
// 初始化包含int对的集合:
std::vector<std::pair<int ,int>> coll{{1,9}, {9,1}, {2,2}, {4,1}, {2,7}};
printPairs(coll);
// 函数,返回一对值中较小的那个:
auto minMember = [] (std::pair<int ,int>& elem) -> int& {
return elem.second < elem.first ? elem.second : elem.first;
};
// 对每个对元素中较小的值进行递增:
for (auto&& member : coll | std::views::transform(minMember)) {
++member;
}
printPairs(coll);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
程序输出如下:
1/9 9/1 2/2 4/1 2/7
2/9 9/2 3/2 4/2 3/7
2
注意,这里传递给transform
作为转换操作的lambda表达式必须指定返回引用:
[] (std::pair<int ,int>& elem) -> int& {
return ...
}
2
3
否则,lambda表达式将返回每个元素的副本,这样只会递增这些副本的成员,而coll
中的元素将保持不变。
转换视图在内部存储传递的范围(可能会像all()
那样将其转换为视图)。因此,只有在传递的范围有效时,转换视图才有效(除非传递的是右值,这意味着在内部使用拥有型视图)。
# 转换视图的特殊特性
和标准视图通常的情况一样,转换视图不会将常量性(constness)传递给元素。这意味着,即使视图是常量的,传递给转换函数的元素也不是常量的。
例如,使用上面transformref.cpp
的示例,你还可以实现以下操作:
std::vector<std::pair<int ,int>> coll{{1,9}, {9,1}, {2,2}, {4,1}, {2,7}};
// 函数,返回一对值中较小的那个
auto minMember = [] (std::pair<int ,int>& elem) -> int& {
return elem.second < elem.first ? elem.second : elem.first;
};
// 对每个pair元素中较小的值进行递增
const auto v = coll | std::views::transform(minMember);
for (auto&& member : v) {
++member;
}
2
3
4
5
6
7
8
9
10
注意,只有当底层范围是可获取大小范围(sized range)且是公共范围(common range)时,take
视图才是公共的(迭代器和哨兵类型相同)。为了统一类型,你可能需要使用common_view
。起始迭代器和哨兵(结束迭代器)都是特殊的内部辅助类型。
# 转换视图的接口
表“std::ranges::transform_view<>
类的操作”列出了转换视图的API。
操作 | 效果 |
---|---|
transform_view r{} transform_view r{rg , func} r.begin() r.end() r.empty() if (r) r.size() r.front() r.back() r[idx] r.data() r.base() | 创建一个引用默认构造范围的转换视图 创建一个转换视图,其中范围 rg 的所有元素的值都由func 进行转换返回起始迭代器 返回哨兵(结束迭代器) 返回 r 是否为空(如果范围支持)如果 r 不为空则返回true (如果定义了empty() )返回元素数量(如果是可获取大小范围) 返回第一个元素(如果支持前向访问) 返回最后一个元素(如果支持双向且是公共范围) 返回第 n 个元素(如果支持随机访问)返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用 |
表8.19 std::ranges::transform_view<>
类的操作
# 8.6.2 元素视图(Elements View)
类型:std::ranges::elements_view<> 内容: 范围中所有类似元组元素的第 n 个成员/属性适配器: std::views::elements<> 元素类型: 成员/属性的类型 要求: 至少输入范围 类别: 与传递范围相同,但最多为随机访问范围 是否为可获取大小范围: 如果传递范围是 是否为公共范围: 如果传递范围是 是否为借用范围: 如果是借用视图或左值非视图 缓存: 无 是否为常量可迭代范围: 如果传递范围是 传播常量性: 仅当传递范围是右值时 |
---|
类模板std::ranges::elements_view<>
定义了一个视图,它选择传递范围中所有元素的第idx
个成员/属性/元素。该视图对每个元素调用get<idx>(elem)
,尤其可用于:
- 获取所有
std::tuple
元素的第idx
个成员。 - 获取所有
std::variant
元素的第idx
个可选值。 - 获取
std::pair
元素的第一个或第二个成员(不过,对于关联容器和无序容器的元素,使用keys_view
和values_view
会更方便)。
注意,对于这个视图,类模板参数推导不起作用,因为你必须显式指定索引作为参数,而类模板不支持部分参数推导。因此,你必须这样写:
std::vector<std::tuple<std::string, std::string, int>> rg{
{ "Bach " , "Johann Sebastian " , 1685},
{ "Mozart " , "Wolfgang Amadeus " , 1756},
{ "Beethoven " , "Ludwig van " , 1770},
{ "Chopin " , "Frederic " , 1810},
};
for (const auto& elem : std::ranges::elements_view<decltype(std::views::all(rg)), 2>{rg}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
8
9
上述循环会输出:
1685 1756 1770 1810
注意,如果传递的范围不是视图,你必须使用范围适配器all()
指定底层范围的类型:
std::ranges::elements_view<decltype(std::views::all(rg)), 2>{rg} // 没问题
你也可以直接使用std::views::all_t<>
指定类型:
std::ranges::elements_view<std::views::all_t<decltype(rg)&>, 2>{rg} // 没问题
std::ranges::elements_view<std::views::all_t<decltype((rg))>, 2>{rg} // 没问题
2
不过,这里的细节很重要。如果范围还不是视图,all_t<>
的参数必须是左值引用。因此,你需要在rg
的类型后加上&
,或者在rg
周围加上双层括号(根据规则,如果传递的表达式是左值,decltype
会产生左值引用)。没有&
的单个括号不起作用:
std::ranges::elements_view<std::views::all_t<decltype(rg)>, 2>{rg} // 错误
因此,声明视图的更简单方法是使用相应的范围适配器。
# 元素视图的范围适配器
元素视图也可以(通常应该)通过范围适配器创建。适配器使视图的使用更加容易,因为你不必指定底层范围的类型:std::views::elements<idx>(rg)
。
例如:
for (const auto& elem : std::views::elements<2>(rg)) {
std::cout << elem << " ";
}
2
3
或者:
for (const auto& elem : rg | std::views::elements<2>) {
std::cout << elem << " ";
}
2
3
使用范围适配器时,elements<idx>(rg)
始终等同于:
std::ranges::elements_view<std::views::all_t<decltype(rg)&>, idx>{rg}
元素视图在内部存储传递的范围(以与all()
相同的方式可选地转换为视图)。因此,只有在传递的范围有效时,它才有效(除非传递的是右值,这意味着内部使用的是拥有视图(owning view))。
起始迭代器和哨兵(结束迭代器)都是特殊的内部辅助类型,如果传递的范围是公共范围,它们也是公共的。
下面是一个使用元素视图的完整示例程序:
// ranges/elementsview.cpp
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
// 用于数学常量
#include <numbers>
// 用于sort()
#include <algorithm>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector<std::tuple<int, std::string, double>> coll{
{1, "pi " , std::numbers::pi},
{2, "e " , std::numbers::e},
{3, "golden-ratio " , std::numbers::egamma},
{4, "euler-constant " , std::numbers::phi},
};
std::ranges::sort(coll, std::less{},
[](const auto& e) {return std::get<2>(e);});
print(std::ranges::elements_view<decltype(std::views::all(coll)), 1>{coll});
print(coll | std::views::elements<2>);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
程序输出如下:
golden-ratio euler-constant e pi
0.577216 1.61803 2.71828 3.14159
2
注意,你不应该像这样对coll
的元素进行排序:
std::ranges::sort(coll | std::views::elements<2>); // 错误
这只会对元素的值进行排序,而不是对整个元素进行排序,会导致如下输出:
pi e golden-ratio euler-constant
0.577216 1.61803 2.71828 3.14159
2
# 用于其他类似元组类型的元素视图
为了能够将这个视图用于用户定义的类型,你需要为它们指定类似元组的API。然而,按照当前的规范,这实际上并非通常的类似元组API设计和相应概念定义方式存在问题。因此,严格来说,你只能将类std::ranges::elements_view
或适配器std::views::elements<>
用于std::pair<>
和std::tuple<>
。
不过,如果你确保在包含<ranges>
头文件之前定义了类似元组的API,它就能正常工作。
例如:
// ranges/elementsviewhack.hpp
#include <iostream>
#include <string>
#include <tuple>
// 不要先包含<ranges>!!
struct Data {
int id;
std::string value;
};
std::ostream& operator<< (std::ostream& strm, const Data& d) {
return strm << " [ " << d.id << " : " << d.value << " ]";
}
// 对Data的类似元组访问:
namespace std {
template<>
struct tuple_size<Data> : integral_constant<size_t, 2> { };
template<>
struct tuple_element<0, Data> {
using type = int ;
};
template<>
struct tuple_element<1, Data> {
using type = std::string;
};
template<size_t Idx> auto get(const Data& d) {
if constexpr (Idx == 0) {
return d.id;
}
else {
return d.value;
}
}
} // namespace std
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
如果你像这样使用它:
// ranges/elementsviewhack.cpp
// 在"elementsview.hpp"之前不要包含<ranges>
#include "elementsviewhack.hpp "
#include <iostream>
#include <vector>
#include <ranges>
void print(const auto& coll) {
std::cout << "coll:\n " ;
for (const auto& elem : coll) {
std::cout << "- " << elem << "\n";
}
}
int main() {
Data d1{42, "truth "};
std::vector<Data> coll{d1, Data{0, "null "}, d1};
print(coll);
print(coll | std::views::take(2));
print(coll | std::views::elements<1>);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
你会得到如下输出:
coll:
- [42: truth]
- [0: null]
- [42: truth]
coll:
- [42: truth]
- [0: null]
coll:
- truth
- null
- truth
2
3
4
5
6
7
8
9
10
11
# 元素视图(Elements Views)的接口
表8.20列出了std::ranges::elements_view<>
类的API。
操作 | 效果 |
---|---|
elements_view r{} elements_view r{rg} | 创建一个引用默认构造范围的elements_view 创建一个引用范围 rg 的elements_view |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是固定大小范围) 返回第一个元素(如果支持前向访问) |
r.back() r[idx] | 返回最后一个元素(如果是双向且通用) 返回第 n 个元素(如果是随机访问) |
r.data() r.base() | 返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用 |
表8.20 std::ranges::elements_view<>
类的操作
# 8.6.3 键视图(Keys View)和值视图(Values View)
类型: 内容: 适配器: 元素类型: 要求: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::keys_view<> std::ranges::values_view<> 范围中所有类似元组(tuple-like)元素的第一个/第二个成员或属性 std::views::keys std::views::values 第一个成员的类型 第二个成员的类型 至少为输入范围 至少为输入范围 与传递的范围相同,但最多为随机访问范围 如果作用于固定大小范围则是 如果作用于固定大小范围则是 如果作用于通用范围则是 如果作用于通用范围则是 如果作用于借用视图或左值非视图则是 无 无 如果作用于可常量迭代的范围则是 如果作用于可常量迭代的范围则是 仅当作用于右值范围时 仅当作用于右值范围时 |
---|
类模板std::ranges::keys_view<>
定义了一个视图,它从传递范围的元素中选择第一个成员/属性/元素。它只不过是使用索引为0的elements_view
的快捷方式。也就是说,它对每个元素调用get<0>(elem)
。
类模板std::ranges::values_view<>
定义了一个视图,它从传递范围的元素中选择第二个成员/属性/元素。它只不过是使用索引为1的elements_view
的快捷方式。也就是说,它对每个元素调用get<1>(elem)
。
这些视图尤其可用于:
- 获取
std::pair
元素的first
/second
成员,这对于选择map
、unordered_map
、multimap
和unordered_multimap
元素的键/值特别有用。 - 获取
std::tuple
元素的第一个/第二个成员。 - 获取
std::variant
元素的第一个/第二个可选值。
不过,对于后两种应用场景,直接使用带索引0的elements_view
可能更具可读性。
注意,这些视图的类模板参数推导目前还不可用7。因此,你必须显式指定模板参数。例如:
std::map<std::string, int> rg{
{ "Bach ", 1685}, { "Mozart ", 1756}, { "Beethoven ", 1770},
{ "Tchaikovsky ", 1840}, { "Chopin ", 1810}, { "Vivaldi ", 1678}
};
for (const auto& e : std::ranges::keys_view<decltype(std::views::all(rg))>{rg}) {
std::cout << e << " ";
}
2
3
4
5
6
7
上述循环输出:
Bach Beethoven Chopin Mozart Tchaikovsky Vivaldi
7 见http://wg21.link/lwg3563。
# 键/值视图的范围适配器
键视图和值视图也可以(通常应该)使用范围适配器创建:
std::views::keys(rg)
std::views::values(rg)
2
适配器让视图的使用变得更加容易,因为你无需指定底层范围的类型。例如:
for (const auto& elem : rg | std::views::keys) {
std::cout << elem << " ";
}
2
3
或者:
for (const auto& elem : rg | std::views::values) {
std::cout << elem << " ";
}
2
3
其他方面与元素视图相同。
以下是一个使用键视图和值视图的完整示例程序:
[`ranges/keysvaluesview.cpp`]
#include <iostream>
#include <string>
#include <unordered_map>
#include <ranges>
#include <numbers> //for math constants
#include <algorithm> //for sort()
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::unordered_map<std::string, double> coll{
{ "pi ", std::numbers::pi},
{ "e ", std::numbers::e},
{ "golden-ratio ", std::numbers::egamma},
{ "euler-constant ", std::numbers::phi}
};
print(std::ranges::keys_view<decltype(std::views::all(coll))>{coll});
print(std::ranges::values_view<decltype(std::views::all(coll))>{coll});
print(coll | std::views::keys);
print(coll | std::views::values);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
该程序的输出如下:
euler-constant golden-ratio e pi
1.61803 0.577216 2.71828 3.14159
euler-constant golden-ratio e pi
1.61803 0.577216 2.71828 3.14159
2
3
4
# 8.7 可变视图(Mutating Views)
本节讨论所有改变元素顺序的视图(目前只有一种视图)。
# 8.7.1 反向视图(Reverse View)
类型: 内容: 适配器: 元素类型: 要求: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::reverse_view<> 范围中的所有元素以相反顺序排列 std::views::reverse 与传递范围的类型相同 至少为双向范围 与传递的范围相同,但最多为随机访问范围 如果作用于固定大小范围则是 始终是 如果作用于借用视图或左值非视图则是 除非是通用范围或随机访问且固定大小范围,否则缓存 begin() 如果作用于可常量迭代的通用范围则是 仅当作用于右值范围时 |
---|
类模板std::ranges::reverse_view<>
定义了一个视图,它以相反顺序遍历底层范围的元素。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& elem : std::ranges::reverse_view{rg}) {
std::cout << elem << " ";
}
2
3
4
5
上述循环输出:
13 12 11 10 9 8 7 6 5 4 3 2 1
# 反向视图的范围适配器
反向视图也可以(通常应该)使用范围适配器创建:
std::views::reverse(rg)
例如:
for (const auto& elem : std::views::reverse(rg)) {
std::cout << elem << " ";
}
2
3
或者:
for (const auto& elem : rg | std::views::reverse) {
std::cout << elem << " ";
}
2
3
注意,适配器并不总是返回reverse_view
:
- 反转后的反转范围会返回原始范围。
- 如果传递的是反转后的子范围,则返回原始子范围及相应的非反转迭代器。
反向视图在内部存储传递的范围(可能会像使用all()
一样转换为视图)。因此,只有在传递的范围有效时,它才有效(除非传递的是右值,这意味着内部使用的是owning_view
) 。
其迭代器只是传递范围的反向迭代器。以下是一个使用反向视图的完整示例程序:
[`ranges/reverseview.cpp`]
#include <iostream>
#include <string>
#include <vector>
#include <ranges>
void print(std::ranges::input_range auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4};
print(coll);
print(std::ranges::reverse_view{coll});
print(coll | std::views::reverse);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
该程序的输出如下:
1 2 3 4 1 2 3 4
4 3 2 1 4 3 2 1
4 3 2 1 4 3 2 1
2
3
# 反向视图与缓存
为了提高性能,反向视图会在视图中缓存begin()
的结果(除非范围仅是输入范围)。
注意,对于具有随机访问功能的范围(例如数组、向量和双端队列),缓存的起始偏移量会随着视图一起复制。否则,缓存的起始位置不会被复制。
当反向视图引用的范围被修改时,缓存可能会产生功能性影响。例如:
[`ranges/reversecache.cpp`]
#include <iostream>
#include <vector>
#include <list>
#include <ranges>
void print(auto&& coll) {
for (const auto& elem : coll) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector vec{1, 2, 3, 4};
std::list lst{1, 2, 3, 4};
auto vVec = vec | std::views::take(3) | std::views::reverse;
auto vLst = lst | std::views::take(3) | std::views::reverse;
print(vVec);
print(vLst);
// 在前端插入一个新元素(变为0 1 2 3 4)
vec.insert(vec.begin(), 0);
lst.insert(lst.begin(), 0);
print(vVec);
print(vLst);
// 创建副本可以解决问题:
auto vVec2 = vVec;
auto vLst2 = vLst;
print(vVec2);
print(vLst2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
从形式上来说,将视图复制到向量会产生未定义行为,因为C++标准并未规定缓存的实现方式。由于向量重新分配内存会使所有迭代器失效,缓存的迭代器也会变为无效。不过,对于随机访问范围,视图通常缓存的是偏移量,而非迭代器。因此,对于向量,视图仍然有效。
还要注意,对整个容器的反向视图通常可以正常工作,因为结束迭代器是被缓存的。
经验法则是,在底层范围被修改后,不要再使用已经调用过begin()
的反向视图。
# 反向视图与const
注意,并非总能遍历const
反向视图。实际上,被引用的范围必须是通用范围。例如:
void printElems(const auto& coll) {
for (const auto elem& e : coll) {
std::cout << elem << "\n";
}
}
std::vector vec{1, 2, 3, 4, 5};
// vec中开头的奇数元素:
auto vecFirstOdd = std::views::take_while(vec, [](auto x) {
return x % 2 != 0;
});
printElems(vec | std::views::reverse);
printElems(vecFirstOdd);
printElems(vecFirstOdd | std::views::reverse); // 错误
2
3
4
5
6
7
8
9
10
11
12
13
14
15
为了在泛型代码中支持这种视图,必须使用万能引用(universal/forwarding references):
void printElems(auto&& coll) {
...
}
std::vector vec{1, 2, 3, 4, 5};
// vec中开头的奇数元素:
auto vecFirstOdd = std::views::take_while(vec, [](auto x) {
return x % 2 != 0;
});
printElems(vecFirstOdd | std::views::reverse); // 正常
2
3
4
5
6
7
8
9
10
11
# 反向视图的接口
表8.21列出了std::ranges::reverse_view<>
类的API。
操作 | 效果 |
---|---|
reverse_view r{} reverse_view r{rg} | 创建一个引用默认构造范围的reverse_view 创建一个引用范围 rg 的reverse_view |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是固定大小范围) 返回第一个元素(如果支持前向访问) |
r.back() r[idx] | 返回最后一个元素(如果是双向且通用) 返回第 n 个元素(如果是随机访问) |
r.data() r.base() | 返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用 |
表8.21 std::ranges::reverse_view<>
类的操作
# 8.8 处理多个范围的视图
本节讨论所有处理多个范围的视图。
# 8.8.1 拆分视图(Split View)和延迟拆分视图(Lazy-Split View)
类型: 内容: 适配器: 元素类型: 要求: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::split_view<> std::ranges::lazy_split_view<> 一个范围的所有元素被拆分成多个视图 std::views::split() std::views::lazy_split() 引用集合 引用集合 至少为前向范围 至少为输入范围 始终为前向 输入或前向 从不 从不 如果是通用前向范围则是 如果是通用前向范围则是 从不 从不 始终缓存 begin() 如果是输入范围,则缓存当前值 从不 如果是可常量迭代的前向范围则是 — 从不 |
---|
类模板std::ranges::split_view<>
和std::ranges::lazy_split_view<>
都定义了一种视图,该视图引用由传递的分隔符分隔的范围的多个子视图8。split_view<>
和lazy_split_view<>
之间的区别如下:
split_view<>
不能遍历const
视图;lazy_split_view<>
可以(如果它引用的范围至少是前向范围)。split_view<>
只能处理具有至少前向迭代器的范围(必须满足forward_range
概念)。split_view<>
的元素只是被引用范围的迭代器类型的std::ranges::subrange
(保持被引用范围的类别)。lazy_split_views<>
的元素是std::ranges::lazy_split_view
类型的视图,始终是前向范围,这意味着甚至不支持size()
。split_view<>
性能更好。
这意味着通常应使用split_view<>
,除非因为仅使用输入范围或视图被用作const
范围而无法使用它。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
...
for (const auto& sub : std::ranges::split_view{rg, 5}) {
for (const auto& elem : sub) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
7
8
上述循环输出:
1 2 3 4
6 7 8 9 10 11 12 13
2
也就是说,无论在rg
中何处找到值为5的元素,都会结束前一个视图并开始一个新视图。
8std::ranges::lazy_split_view<>
最初并非C++20标准的一部分,而是在之后通过http://wg21.link/p2210r2添加到C++20中的。
# 拆分视图和延迟拆分视图的范围适配器
拆分视图和延迟拆分视图也可以(并且通常应该)使用范围适配器创建。适配器只是将它们的参数传递给相应的视图构造函数:
std::views::split(rg, sep)
std::views::lazy_split(rg, sep)
2
例如:
for (const auto& sub : std::views::split(rg, 5)) {
for (const auto& elem : sub) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
或者:
for (const auto& sub : rg | std::views::split(5)) {
for (const auto& elem : sub) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
创建的视图可能为空。因此,对于每个前导和尾随分隔符,以及每当两个分隔符相邻时,都会创建一个空视图。例如:
std::list<int> rg{5, 5, 1, 2, 3, 4, 5, 6, 5, 5, 4, 3, 2, 1, 5, 5};
for (const auto& sub : std::ranges::split_view{rg, 5}) {
std::cout << "subview : ";
for (const auto& elem : sub) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
7
8
这里的输出如下9:
subview:
subview:
subview: 1 2 3 4
subview: 6
subview:
subview: 4 3 2 1
subview:
subview:
2
3
4
5
6
7
8
9最初的C++20标准规定最后一个分隔符会被忽略,这意味着我们在末尾只会得到一个空子视图。此问题已通过http://wg21.link/p2210r2修复。
除了单个值,也可以传递一个值序列作为分隔符。例如:
std::vector<int> rg{1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3 };
...
// 以5和1的序列为分隔符进行拆分:
for (const auto& sub : std::views::split(rg, std::list{5, 1})) {
for (const auto& elem : sub) {
std::cout << elem << " ";
}
std::cout << "\n";
}
2
3
4
5
6
7
8
9
这段代码的输出是:
1 2 3 4
2 3 4
2 3
2
3
传递的元素集合必须是有效的视图且是forward_range
。因此,在容器中指定子序列时,必须将其转换为视图。例如:
// 以指定的4和5的模式进行拆分:
std::array pattern{4, 5};
for (const auto& sub : std::views::split(rg, std::views::all(pattern))) {
...
}
2
3
4
5
可以使用拆分视图来拆分字符串。例如:
std::string str{ "No problem can withstand the assault of sustained thinking "};
for (auto sub : std::views::split(str, "th "sv)) { // 按"th"拆分
std::cout << std::string_view{sub} << "\n";
}
2
3
4
每个子字符串sub
的类型是std::ranges::subrange<decltype(str.begin())>
。这样的代码在延迟拆分视图中无法工作。
拆分视图或延迟拆分视图在内部存储传递的范围(可能会像使用all()
一样转换为视图)。因此,只有在传递的范围有效时,它才有效(除非传递的是右值,这意味着内部使用的是owning_view
)。
起始迭代器和哨兵(结束迭代器)都是特殊的内部辅助类型,如果传递的范围是通用的,它们也是通用的。
以下是一个使用拆分视图的完整示例程序:
[`ranges/splitview.cpp`]
#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <ranges>
void print(auto&& obj, int level = 0) {
if constexpr(std::ranges::input_range<decltype(obj)>) {
std::cout << "[";
for (const auto& elem : obj) {
print(elem, level+1);
}
std::cout << "]";
}
else {
std::cout << obj << " ";
}
if (level == 0) std::cout << "\n";
}
int main() {
std::vector coll{1, 2, 3, 4, 1, 2, 3, 4};
print(coll);
print(std::ranges::split_view{coll, 2});
print(coll | std::views::split(3));
print(coll | std::views::split(std::array{4, 1}));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
该程序的输出如下:
[1 2 3 4 1 2 3 4 ]
[[1 ][3 4 1 ][3 4 ]]
[[1 2 ][4 1 2 ][4 ]]
[[1 2 3 ][2 3 4 ]]
2
3
4
# 拆分视图与const
注意,无法遍历const
拆分视图。例如:
std::vector<int> coll{5, 1, 5, 1, 2, 5, 5, 1, 2, 3, 5, 5, 5};
...
const std::ranges::split_view sv{coll, 5};
for (const auto& sub : sv) { // 对于const拆分视图,这是错误的
std::cout << sub.size() << " ";
}
2
3
4
5
6
如果将视图传递给一个以const
引用作为参数的泛型函数,这会是个问题:
void printElems(const auto& coll) {
...
}
printElems(std::views::split(rg, 5)); // 错误
2
3
4
5
为了在泛型代码中支持这种视图,必须使用万能引用(universal/forwarding references):
void printElems(auto&& coll) {
...
}
2
3
或者,可以使用延迟拆分视图(lazy_split_view)。然而,这样子视图元素只能作为前向范围使用,这意味着不能进行诸如调用size()
、反向迭代或对元素进行排序等操作:
std::vector<int> coll{5, 1, 5, 1, 2, 5, 5, 1, 2, 3, 5, 5, 5};
...
const std::ranges::lazy_split_view sv{coll, 5};
for (const auto& sub : sv) { // 对于const延迟拆分视图,这是正确的
std::cout << sub.size() << " "; // 错误
std::sort(sub); // 错误
for (const auto& elem : sub) {
std::cout << elem << " ";
}
}
2
3
4
5
6
7
8
9
10
11
# 拆分视图和延迟拆分视图的接口
表8.22列出了std::ranges::split_view<>
和std::ranges::lazy_split_view<>
类的API。
操作 | 效果 |
---|---|
split_view r{} split_view r{rg} | 创建一个引用默认构造范围的split_view 创建一个引用范围 rg 的split_view |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是固定大小范围) 返回第一个元素(如果支持前向访问) |
r.back() r[idx] | 返回最后一个元素(如果是双向且通用) 返回第 n 个元素(如果是随机访问) |
r.data() r.base() | 返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用 |
表8.22 std::ranges::split_view<>
和std::ranges::lazy_split_view<>
类的操作
# 8.8.2 连接视图(Join View)
类型: 内容: 适配器: 元素类型: 要求: 类别: 是否为固定大小范围: 是否为通用范围: 是否为借用范围: 缓存: 是否可常量迭代: 是否传播常量性: | std::ranges::join_view<> 将多个范围的视图中的所有元素作为一个视图进行遍历 std::views::join() 与传递范围的类型相同 至少为输入范围 输入到双向 从不 不一定 从不 无 如果在可常量迭代的范围且元素仍然是 const 引用,则是仅当在右值范围时 |
---|
类模板std::ranges::join_view<>
定义了一种视图,它遍历由多个范围组成的视图中的所有元素。
例如:
std::vector<int> rg1{1, 2, 3, 4};
std::vector<int> rg2{0, 8, 15};
std::vector<int> rg3{5, 4, 3, 2, 1, 0};
std::array coll{rg1, rg2, rg3};
...
for (const auto& elem : std::ranges::join_view{coll}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
8
上述循环输出:
1 2 3 4 0 8 15 5 4 3 2 1 0
# 连接视图的范围适配器
连接视图也可以(并且通常应该)使用范围适配器创建。适配器只是将其参数传递给std::ranges::join_view
构造函数:
std::views::join(rg)
例如:
for (const auto& elem : std::views::join(coll)) {
std::cout << elem << " ";
}
2
3
或者:
for (const auto& elem : coll | std::views::join) {
std::cout << elem << " ";
}
2
3
以下是一个使用连接视图的完整示例程序:
[`ranges/joinview.cpp`]
#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <ranges>
#include "printcoll.hpp"
int main() {
std::vector<std::string> rg1{ "he ", "hi ", "ho"};
std::vector<std::string> rg2{ "--- ", " | ", "---"};
std::array coll{rg1, rg2, rg1};
printColl(coll);
printColl(std::ranges::join_view{coll});
printColl(coll | std::views::join);
printColl(coll | std::views::join | std::views::join);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
它使用了一个辅助函数来递归打印集合:
[`ranges/printcoll.hpp`]
#include <iostream>
#include <ranges>
template<typename T>
void printColl(T&& obj, int level = 0) {
if constexpr(std::same_as<std::remove_cvref_t<T>, std::string>) {
std::cout << "\" " << obj << "\" ";
}
else if constexpr(std::ranges::input_range<T>) {
std::cout << "[";
for (auto pos = obj.begin(); pos != obj.end(); ++pos) {
printColl(*pos, level+1);
if (std::ranges::next(pos) != obj.end()) {
std::cout << " ";
}
}
std::cout << "]";
}
else {
std::cout << obj;
}
if (level == 0) std::cout << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
该程序的输出如下:
[["he" "hi" "ho"] ["--- " " | " "---"] ["he" "hi" "ho"]]
["he" "hi" "ho" "--- " " | " "---" "he" "hi" "ho"]
["he" "hi" "ho" "--- " " | " "---" "he" "hi" "ho"]
[h e h i h o - - - | - - - h e h i h o]
2
3
4
结合std::ranges::subrange
类型,你可以使用连接视图连接多个数组的元素。例如:
int arr1[]{1, 2, 3, 4, 5};
int arr2[] = {0, 8, 15};
int arr3[10]{1, 2, 3, 4, 5};
...
std::array<std::ranges::subrange<int*>, 3> coll{arr1, arr2, arr3};
for (const auto& elem : std::ranges::join_view{coll}) {
std::cout << elem << " ";
}
2
3
4
5
6
7
8
或者,你可以如下声明coll
:
std::array coll{std::ranges::subrange{arr1}, std::ranges::subrange{arr2}, std::ranges::subrange{arr3}};
连接视图是C++20中唯一处理范围的范围的视图。因此,它有外层和内层迭代器。结果视图的属性可能取决于这两者。
注意,内部迭代器不支持迭代器特性(iterator traits)。因此,你应该优先使用std::ranges::next()
等工具,而不是std::next()
。否则,代码可能无法编译。
# 连接视图与const
注意,并非总能遍历const
连接视图。如果范围不可常量迭代,或者内层范围产生的是普通值而非引用,就会出现这种情况。
对于后一种情况,考虑以下示例:
[`ranges/joinconst.cpp`]
#include <vector>
#include <array>
#include <ranges>
#include "printcoll.hpp"
void printConstColl(const auto& coll) {
printColl(coll);
}
int main() {
std::vector<int> rg1{1, 2, 3, 4};
std::vector<int> rg2{0, 8, 15};
std::vector<int> rg3{5, 4, 3, 2, 1, 0};
std::array coll{rg1, rg2, rg3};
printConstColl(coll);
printConstColl(coll | std::views::join);
auto collTx = [] (const auto& coll) { return coll; };
auto coll2values = coll | std::views::transform(collTx);
printConstColl(coll2values);
printConstColl(coll2values | std::views::join); // 错误
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
当我们连接三个范围组成的数组的元素时,可以调用printConstColl()
,它以const
引用的方式接受范围。我们得到以下输出:
[[1 2 3 4] [0 8 15] [5 4 3 2 1 0]]
[1 2 3 4 0 8 15 5 4 3 2 1 0]
2
然而,当我们将整个数组传递给一个转换视图,该视图按值返回所有内层范围时,调用printConstColl()
就会出错。
对于内层范围产生普通值的视图,调用printColl()
是可行的。注意,这要求printColl()
使用std::ranges::next()
而不是std::next()
。否则,即使下面的代码也无法编译:
printColl(coll2values | std::views::join); // 如果使用std::next(),则错误
# 连接视图的特殊特性
如果外层和内层范围至少都是双向的,并且内层范围是通用范围,那么结果类别是双向的。否则,如果外层和内层范围至少都是前向范围,结果类别是前向的。否则,结果类别是输入范围。
# 连接视图的接口
表8.23列出了std::ranges::join_view<>
类的API。
操作 | 效果 |
---|---|
join_view r{} join_view r{rg} | 创建一个引用默认构造范围的join_view 创建一个引用范围 rg 的join_view |
r.begin() r.end() | 返回起始迭代器 返回哨兵(结束迭代器) |
r.empty() if (r) | 返回r 是否为空(如果范围支持)如果 r 不为空则为true (如果定义了empty() ) |
r.size() r.front() | 返回元素数量(如果引用的是固定大小范围) 返回第一个元素(如果支持前向访问) |
r.back() r[idx] | 返回最后一个元素(如果是双向且通用) 返回第 n 个元素(如果是随机访问) |
r.data() r.base() | 返回元素内存的原始指针(如果元素在连续内存中) 返回 r 引用或拥有的范围的引用 |
表8.23 std::ranges::join_view<>
类的操作