Rangers库
好的,我们来详细聊聊 C++20 引入的 Ranges 库。
Ranges 库是 C++20 中一个非常重要的特性,它彻底改变了我们在 C++ 中处理序列(集合、容器)的方式。它的核心思想是提供一种组合式 (composable)、声明式 (declarative)、惰性求值 (lazy evaluation) 的方式来操作数据序列,从而使得代码更简洁、更易读、更安全、更高效。
1. 为什么需要 Ranges 库?
在 C++20 之前,我们主要使用标准库算法(如 std::for_each
, std::transform
, std::sort
等)配合迭代器对来操作序列:
1 |
|
这段代码虽然可以工作,但存在一些问题:
- 冗长和重复: 每次操作都需要
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 |
|
可以看到,代码变得:
- 更简洁: 没有
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 |
|
5. Range Algorithms
std::ranges
命名空间下的算法是传统的 std::algorithm
的 Range-aware 版本。它们可以直接接受 Range 作为参数,而无需 begin()
和 end()
。
1 |
|
6. Ranges 库的优势总结
- 高可读性: 管道操作符
|
使得数据流向和操作顺序清晰明了。 - 简洁性: 减少了
begin()
,end()
, 临时变量和循环的样板代码。 - 惰性求值: 避免了不必要的中间数据结构和计算,提高了性能和内存效率。
- 组合性: 各种 View 可以灵活地组合,实现复杂的逻辑。
- 通用性: 适用于任何满足 Range 概念的序列,包括自定义容器。
- 安全性: 减少了迭代器错误和边界错误的可能性。
- 函数式编程风格: 鼓励声明式编程,更关注“做什么”而不是“怎么做”。
7. 注意事项
- 生命周期管理: View 是非拥有的,它只是底层数据的视图。确保底层数据在 View 的生命周期内是有效的,否则会导致悬空引用和未定义行为。
- 编译时间: 复杂的 Ranges 管道可能会增加编译时间,但通常可以接受。
- 学习曲线: 对于习惯了传统迭代器/算法的开发者来说,Ranges 库可能需要一些时间来适应其新的编程范式。
Ranges 库是 C++ 泛型编程演进的重要一步,它使得 C++ 能够以更现代、更富有表现力的方式处理数据序列。在 C++20 及以后的项目中,强烈推荐使用 Ranges 库来简化和优化你的代码。