好的,我们来详细聊聊 C++20 引入的 Ranges 库

Ranges 库是 C++20 中一个非常重要的特性,它彻底改变了我们在 C++ 中处理序列(集合、容器)的方式。它的核心思想是提供一种组合式 (composable)声明式 (declarative)惰性求值 (lazy evaluation) 的方式来操作数据序列,从而使得代码更简洁、更易读、更安全、更高效。

1. 为什么需要 Ranges 库?

在 C++20 之前,我们主要使用标准库算法(如 std::for_each, std::transform, std::sort 等)配合迭代器对来操作序列:

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
32
#include <vector>
#include <algorithm> // for std::transform, std::for_each
#include <iostream>
#include <numeric> // for std::iota

int main() {
std::vector<int> nums(10);
std::iota(nums.begin(), nums.end(), 1); // nums: {1, 2, ..., 10}

std::vector<int> squares;
squares.reserve(nums.size());

// 1. 转换:计算平方
std::transform(nums.begin(), nums.end(),
std::back_inserter(squares), // 输出迭代器
[](int n) { return n * n; });

// 2. 过滤:只保留偶数
std::vector<int> even_squares;
for (int s : squares) {
if (s % 2 == 0) {
even_squares.push_back(s);
}
}

// 3. 打印
std::for_each(even_squares.begin(), even_squares.end(),
[](int n) { std::cout << n << " "; });
std::cout << std::endl; // 4 16 36 64 100

return 0;
}

这段代码虽然可以工作,但存在一些问题:

  • 冗长和重复: 每次操作都需要 begin()end() 迭代器,并且需要显式地创建中间容器(squares, even_squares)。
  • 非组合式: 多个操作链式调用时,需要嵌套或使用临时变量,可读性差。
  • 即时求值: 每个算法都会立即执行并生成新的数据,可能导致不必要的内存分配和计算。
  • 迭代器/哨兵对的复杂性: 对于初学者来说,迭代器对的概念有时比较抽象,容易出错。

Ranges 库旨在解决这些痛点,提供一种更现代、更函数式、更 C++ 的方式来处理序列。

2. Ranges 库的核心概念

Ranges 库主要由以下几个核心概念组成:

2.1 Range (范围)

一个 Range 是一个可以被迭代器对 [begin(), end()) 访问的序列。简单来说,任何可以用于范围 for 循环的东西都是一个 Range。
标准库容器(std::vector, std::list, std::array)、C 风格数组、std::string 等都是 Range。

2.2 View (视图)

View 是 Ranges 库的魔法所在。它是一个轻量级非拥有 (non-owning) 的 Range,它通过惰性求值的方式对底层数据进行转换、过滤等操作。View 本身不存储数据,它只是提供了对底层数据的“视图”或“投影”。

View 的关键特性:

  • 惰性求值: 只有当真正需要访问元素时,View 才会执行其转换逻辑。
  • 非拥有: View 不管理底层数据的生命周期。
  • O(1) 复制/移动: 复制或移动 View 对象非常廉价,因为它只包含少量状态(通常是迭代器)。
  • 可组合性: View 可以通过管道操作符 | 像 Unix 管道一样链式组合起来。

2.3 Range Adaptor (范围适配器)

Range Adaptor 是用于创建 View 的函数对象。它们接受一个 Range 作为输入,并返回一个新的 View。
例如:std::views::filter, std::views::transform, std::views::take, std::views::drop, std::views::reverse 等。

2.4 Range Algorithm (范围算法)

Range 算法是标准库算法的“Range-aware”版本。它们接受一个 Range 作为参数,而不是迭代器对。
例如:std::ranges::sort, std::ranges::find, std::ranges::for_each 等。

3. 如何使用 Ranges 库?

使用 Ranges 库,上面的例子可以改写成这样:

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
32
33
34
35
36
#include <vector>
#include <iostream>
#include <numeric>
#include <ranges> // 引入 Ranges 库的主要头文件

int main() {
std::vector<int> nums(10);
std::iota(nums.begin(), nums.end(), 1); // nums: {1, 2, ..., 10}

// 使用 Ranges 库进行链式操作
auto result_view = nums
| std::views::transform([](int n) { return n * n; }) // 计算平方
| std::views::filter([](int n) { return n % 2 == 0; }); // 过滤偶数

// 惰性求值:此时 result_view 还没有真正计算任何东西
// 只有在迭代时才会执行计算

std::cout << "Even squares: ";
for (int val : result_view) { // 遍历 View,触发计算
std::cout << val << " ";
}
std::cout << std::endl; // 4 16 36 64 100

// 你也可以将 View 转换为实际的容器
std::vector<int> final_vec(result_view.begin(), result_view.end());
// 或者更简洁 (C++23)
// std::vector<int> final_vec = result_view | std::ranges::to<std::vector>();

std::cout << "Final vector: ";
for (int val : final_vec) {
std::cout << val << " ";
}
std::cout << std::endl; // 4 16 36 64 100

return 0;
}

可以看到,代码变得:

  • 更简洁: 没有 begin(), end(), back_inserter 和中间容器。
  • 更可读: 管道操作符 | 使得操作流程一目了然,从左到右依次应用。
  • 更高效: 避免了不必要的中间容器分配和数据复制。

4. 常见的 Range Adaptors

std::views 命名空间下提供了大量的 Range Adaptors:

  • 转换 (Transformation):

    • std::views::transform(func): 对每个元素应用函数。
    • std::views::elements<N>() (C++23): 从元组或结构体中提取第 N 个元素。
    • std::views::keys(), std::views::values() (C++23): 从 std::map 等中提取键或值。
  • 过滤 (Filtering):

    • std::views::filter(pred): 只保留满足谓词的元素。
    • std::views::take_while(pred): 从开头取满足谓词的元素,直到不满足为止。
    • std::views::drop_while(pred): 从开头丢弃满足谓词的元素,直到不满足为止。
    • std::views::take(n): 取前 n 个元素。
    • std::views::drop(n): 丢弃前 n 个元素。
  • 组合 (Composition):

    • std::views::join: 将一个 Range of Ranges 扁平化为一个 Range。
    • std::views::zip (C++23): 将多个 Range 的元素按索引组合成元组。
    • std::views::cartesian_product (C++23): 计算多个 Range 的笛卡尔积。
  • 生成 (Generation):

    • std::views::iota(start, end): 生成一个整数序列。
    • std::views::repeat(val, count) (C++23): 重复生成一个值。
    • std::views::empty<T>(): 生成一个空的 Range。
  • 其他 (Miscellaneous):

    • std::views::reverse: 反转 Range。
    • std::views::counted(it, n): 从迭代器 it 开始,取 n 个元素。
    • std::views::common: 将 Range 转换为具有相同迭代器和哨兵类型的 Range。
    • std::views::split (C++23): 按分隔符分割 Range。
    • std::views::slide (C++23): 创建滑动窗口。

示例:更多 Range Adaptors

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <vector>
#include <string>
#include <ranges>
#include <map>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 1. 过滤偶数,取前3个,然后平方
std::cout << "Filtered, taken, squared: ";
for (int val : numbers
| std::views::filter([](int n) { return n % 2 == 0; }) // 2, 4, 6, 8, 10
| std::views::take(3) // 2, 4, 6
| std::views::transform([](int n) { return n * n; })) { // 4, 16, 36
std::cout << val << " ";
}
std::cout << std::endl; // Output: 4 16 36

// 2. 反转并跳过前3个
std::cout << "Reversed, dropped: ";
for (int val : numbers
| std::views::reverse // 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
| std::views::drop(3)) { // 7, 6, 5, 4, 3, 2, 1
std::cout << val << " ";
}
std::cout << std::endl; // Output: 7 6 5 4 3 2 1

// 3. 生成序列 (iota)
std::cout << "Iota view: ";
for (int val : std::views::iota(10, 15)) { // 10, 11, 12, 13, 14
std::cout << val << " ";
}
std::cout << std::endl; // Output: 10 11 12 13 14

// 4. 字符串分割 (C++23)
// std::string sentence = "hello world how are you";
// std::cout << "Split view: ";
// for (auto word_view : sentence | std::views::split(' ')) {
// std::cout << std::string(word_view.begin(), word_view.end()) << " | ";
// }
// std::cout << std::endl; // Output: hello | world | how | are | you |

// 5. 使用 std::views::zip (C++23)
// std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
// std::vector<int> ages = {30, 25, 35};
// std::cout << "Zipped view: ";
// for (auto [name, age] : std::views::zip(names, ages)) {
// std::cout << "(" << name << ", " << age << ") ";
// }
// std::cout << std::endl; // Output: (Alice, 30) (Bob, 25) (Charlie, 35)

// 6. 使用 std::views::keys / std::views::values (C++23)
// std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 92}};
// std::cout << "Map keys: ";
// for (const auto& key : scores | std::views::keys) {
// std::cout << key << " ";
// }
// std::cout << std::endl; // Output: Alice Bob Charlie
// std::cout << "Map values: ";
// for (int score : scores | std::views::values) {
// std::cout << score << " ";
// }
// std::cout << std::endl; // Output: 90 85 92

return 0;
}

5. Range Algorithms

std::ranges 命名空间下的算法是传统的 std::algorithm 的 Range-aware 版本。它们可以直接接受 Range 作为参数,而无需 begin()end()

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
32
33
34
35
36
37
38
39
#include <iostream>
#include <vector>
#include <ranges> // for ranges::sort, ranges::find
#include <algorithm> // for ranges::sort, ranges::find
#include <numeric> // for std::iota

int main() {
std::vector<int> data = {5, 2, 8, 1, 9, 4};

// 排序整个 vector
std::ranges::sort(data);
std::cout << "Sorted data: ";
for (int x : data) {
std::cout << x << " ";
}
std::cout << std::endl; // Output: 1 2 4 5 8 9

// 查找元素
auto it = std::ranges::find(data, 8);
if (it != data.end()) {
std::cout << "Found 8 at index: " << std::distance(data.begin(), it) << std::endl;
}

// 结合 View 和 Algorithm
std::vector<int> numbers(10);
std::iota(numbers.begin(), numbers.end(), 1); // 1, 2, ..., 10

// 找到第一个大于 5 的偶数
auto result_it = std::ranges::find_if(
numbers | std::views::filter([](int n) { return n % 2 == 0; }), // 2, 4, 6, 8, 10
[](int n) { return n > 5; } // 找到 6
);

if (result_it != std::end(numbers | std::views::filter([](int n) { return n % 2 == 0; }))) {
std::cout << "First even number greater than 5: " << *result_it << std::endl; // Output: 6
}

return 0;
}

6. Ranges 库的优势总结

  • 高可读性: 管道操作符 | 使得数据流向和操作顺序清晰明了。
  • 简洁性: 减少了 begin(), end(), 临时变量和循环的样板代码。
  • 惰性求值: 避免了不必要的中间数据结构和计算,提高了性能和内存效率。
  • 组合性: 各种 View 可以灵活地组合,实现复杂的逻辑。
  • 通用性: 适用于任何满足 Range 概念的序列,包括自定义容器。
  • 安全性: 减少了迭代器错误和边界错误的可能性。
  • 函数式编程风格: 鼓励声明式编程,更关注“做什么”而不是“怎么做”。

7. 注意事项

  • 生命周期管理: View 是非拥有的,它只是底层数据的视图。确保底层数据在 View 的生命周期内是有效的,否则会导致悬空引用和未定义行为。
  • 编译时间: 复杂的 Ranges 管道可能会增加编译时间,但通常可以接受。
  • 学习曲线: 对于习惯了传统迭代器/算法的开发者来说,Ranges 库可能需要一些时间来适应其新的编程范式。

Ranges 库是 C++ 泛型编程演进的重要一步,它使得 C++ 能够以更现代、更富有表现力的方式处理数据序列。在 C++20 及以后的项目中,强烈推荐使用 Ranges 库来简化和优化你的代码。