什么是范围(Ranges)?
Ranges 是 C++20 提供的一种更现代的处理集合的方式,可以看作是对迭代器的进一步抽象。
如果要筛选处vector中的偶数,可使用ranges:
1
2
3
4
5
6
7
8
9
10
11
12 | #include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
for (int n : v | std::views::filter([](int n){ return n % 2 == 0; })) {
std::cout << n << " ";
}
return 0;
}
|
其中,std::views::filter
是一个 Range 适配器,它直接对Range v
进行过滤操作。通过管道操作符 |
,我们将过滤器应用到range上。
在 C++20 之前,使用标准库算法时,需要显式地传递迭代器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | #include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
std::vector<int> result;
std::copy_if(v.begin(), v.end(), std::back_inserter(result), [](int n){ return n % 2 == 0; });
for (int n : result) {
std::cout << n << " ";
}
return 0;
}
|
在这个例子中,使用了 std::copy_if
来过滤 v
中的偶数,需要手动传递起始和结束迭代器。
Ranges 的核心组件
主要由以下三个核心组件:
- Range 表达式:它可以是一个范围(如容器)或任何可以产生范围的对象。
- Range 适配器(Views):允许对范围进行变换、过滤等操作。
- Range 算法:它是标准库中已有算法的扩展版本,直接作用于 Ranges。
Range 表达式
Range 表达式可以是任何可以被视作“范围”的对象,比如标准库容器std::vector、std::list 等。此外,C++20 还引入了 std::ranges::range 概念(concept),用于判定一个类型是否是range。
1
2
3
4
5
6
7
8
9
10
11
12 | #include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3};
if constexpr (std::ranges::range<decltype(v)>) {
std::cout << "v is a range" << std::endl;
}
return 0;
}
|
Range 适配器(Views)
Range 适配器(Views)是 Ranges 的关键部分,它们允许对范围进行变换、过滤等操作,而不会修改原始数据。一些的适配器:
- std::views::filter:过滤满足条件的元素。
- std::views::transform:对每个元素进行某种变换。
- std::views::take:获取前 N 个元素。
- std::views::drop:跳过前 N 个元素。
- ...
下面例子通过管道操作符 | 组合多个适配器——首先过滤出偶数,然后对这些偶数进行平方操作:
1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
for (int n : v | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; })) {
std::cout << n << " ";
}
return 0;
}
|
Range 算法
C++20 为标准库的算法添加了 Ranges 版本,这些算法直接作用于范围,而不再需要显式传递迭代器。常见的 Range 算法包括:
- std::ranges::find
- std::ranges::sort
- std::ranges::for_each
- std::ranges::copy
1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <vector>
#include <iostream>
#include <ranges>
int main() {
std::vector<int> v = {5, 3, 1, 4, 2};
std::ranges::sort(v);
for (int n : v) {
std::cout << n << " ";
}
return 0;
}
|
std::ranges::sort 直接作用于范围 v,无需显式传递 begin() 和 end() 迭代器。
Range 适配器的惰性计算
Ranges 的一个重要特性是 惰性计算。适配器不会立即执行操作,而是会在真正访问元素时才进行计算。
如下代码输出 0 1 2 3 4 5 6 7 8 9
:
| #include <iostream>
#include <ranges>
int main() {
auto r = std::views::iota(0) | std::views::take(10);
for (int n : r) {
std::cout << n << " ";
}
return 0;
}
|
例子中, std::views::iota(0) 生成了一个从 0 开始的无限整数序列,通过 std::views::take(10),我们只取了前 10 个元素。由于 Ranges 的惰性特性,程序并不会生成整个无限序列,而是按需生成并处理元素。
自定义 Range 适配器
C++20 允许我们定义自己的 Range 适配器。自定义适配器可以帮助我们实现特定的数据处理逻辑。下面是一个简单的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #include <ranges>
#include <iostream>
#include <concepts>
struct double_transform_t {
template<std::ranges::input_range R>
constexpr auto operator()(R&& r) const {
return std::views::transform(r, [](auto x) { return x * 2; });
}
friend constexpr auto operator|
(std::ranges::input_range auto&& r, double_transform_t d) { return d(r); }
} inline constexpr double_transform;
int main() {
auto r = std::views::iota(1) | std::views::take(5) | double_transform;
for (int i : r)
std::cout << i << " "; // output:2 4 6 8 10
std::cout << '\n';
return 0;
}
|
在这个例子中,double_transform
是一个简单的自定义 Range 适配器,它将范围中的每个元素都乘以 2。
尽管 view_base
是空类,据说从它派生会有好多好处:
| struct double_transform_t : std::ranges::view_base {
template<std::ranges::input_range R>
constexpr auto operator()(R&& r) const {
return std::views::transform(r, [](auto x) { return x * 2; });
}
} inline constexpr double_transform;
template<std::ranges::input_range R>
constexpr auto operator|(R&& r, double_transform_t) {
return double_transform(r);
}
|
参考
- https://zh.cppreference.com/w/cpp/ranges