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 库来简化和优化你的代码。


