【C++】Ranges:彻底改变STL编程方式

文章目录

  • [C++20 Ranges 全面详解](#C++20 Ranges 全面详解)
    • [一、为什么 C++20 要引入 Ranges?](#一、为什么 C++20 要引入 Ranges?)
    • [二、Ranges 核心基本概念](#二、Ranges 核心基本概念)
      • [1. 核心定义](#1. 核心定义)
      • [2. 核心概念检查示例](#2. 核心概念检查示例)
    • [三、Range 视图(Views)](#三、Range 视图(Views))
      • [1. 视图的核心特性](#1. 视图的核心特性)
      • [2. 常用视图及示例](#2. 常用视图及示例)
      • [3. 视图基础示例(逐行解析)](#3. 视图基础示例(逐行解析))
      • [4. 自定义视图(扩展)](#4. 自定义视图(扩展))
    • [四、Range 算法](#四、Range 算法)
      • [1. Range 算法的核心改进](#1. Range 算法的核心改进)
      • [2. 常用 Range 算法示例](#2. 常用 Range 算法示例)
    • [五、Ranges 综合应用样例](#五、Ranges 综合应用样例)
    • 六、总结

C++20 Ranges 全面详解

详细讲解 C++20 Ranges 的核心概念、视图、算法及综合应用,并对比没有 Ranges 之前的编程痛点,同时对示例代码进行逐段解析,帮助你深入理解这一 C++20 核心特性。

一、为什么 C++20 要引入 Ranges?

在 Ranges 出现之前,C++ 标准库的迭代器和算法体系存在诸多痛点,这也是 Ranges 被引入的核心原因:

1. 无 Ranges 时代的具体问题

(1)算法调用冗余且不直观

传统 STL 算法需要手动传递 begin()/end() 迭代器对,代码冗长且语义不连贯:

cpp 复制代码
#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8};
    
    // 找出第一个大于5的元素:必须手动传begin/end
    auto it = std::find(vec.begin(), vec.end(), 5);
    if (it != vec.end()) {
        std::cout << *it << std::endl;
    }
    
    // 过滤+转换:需要临时容器,多轮遍历
    std::vector<int> temp;
    // 第一步:过滤偶数
    std::copy_if(vec.begin(), vec.end(), std::back_inserter(temp),
                 [](int x) { return x % 2 == 0; });
    // 第二步:每个元素乘2
    std::vector<int> result;
    std::transform(temp.begin(), temp.end(), std::back_inserter(result),
                   [](int x) { return x * 2; });
    
    return 0;
}
(2)无法组合操作,性能/内存开销大

传统算法是"一次性"的,多个操作需要借助临时容器存储中间结果,既浪费内存,又增加多次遍历的开销(如上例中的 temp 容器)。

(3)迭代器与容器强耦合

算法只能接收迭代器对,无法直接操作"可遍历的对象"(如生成器、无限序列),扩展性差。

(4)错误处理不友好

若传递的 begin()/end() 不属于同一个容器,或迭代器类型不匹配,只能在运行时崩溃,无法在编译期检测。

2. Ranges 的核心价值

  • 简化语法 :直接操作容器/序列,无需手动传递 begin()/end()
  • 惰性求值:视图(View)仅在需要时计算,无中间容器开销
  • 可组合性:多个 Range 操作可以链式调用,语义更连贯
  • 编译期安全:迭代器不匹配等问题在编译期暴露
  • 扩展性:支持自定义 Range 类型,适配更多数据源

二、Ranges 核心基本概念

1. 核心定义

  • Range(范围) :任何可以提供迭代器对(begin()/end())的对象,满足 std::ranges::range 概念。
    常见类型:std::vectorstd::stringstd::array、原生数组、视图(View)等。
  • Iterator(迭代器) :Ranges 重新定义了迭代器概念层级(比传统迭代器更精细),如:
    • input_iterator:可读,单向遍历
    • forward_iterator:可读,双向遍历
    • random_access_iterator:随机访问
    • contiguous_iterator:连续内存迭代器
  • Sentinel(哨位) :替代传统 end() 迭代器,可以是不同类型(如数值、特殊标记),只要能判断迭代器是否到达末尾。

2. 核心概念检查示例

cpp 复制代码
#include <vector>
#include <ranges>
#include <iostream>

int main() {
    std::vector<int> vec = {1,2,3};
    int arr[] = {4,5,6};
    
    // 检查是否是Range
    std::cout << std::boolalpha;
    std::cout << "vector is range: " << std::ranges::range<decltype(vec)> << std::endl; // true
    std::cout << "array is range: " << std::ranges::range<decltype(arr)> << std::endl;   // true
    std::cout << "int is range: " << std::ranges::range<int> << std::endl;               // false
    
    // 检查迭代器类型
    using VecIt = decltype(vec.begin());
    std::cout << "vector iterator is random access: " 
              << std::ranges::random_access_iterator<VecIt> << std::endl; // true
    
    return 0;
}

代码解释

  • std::ranges::range<T> 是一个概念(Concept),用于判断类型 T 是否是合法的 Range;
  • 容器/数组满足 Range 概念,基础类型(如 int)不满足;
  • std::ranges::random_access_iterator 检查迭代器是否支持随机访问(如 vector 的迭代器支持,list 的迭代器不支持)。

三、Range 视图(Views)

1. 视图的核心特性

  • 惰性求值:视图不存储数据,仅记录"如何转换原 Range",只有在遍历/访问元素时才计算;
  • 轻量级:视图对象本身占用极少内存(通常是几个指针/参数);
  • 可组合:多个视图可以链式调用,形成复杂的转换逻辑;
  • 不可变(通常):视图本身不修改原数据,仅提供转换后的"视图"。

2. 常用视图及示例

C++20 提供了丰富的视图(位于 <ranges> 头文件),核心视图如下:

视图类型 作用 示例
views::filter 过滤元素 保留偶数:`vec
views::transform 转换元素 元素乘2:`vec
views::take 取前N个元素 取前3个:`vec
views::drop 跳过前N个元素 跳过前2个:`vec
views::reverse 反转序列 反转:`vec
views::iota 生成无限/有限整数序列 生成1-10:views::iota(1, 11)
views::join 扁平化嵌套Range 扁平化二维数组:`vec2d
views::slice 切片(C++23) 取[2,5)区间:`vec

3. 视图基础示例(逐行解析)

cpp 复制代码
#include <vector>
#include <ranges>
#include <iostream>

int main() {
    // 原始数据
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 组合视图:过滤偶数 → 乘2 → 取前3个 → 反转
    auto processed = nums 
        | std::views::filter([](int x) { return x % 2 == 0; })  // 过滤偶数:2,4,6,8,10
        | std::views::transform([](int x) { return x * 2; })    // 乘2:4,8,12,16,20
        | std::views::take(3)                                   // 取前3个:4,8,12
        | std::views::reverse;                                  // 反转:12,8,4
    
    // 遍历视图(此时才真正计算)
    std::cout << "处理结果:";
    for (int x : processed) {
        std::cout << x << " ";  // 输出:12 8 4
    }
    std::cout << std::endl;
    
    // 原始数据未被修改
    std::cout << "原始数据:";
    for (int x : nums) {
        std::cout << x << " ";  // 输出:1 2 3 ... 10
    }
    std::cout << std::endl;
    
    // 无限序列视图(views::iota)
    auto infinite_seq = std::views::iota(1); // 生成1,2,3,...无限序列
    std::cout << "无限序列前5个:";
    for (int x : infinite_seq | std::views::take(5)) {
        std::cout << x << " ";  // 输出:1 2 3 4 5
    }
    std::cout << std::endl;
    
    return 0;
}

代码关键解析

  • 管道运算符 | :Ranges 重载了 |,用于将 Range 传递给视图/算法,语义类似 Unix 管道,更符合阅读习惯;
  • 惰性求值验证processed 视图定义时不会执行任何计算,只有在 for 循环遍历它时,才会逐元素执行过滤→转换→取前3→反转;
  • 原数据不可变 :所有视图操作都是"只读"的,不会修改原始 nums 容器;
  • 无限序列views::iota(1) 生成无限序列,但结合 views::take(5) 后仅取前5个,避免无限循环。

4. 自定义视图(扩展)

你可以通过 std::ranges::view_interface 实现自定义视图,示例如下(生成斐波那契数列):

cpp 复制代码
#include <ranges>
#include <iostream>

// 自定义斐波那契视图
class FibView : public std::ranges::view_interface<FibView> {
private:
    size_t max_size_; // 最大元素个数
public:
    // 迭代器类型
    struct iterator {
        using iterator_category = std::input_iterator_tag;
        using value_type = unsigned long long;
        using difference_type = std::ptrdiff_t;
        using pointer = value_type*;
        using reference = value_type&;

        size_t idx_ = 0;
        value_type a_ = 0, b_ = 1;

        // 解引用
        value_type operator*() const { 
            return (idx_ == 0) ? 0 : (idx_ == 1) ? 1 : a_ + b_; 
        }

        // 自增
        iterator& operator++() {
            if (idx_ >= 2) {
                auto temp = a_ + b_;
                a_ = b_;
                b_ = temp;
            }
            idx_++;
            return *this;
        }

        // 相等判断(哨位)
        bool operator==(const iterator& other) const {
            return idx_ == other.idx_;
        }
    };

    // 构造函数
    explicit FibView(size_t max_size) : max_size_(max_size) {}

    // 开始迭代器
    iterator begin() const { return iterator{}; }

    // 结束迭代器(哨位)
    iterator end() const { 
        iterator it;
        it.idx_ = max_size_;
        return it;
    }
};

int main() {
    // 使用自定义斐波那契视图
    FibView fib(10); // 生成前10个斐波那契数
    std::cout << "斐波那契前10项:";
    for (auto x : fib) {
        std::cout << x << " "; // 输出:0 1 1 2 3 5 8 13 21 34
    }
    std::cout << std::endl;
    
    // 组合内置视图:取偶数项
    auto even_fib = fib | std::views::filter([](auto x) { return x % 2 == 0; });
    std::cout << "偶数项:";
    for (auto x : even_fib) {
        std::cout << x << " "; // 输出:0 2 8 34
    }
    std::cout << std::endl;
    
    return 0;
}

核心要点

  • 自定义视图需继承 std::ranges::view_interface,简化迭代器接口实现;
  • 必须实现 begin()/end() 方法,返回符合迭代器概念的对象;
  • 自定义视图可与内置视图无缝组合。

四、Range 算法

1. Range 算法的核心改进

C++20 为 <algorithm> 提供了 Ranges 版本(位于 std::ranges 命名空间),相比传统算法:

  • 直接接收 Range 对象,无需手动传 begin()/end()
  • 支持返回更有用的结果(如 std::ranges::find 返回迭代器,而传统 std::find 也返回迭代器,但 Range 版本编译期更安全);
  • 与视图无缝配合;
  • 支持更灵活的投影(Projection)参数,简化元素访问。

2. 常用 Range 算法示例

cpp 复制代码
#include <vector>
#include <ranges>
#include <algorithm>
#include <iostream>
#include <string>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
    std::vector<Person> people = {
        {"Alice", 25},
        {"Bob", 30},
        {"Charlie", 20}
    };
    
    // 1. 排序(Range版本)
    std::ranges::sort(nums); // 无需传begin/end
    std::cout << "排序后nums:";
    for (int x : nums) std::cout << x << " "; // 1 1 2 3 4 5 6 9
    std::cout << std::endl;
    
    // 2. 查找(Range版本)
    auto it = std::ranges::find(nums, 5);
    if (it != nums.end()) {
        std::cout << "找到5,索引:" << std::ranges::distance(nums.begin(), it) << std::endl; // 索引5
    }
    
    // 3. 投影(Projection):按Person的age排序
    // 投影参数:指定按哪个成员/函数排序,无需手动写lambda
    std::ranges::sort(people, std::ranges::less{}, &Person::age);
    std::cout << "按年龄排序的人员:\n";
    for (const auto& p : people) {
        std::cout << p.name << " (" << p.age << ")\n"; // Charlie(20) → Alice(25) → Bob(30)
    }
    
    // 4. 算法+视图组合:找出大于5的元素并计数
    auto count = std::ranges::count_if(nums | std::views::filter([](int x){return x>0;}), 
                                       [](int x) { return x > 5; });
    std::cout << "大于5的元素个数:" << count << std::endl; // 3(6,9)
    
    // 5. 生成(generate)
    std::vector<int> vec(5);
    std::ranges::generate(vec, []() { static int x=0; return x++; }); // 生成0,1,2,3,4
    std::cout << "generate结果:";
    for (int x : vec) std::cout << x << " ";
    std::cout << std::endl;
    
    return 0;
}

代码关键解析

  • 投影参数std::ranges::sort(people, std::ranges::less{}, &Person::age) 中的第三个参数是"投影",表示按 Person::age 成员排序,替代了传统的 [](const auto& a, const auto& b){return a.age < b.age;},简化代码;
  • 算法+视图nums | views::filter(...) 生成的视图可直接作为算法的输入,无需临时容器;
  • 安全性 :Range 算法会在编译期检查 Range 的迭代器是否满足算法要求(如 sort 需要随机访问迭代器,若传入 std::list 会编译报错,而传统 std::sort 仅在运行时崩溃)。

五、Ranges 综合应用样例

场景:数据处理流水线

需求:

  1. 读取一个整数列表;
  2. 过滤掉负数;
  3. 转换为平方值;
  4. 按降序排序;
  5. 取前5个元素;
  6. 计算这些元素的总和;
  7. 输出结果及原始数据。
cpp 复制代码
#include <vector>
#include <ranges>
#include <algorithm>
#include <numeric>
#include <iostream>
#include <random>

// 生成随机整数(辅助函数)
std::vector<int> generate_random_nums(int count, int min_val, int max_val) {
    std::vector<int> nums(count);
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(min_val, max_val);
    std::ranges::generate(nums, [&]() { return dist(gen); });
    return nums;
}

int main() {
    // 步骤1:生成随机数据(-10到20之间的20个整数)
    auto raw_nums = generate_random_nums(20, -10, 20);
    std::cout << "原始数据:";
    for (int x : raw_nums) std::cout << x << " ";
    std::cout << "\n" << std::endl;
    
    // 步骤2-5:构建数据处理流水线(惰性求值)
    auto processed = raw_nums
        | std::views::filter([](int x) { return x >= 0; })          // 过滤负数
        | std::views::transform([](int x) { return x * x; })        // 平方
        | std::views::sort(std::ranges::greater{})                  // 降序排序
        | std::views::take(5);                                      // 取前5个
    
    // 步骤6:计算总和(触发求值)
    // 注意:std::accumulate暂无Range版本,需手动传begin/end
    auto sum = std::accumulate(processed.begin(), processed.end(), 0);
    
    // 步骤7:输出结果
    std::cout << "处理后前5个平方值(降序):";
    for (int x : processed) std::cout << x << " ";
    std::cout << "\n总和:" << sum << std::endl;
    
    // 扩展:统计原始数据中正数的个数
    auto positive_count = std::ranges::count_if(raw_nums, [](int x) { return x > 0; });
    std::cout << "原始数据中正数个数:" << positive_count << std::endl;
    
    return 0;
}

输出示例

复制代码
原始数据:-5 12 8 -3 18 7 -9 15 4 11 -2 9 19 6 -7 10 1 17 -4 13 

处理后前5个平方值(降序):361 324 289 225 169 
总和:1368
原始数据中正数个数:14

核心解析

  • 流水线设计 :所有转换操作通过 | 链式调用,语义清晰,符合"数据流式处理"的思维;
  • 惰性求值processed 视图在定义时不计算,仅在遍历(for 循环)和 accumulate 时才执行实际计算;
  • 无临时容器:整个处理过程无需创建任何中间容器,内存开销极小;
  • 代码简洁性:相比传统 STL 实现(需要多个临时容器、多次遍历),Ranges 版本代码量减少50%以上,可读性大幅提升。

六、总结

核心要点回顾

  1. Ranges 的诞生背景:解决传统 STL 算法需要手动传迭代器、无法组合操作、中间容器开销大等痛点,提供更直观、高效的序列处理方式;
  2. 核心概念:Range 是可遍历的序列,视图(View)是轻量级、惰性求值的 Range 转换,Range 算法直接操作 Range 且更安全;
  3. 关键特性
    • 视图的惰性求值:仅在访问时计算,无中间容器开销;
    • 操作的可组合性 :通过 | 链式调用形成处理流水线;
    • 编译期安全性:迭代器不匹配等问题提前暴露;
    • 投影参数:简化按成员/函数的排序、查找等操作;
  4. 应用价值:大幅简化序列处理代码,提升可读性和性能,支持自定义扩展(如自定义视图)。

扩展建议

  • C++23 对 Ranges 进行了扩展(如 views::zipviews::slice),可关注后续标准;
  • 实际项目中,优先使用 Range 算法替代传统 STL 算法,减少迭代器传递错误;
  • 视图适合高频次、多步骤的序列转换场景,避免临时容器的内存/性能开销。

通过 Ranges,C++ 终于拥有了现代化的序列处理能力,其设计理念与 Rust 的迭代器、Python 的生成器异曲同工,是 C++20 最具价值的特性之一。

相关推荐
云游云记2 小时前
php 随机红包数生成
开发语言·php·随机红包
程序员林北北2 小时前
【前端进阶之旅】JavaScript 一些常用的简写技巧
开发语言·前端·javascript
Polaris北2 小时前
第二十三天打卡
c++
gAlAxy...3 小时前
MyBatis-Plus 核心 CRUD 操作全解析:BaseMapper 与通用 Service 实战
java·开发语言·mybatis
开开心心就好3 小时前
一键加密隐藏视频,专属格式播放工具
java·linux·开发语言·网络·人工智能·macos
CUC-MenG3 小时前
Codeforces Round 1079 (Div. 2)A,B,C,D,E1,E2,F个人题解
c语言·开发语言·数学·算法
阿里嘎多学长3 小时前
2026-02-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Anastasiozzzz4 小时前
Java异步编程:CompletableFuture从入门到底层实现
java·开发语言
九.九4 小时前
高性能算子库 ops-nn 的底层架构:从调度到指令的极致优化
开发语言