四时宝库

程序员的知识宝库

C++高阶用法 - Range(数据处理带来了革命性变化) 详细使用指南

C++20 引入的 Range 库是 C++ 标准库的一个重大扩展,它提供了一种全新的处理数据集合的方式,它为 C++ 数据处理带来了革命性的变化。通过使用 Range 库,我们可以编写出更简洁、更可读、更高效的代码。它基于范围 (range) 的概念,并提供了一系列视图 (view)动作 (action)算法 (algorithm),用于操作和转换数据集合。

1. 什么是 Range?

在 Range 库中,Range 是一个表示元素序列的概念。一个 Range 可以是任何可以迭代的东西,例如:

  • 标准库容器(如 std::vector, std::list, std::array 等)
  • C 风格数组
  • 字符串
  • 迭代器对定义的序列
  • 生成器产生的序列

Range 的核心思想是将数据和操作分离。传统的 C++ 算法通常需要迭代器对来指定操作的范围,而 Range 库则直接操作 Range 对象,使得代码更加简洁和直观。

2. Range 库的核心组件

Range 库主要包含以下几个核心组件:

  • 视图 (View): 视图是 Range 库的核心概念,它是一种轻量级的 Range,可以对底层的 Range 进行转换和过滤,而无需复制数据。视图是惰性求值 (lazy evaluation) 的,这意味着视图的转换操作只会在需要元素时才执行。
  • 动作 (Action): 动作是对 Range 进行修改或产生副作用的操作。与视图不同,动作通常会返回一个新的 Range 或直接修改原有的 Range。
  • Range 算法 (Range Algorithm): Range 库提供了许多与 头文件中算法功能相似的 Range 版本算法。这些算法可以直接操作 Range 对象,而无需显式地使用迭代器。

3. View 的使用

视图是 Range 库中最常用的组件之一,它提供了丰富的 Range 转换功能。下面介绍一些常用的视图:

3.1 views::iota: 生成一个整数序列。

views::iota(start) 生成一个从 start 开始无限递增的整数序列。

views::iota(start, end) 生成一个从 start 开始到 end (不包含) 的整数序列。

 #include 
 #include 
 #include 
 
 int main() {
     // 生成从 0 到 9 的整数序列
     auto numbers = std::views::iota(0, 10);
     for (int number : numbers) {
         std::cout << number << " "; // 输出: 0 1 2 3 4 5 6 7 8 9
     }
     std::cout << std::endl;
 
     // 生成从 5 开始的无限整数序列,并取前 5 个
     auto infinite_numbers = std::views::iota(5) | std::views::take(5);
     for (int number : infinite_numbers) {
         std::cout << number << " "; // 输出: 5 6 7 8 9
     }
     std::cout << std::endl;
     return 0;
 }

3.2 views::filter: 过滤 Range 中的元素,只保留满足特定条件的元素。

views::filter(predicate) 接受一个谓词函数 predicate,返回一个新的视图,其中只包含原始 Range 中满足 predicate 条件的元素。

 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 过滤出偶数
     auto even_numbers = data | std::views::filter([](int n){ return n % 2 == 0; });
     for (int number : even_numbers) {
         std::cout << number << " "; // 输出: 2 4 6 8 10
     }
     std::cout << std::endl 5 auto odd_greater_than_5='data' std::views::filterint n return n 2 std::views::filterint n return n> 5; });
     for (int number : odd_greater_than_5) {
         std::cout << number << " "; // 输出: 7 9
     }
     std::cout << std::endl;
     return 0;
 }

3.3 views::transform: 转换 Range 中的元素。

views::transform(function) 接受一个函数 function,返回一个新的视图,其中每个元素都是原始 Range 中对应元素经过 function 转换后的结果。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5};
 
     // 将每个元素平方
     auto squared_numbers = data | std::views::transform([](int n){ return n * n; });
     for (int number : squared_numbers) {
         std::cout << number << " "; // 输出: 1 4 9 16 25
     }
     std::cout << std::endl;
 
     std::vector words = {"hello", "world", "cpp"};
 
     // 将每个字符串转换为大写
     auto uppercase_words = words | std::views::transform([](const std::string& s){
         std::string upper_s = s;
         for (char &c : upper_s) {
             c = std::toupper(c);
         }
         return upper_s;
     });
     for (const std::string& word : uppercase_words) {
         std::cout << word << " "; // 输出: HELLO WORLD CPP
     }
     std::cout << std::endl;
     return 0;
 }

3.4 views::take: 从 Range 的开头获取指定数量的元素。

views::take(count) 接受一个整数 count,返回一个新的视图,其中包含原始 Range 的前 count 个元素。

 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 获取前 5 个元素
     auto first_five = data | std::views::take(5);
     for (int number : first_five) {
         std::cout << number << " "; // 输出: 1 2 3 4 5
     }
     std::cout << std::endl;
     return 0;
 }

3.5 views::drop: 从 Range 的开头丢弃指定数量的元素,保留剩余的元素。

views::drop(count) 接受一个整数 count,返回一个新的视图,其中包含原始 Range 从第 count + 1 个元素开始到末尾的所有元素。

 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 丢弃前 3 个元素,保留剩余的元素
     auto remaining_elements = data | std::views::drop(3);
     for (int number : remaining_elements) {
         std::cout << number << " "; // 输出: 4 5 6 7 8 9 10
     }
     std::cout << std::endl;
     return 0;
 }

3.6 views::reverse: 反转 Range 中元素的顺序。

views::reverse 返回一个新的视图,其中元素的顺序与原始 Range 相反。

 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5};
 
     // 反转元素顺序
     auto reversed_data = data | std::views::reverse;
     for (int number : reversed_data) {
         std::cout << number << " "; // 输出: 5 4 3 2 1
     }
     std::cout << std::endl;
     return 0;
 }

3.7 views::common: 将 Range 转换为 common range。

Common range 是一种更严格的 Range 类型,它要求 begin 迭代器和 end 迭代器具有相同的类型。某些情况下,为了兼容旧代码或者某些算法的要求,可能需要将 Range 转换为 common range。views::common 可以将 Range 转换为 common range。

 #include 
 #include 
 #include 
 
 int main() {
     auto numbers = std::views::iota(0, 5); // numbers 通常不是 common range
 
     // 转换为 common range
     auto common_numbers = numbers | std::views::common;
 
     // 现在 common_numbers 是 common range,可以用于某些需要 common range 的场景
     for (int number : common_numbers) {
         std::cout << number << " "; // 输出: 0 1 2 3 4
     }
     std::cout << std::endl;
     return 0;
 }

3.8 更多 Views: Range 库还提供了许多其他的 View,例如:

  • views::join: 将 Range 的 Range 扁平化为一个 Range。
  • views::split: 将 Range 分割成子 Range。
  • views::elements: 从 Range 的元素中提取特定索引的元素(例如,提取 pair 的第一个元素或第二个元素)。
  • views::stride: 以步长跳跃的方式获取 Range 的元素。
  • views::zip: 将多个 Range 合并为一个 Range,元素为 tuple。
  • views::concat: 连接多个 Range。
  • views::adjacent_transform: 对相邻元素进行转换。
  • views::pairwise_transform: 对两个 Range 的对应元素进行转换。

你可以查阅 C++ 标准库文档以获取完整的 View 列表及其详细用法。

4. Action 的使用

Action 对 Range 进行修改或产生副作用。 与 View 不同,Action 通常会消耗 Range 并产生新的结果或者直接修改 Range 本身。下面介绍一些常用的 Action:

4.1 actions::sort: 对 Range 中的元素进行排序。

actions::sort 对 Range 中的元素进行原地排序(修改原始 Range)。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {5, 2, 8, 1, 9, 4, 7, 3, 6};
 
     // 使用 action::sort 对 data 进行排序
     auto sorted_data = data | std::ranges::actions::sort; //  注意这里是 std::ranges::actions::sort
     // actions::sort 返回 void, 它会直接修改 data
 
     for (int number : data) { // data 已经被排序
         std::cout << number << " "; // 输出: 1 2 3 4 5 6 7 8 9
     }
     std::cout << std::endl;
     return 0;
 }

4.2 actions::reverse: 反转 Range 中元素的顺序 (原地反转)。

actions::reverse 原地反转 Range 中的元素。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5};
 
     // 使用 action::reverse 反转 data
     data | std::ranges::actions::reverse; //  注意这里是 std::ranges::actions::reverse
     // actions::reverse 返回 void, 它会直接修改 data
 
     for (int number : data) { // data 已经被反转
         std::cout << number << " "; // 输出: 5 4 3 2 1
     }
     std::cout << std::endl;
     return 0;
 }

4.3 actions::unique: 移除 Range 中连续重复的元素 (原地去重)。

actions::unique 移除 Range 中连续重复的元素,并返回一个指向去重后 Range 逻辑结尾的迭代器。通常需要配合 erase 方法来真正移除容器中的元素。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5};
 
     // 使用 action::unique 去重
     auto end_iterator = data | std::ranges::actions::unique; // 返回去重后逻辑结尾的迭代器
 
     // 真正移除容器中多余的元素
     data.erase(end_iterator.begin(), data.end());
 
     for (int number : data) {
         std::cout << number << " "; // 输出: 1 2 3 4 5
     }
     std::cout << std::endl;
     return 0;
 }

4.4 actions::remove_if: 移除 Range 中满足特定条件的元素 (原地移除)。

actions::remove_if(predicate) 移除 Range 中满足谓词 predicate 的元素,并返回一个指向移除后 Range 逻辑结尾的迭代器。 同样需要配合 erase 来真正移除元素。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 移除偶数
     auto end_iterator = data | std::ranges::actions::remove_if([](int n){ return n % 2 == 0; });
     data.erase(end_iterator.begin(), data.end());
 
     for (int number : data) {
         std::cout << number << " "; // 输出: 1 3 5 7 9
     }
     std::cout << std::endl;
     return 0;
 }

4.5 更多 Actions: Range 库还提供了其他的 Action,例如:

  • actions::copy: 将 Range 的元素复制到另一个 Range 或容器。
  • actions::transform: 对 Range 的元素进行原地转换。

你可以查阅 C++ 标准库文档以获取完整的 Action 列表及其详细用法。

5. Range 算法的使用

Range 库提供了许多 Range 版本的算法,这些算法位于 std::ranges 命名空间下,例如 std::ranges::for_each, std::ranges::copy, std::ranges::transform, std::ranges::sort, std::ranges::find, std::ranges::count, std::ranges::min_element, std::ranges::max_element 等等。

Range 算法与传统的 中的算法功能相似,但它们可以直接操作 Range 对象,而无需显式地传递迭代器对。这使得代码更加简洁易读。

示例:使用 Range 算法进行查找、计数和遍历

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 2, 3, 2};
 
     // 查找第一个大于 3 的元素
     auto it = std::ranges::find_if(data, [](int n){ return n > 3; });
     if (it != data.end()) {
         std::cout << "找到第一个大于 3 的元素: " << *it << std::endl; // 输出: 找到第一个大于 3 的元素: 4
     }
 
     // 统计值为 2 的元素个数
     int count = std::ranges::count(data, 2);
     std::cout << "值为 2 的元素个数: " << count << std::endl; // 输出: 值为 2 的元素个数: 3
 
     // 遍历并打印每个元素
     std::cout << "遍历所有元素: ";
     std::ranges::for_each(data, [](int n){ std::cout << n << " "; }); // 输出: 遍历所有元素: 1 2 3 4 5 2 3 2
     std::cout << std::endl;
 
     return 0;
 }

示例:组合 View 和 Range 算法C++

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 1. 过滤出偶数
     // 2. 将偶数平方
     // 3. 取前 3 个平方数
     auto result_view = data
                        | std::views::filter([](int n){ return n % 2 == 0; })
                        | std::views::transform([](int n){ return n * n; })
                        | std::views::take(3);
 
     std::cout << "处理结果: ";
     std::ranges::for_each(result_view, [](int n){ std::cout << n << " "; }); // 输出: 处理结果: 4 16 36
     std::cout << std::endl;
 
     return 0;
 }

Range 的强大之处在于可以组合 View 和 Range 算法,构建复杂的数据处理流水线。

 #include 
 #include 
 #include 
 #include 
 
 int main() {
     std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
     // 1. 过滤出偶数
     // 2. 将偶数平方
     // 3. 取前 3 个平方数
     auto result_view = data
                        | std::views::filter([](int n){ return n % 2 == 0; })
                        | std::views::transform([](int n){ return n * n; })
                        | std::views::take(3);
 
     std::cout << "处理结果: ";
     std::ranges::for_each(result_view, [](int n){ std::cout << n << " "; }); // 输出: 处理结果: 4 16 36
     std::cout << std::endl;
 
     return 0;
 }

6. 自定义 Range 和 View (进阶)

除了使用标准库提供的 Range 和 View,你还可以创建自定义的 Range 和 View,以满足特定的需求。 这涉及到实现 Range concepts,例如 range, view, input_range, forward_range, bidirectional_range, random_access_range 等。 自定义 Range 和 View 属于比较高级的主题,这里只做简单介绍。

7. Range 的优势总结

  • 代码简洁性: Range 库减少了迭代器的显式使用,使得代码更加简洁易读。
  • 可组合性: View 和 Action 可以像管道一样组合起来,构建复杂的数据处理流水线,提高了代码的灵活性和可维护性。
  • 惰性求值: View 的惰性求值特性可以避免不必要的计算,提高效率,特别是在处理大型数据集时。
  • 更安全: Range 库减少了手动管理迭代器的需求,降低了出错的风险。
  • 与标准库算法的兼容性: Range 库提供了 Range 版本的算法,可以方便地与现有的标准库算法进行集成。

8. 注意事项

  • C++20 支持: Range 库是 C++20 标准引入的新特性,需要编译器支持 C++20 标准。
  • 性能考虑: 虽然 View 是惰性求值的,但在某些复杂的 Range 管道中,过度使用 View 可能会引入额外的开销。 需要根据实际情况进行性能评估和优化。
  • 学习曲线: 理解 Range 的概念和使用方式可能需要一定的学习成本,特别是对于刚接触 Range 库的开发者。



发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接