创建容器视图的范围
新的范围库是 C++20 中更重要的新增内容之一。它为过滤和处理容器提供了一种新的范式。范围提供了干净直观的构建块,使得代码更有效、更易读。
让我们首先定义一些术语:
一个范围是可以迭代的对象集合。换句话说,任何支持 begin() 和 end() 迭代器的结构都是一个范围。这包括大多数 STL 容器。
视图是一个转换另一个底层范围的范围。视图是惰性的,这意味着它们只在范围迭代时操作。视图返回底层范围的数据,本身不拥有任何数据。视图以 O(1) 常数时间操作。
视图适配器是一个对象,它接受一个范围并返回一个视图对象。视图适配器可以使用 | 运算符与其他视图适配器链式使用。
注意
<ranges> 库使用 std::ranges 和 std::ranges::view 名称空间。认识到这很笨重,标准包括一个别名 std::ranges::view 作为简单的 std::view。我觉得这仍然很笨重。对于这个食谱,我将使用以下别名,以节省空间,因为我觉得它更优雅:
namespace ranges = std::ranges; // 节省手指!
namespace views = std::ranges::views;
这适用于这个食谱中的所有代码。
如何做到这一点…
范围和视图类在 <ranges> 头文件中。让我们看看你如何使用它们:
视图应用于范围,如下所示:
const vector<int> nums{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result = ranges::take_view(nums, 5);
for (auto v: result) cout << v << " ";
输出:
1 2 3 4 5
ranges::take_view(range, n) 是一个返回前 n 个元素的视图。
你也可以使用 take_view() 的视图适配器版本:
auto result = nums | views::take(5);
for (auto v: result) cout << v << " ";
输出:
1 2 3 4 5
视图适配器在 std::ranges::views 名称空间中。视图适配器从 | 运算符左侧的 range 操作数中获取范围,很像 iostreams 使用 << 运算符的方式。| 运算符是从左到右评估的。
因为视图适配器是可迭代的,它也符合范围的资格。这允许它们被连续应用,如下所示:
const vector<int> nums{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result = nums | views::take(5) |
views::reverse;
输出:
5 4 3 2 1
filter() 视图使用谓词函数:
auto result = nums |
views::filter([](int i){ return 0 == i % 2; });
输出:
2 4 6 8 10
transform() 视图使用转换函数:
auto result = nums |
views::transform([](int i){ return i * i; });
输出:
1 4 9 16 25 36 49 64 81 100
当然,这些视图和适配器适用于任何类型的范围:
cosnt vector<string> words{ "one", "two", "three", "four", "five" };
auto result = words | views::reverse;
输出:
five four three two one
范围库还包括一些范围工厂。iota 工厂将生成一系列递增的值:
auto rnums = views::iota(1, 10);
输出:
1 2 3 4 5 6 7 8 9
iota(value, bound) 函数从 value 开始生成一个序列,结束在 bound 之前。如果省略了 bound,序列就是无限的:
auto rnums = views::iota(1) | views::take(200);
输出:
1 2 3 4 5 6 7 8 9 10 11 12 […] 196 197 198 199 200
范围、视图和视图适配器非常灵活和有用。让我们更深入地了解,以更好地理解。
它是如何工作的…
为了满足范围的基本要求,一个对象必须至少有两个迭代器,begin() 和 end(),其中 end() 迭代器是一个哨兵,用于确定范围的终点。大多数 STL 容器都符合范围的要求,包括 string、vector、array、map 等,值得注意的例外是容器适配器,如 stack 和 queue,它们没有 begin 和 end 迭代器。
视图是一个操作范围并返回修改后范围的对象。视图惰性操作,并且不包含自己的数据。它不保留底层数据的副本,而是根据需要简单地返回指向底层元素的迭代器。让我们检查这段代码片段:
vector<int> vi { 0, 1, 2, 3, 4, 5 };
ranges::take_view tv{vi, 2};
for(int i : tv) {
cout << i << " ";
}
cout << "\n";
输出:
0 1
在这个例子中,take_view 对象接受两个参数,一个范围(在这种情况下,一个 vector<int> 对象)和一个计数。结果是从向量中取出前计数个对象的视图。在评估时,在 for 循环迭代期间,take_view 对象简单地根据需要返回指向向量对象元素的迭代器。在这个过程中,向量对象没有被修改。
范围名称空间中的许多视图在 views 名称空间中都有相应的范围适配器。这些适配器可以使用按位或 (|) 运算符使用,像这样:
vector<int> vi { 0, 1, 2, 3, 4, 5 };
ranges::take_view tv{vi, 2};
for(int i : tv) {
cout << i << " ";
}
cout << "\n";
输出:
0 1
正如预期的那样,| 运算符从左到右评估。由于范围适配器的结果又是另一个范围,这些适配器表达式可以被链接:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto tview = vi | views::reverse | views::take(5);
for(int i : tview) {
cout << i << " ";
}
cout << "\n";
输出:
9 8 7 6 5
库中还包括一个 filter 视图,它与谓词一起使用,用于定义简单过滤器:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto even = [](long i) { return 0 == i % 2; };
auto tview = vi | views::filter(even);
输出:
0 2 4 6 8
还包括一个 transform 视图,它与转换函数一起使用,用于转换结果:
vector<int> vi { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto even = [](int i) { return 0 == i % 2; };
auto x2 = [](auto i) { return i * 2; };
auto tview = vi | views::filter(even) | views::transform(x2);
输出:
0 4 8 12 16
库中还有很多有用的视图和视图适配器。请查看您最喜欢的参考网站,或 (https://j.bw.org/ranges) 以获取完整列表。
还有更多…
从 C++20 开始,<algorithm> 头文件中的大多数算法都包括了用于范围的版本。这些版本仍然在 <algorithm> 头文件中,但在 std::ranges 名称空间中。这使它们与旧算法区分开来。
这意味着,你可以用一个范围而不是两个迭代器来调用一个算法:
sort(v.begin(), v.end());
你现在可以这样调用它:
ranges::sort(v);
这当然更方便,但它真的有什么帮助吗?
考虑你想对向量的某部分进行排序的情况,你可以用旧方法这样做:
sort(v.begin() + 5, v.end());
这将对向量的前 5 个元素之后的元素进行排序。使用范围版本,你可以使用视图跳过前 5 个元素:
ranges::sort(views::drop(v, 5));
你甚至可以组合视图:
ranges::sort(views::drop(views::reverse(v), 5));
实际上,你可以甚至将范围适配器作为参数传递给 ranges::sort:
ranges::sort(v | views::reverse | views::drop(5));
与使用传统的 sort 算法和向量迭代器相比,虽然这肯定更短,也不是不可能理解,但我觉得范围适配器的版本直观得多。
你可以在 cppreference 网站(https://j.bw.org/algoranges)上找到已经约束为与范围一起工作的算法的完整列表。
在本文中,我们只是浅尝辄止地介绍了范围和视图。这个特性是十多年来许多不同团队工作的结晶,我预计它将根本改变我们在 STL 中使用容器的方式。
本文暂时没有评论,来添加一个吧(●'◡'●)