专业的编程技术博客社区

网站首页 > 博客文章 正文

C++ 20 创建容器视图与范围(c++ 容器模板)

baijin 2024-08-13 00:56:33 博客文章 7 ℃ 0 评论

创建容器视图的范围

新的范围库是 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 中使用容器的方式。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表