编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
        • 1. 案例引入
          • 1.1 STL 算法的迭代器对地狱——10 个中间变量只是为了传 begin/end
          • 1.2 filter + transform 的两次遍历——非惰性求值的性能陷阱
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 range 的概念层级——从 inputrange 到 contiguousrange
          • 2.2 为何这么切
        • 3. Views——惰性求值的核心
          • 3.1 view 的编译器魔法——不拥有数据、O(1) 拷贝、惰性求值
          • 3.2 常用 view 适配器——filter / transform / take / drop / reverse
          • 3.3 惰性求值 vs 急切求值——view 在整个管道中只遍历一次
          • 3.4 为什么 view 的迭代器需要特殊处理——sentinel 替代 end iterator
        • 4. 管道操作符 | ——从函数嵌套到从左到右的数据流
          • 4.1 | 的本质——operator| 重载与适配器的链式组合
          • 4.2 管道 vs 传统 STL 算法链——可读性的量变引起质变
          • 4.3 管道中的类型推演——每一步 view 的完整类型签名
        • 5. Ranges 算法——超越迭代器对的 std::ranges 命名空间
          • 5.1 std::ranges::sort 和 std::sort 的区别——range 参数 vs 迭代器对
          • 5.2 算法返回值的增强——不仅返回迭代器、还返回 subrange
          • 5.3 投影(projection)——把「比什么」从比较器中解放出来
          • 5.4 projection 的汇编零开销——编译器内联整个 lambda 链
        • 6. 组合 view 适配器——构建数据处理管道
          • 6.1 经典六步管道——filter → transform → take → reverse → filter → join
          • 6.2 管道的惰性求值模型——每个元素独立经过整个管道才产下一个
          • 6.3 与 Unix 管道的哲学同构——数据流从左到右、每步独立变换
        • 7. 自定义 view 与适配器——扩展 ranges 生态
          • 7.1 用 views::transform 函数对象替代手写 lambda
          • 7.2 C++23 新增的适配器——丰富 ranges 工具箱
        • 8. Ranges 的性能——汇编层验证零开销
          • 8.1 filter|transform 管道 vs 手写 for 循环——汇编对比
          • 8.2 ranges::sort + projection vs 手写 lambda——性能差异
          • 8.3 惰性求值的缓存友好性——单次遍历 vs 多次中间容器分配
        • 9. 常见陷阱与反模式
          • 9.1 view 在源容器析构后使用——悬挂引用
          • 9.2 试图给 view 做 sort——view 不拥有数据、不可修改
          • 9.3 filter_view 的迭代器递增——可能多次 ++ 才找到一个有效元素
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次管道求值的完整旅程——从容器到输出
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

Ranges革命与管道

# 57.Ranges革命与管道

# 目录介绍

  • 1. 案例引入
    • 1.1 STL 算法的迭代器对地狱——10 个中间变量只是为了传 begin/end
    • 1.2 filter + transform 的两次遍历——非惰性求值的性能陷阱
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 range 的概念层级——从 input_range 到 contiguous_range
    • 2.2 为何这么切
  • 3. Views——惰性求值的核心
    • 3.1 view 的编译器魔法——不拥有数据、O(1) 拷贝、惰性求值
    • 3.2 常用 view 适配器——filter / transform / take / drop / reverse
    • 3.3 惰性求值 vs 急切求值——view 在整个管道中只遍历一次
    • 3.4 为什么 view 的迭代器需要特殊处理——sentinel 替代 end iterator
  • 4. 管道操作符 | ——从函数嵌套到从左到右的数据流
    • 4.1 | 的本质——operator| 重载与适配器的链式组合
    • 4.2 管道 vs 传统 STL 算法链——可读性的量变引起质变
    • 4.3 管道中的类型推演——每一步 view 的完整类型签名
  • 5. Ranges 算法——超越迭代器对的 std::ranges 命名空间
    • 5.1 std::ranges::sort 和 std::sort 的区别——range 参数 vs 迭代器对
    • 5.2 算法返回值的增强——不仅返回迭代器、还返回 subrange
    • 5.3 投影(projection)——把「比什么」从比较器中解放出来
    • 5.4 projection 的汇编零开销——编译器内联整个 lambda 链
  • 6. 组合 view 适配器——构建数据处理管道
    • 6.1 经典六步管道——filter → transform → take → reverse → filter → join
    • 6.2 管道的惰性求值模型——每个元素独立经过整个管道才产下一个
    • 6.3 与 Unix 管道的哲学同构——数据流从左到右、每步独立变换
  • 7. 自定义 view 与适配器——扩展 ranges 生态
    • 7.1 用 views::transform 函数对象替代手写 lambda
    • 7.2 C++23 新增的适配器——zip / adjacent / stride / slide / chunk
  • 8. Ranges 的性能——汇编层验证零开销
    • 8.1 filter|transform 管道 vs 手写 for 循环——汇编对比
    • 8.2 ranges::sort + projection vs 手写 lambda——性能差异
    • 8.3 惰性求值的缓存友好性——单次遍历 vs 多次中间容器分配
  • 9. 常见陷阱与反模式
    • 9.1 view 在源容器析构后使用——悬挂引用
    • 9.2 试图给 view 做 sort——view 不拥有数据、不可修改
    • 9.3 filter_view 的迭代器递增——可能多次 ++ 才找到一个有效元素
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次管道求值的完整旅程——从容器到输出
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 STL 算法的迭代器对地狱——10 个中间变量只是为了传 begin/end

某数据处理模块用传统 STL 算法做数据清洗——代码散落在 40 行中——中间临时容器就有 5 个:

// ====== 事故代码 V1:迭代器对地狱 ======
std::vector<int> data = read_data();

// 过滤:只保留偶数
std::vector<int> filtered;
std::copy_if(data.begin(), data.end(), std::back_inserter(filtered),
             [](int x) { return x % 2 == 0; });

// 变换:平方
std::vector<int> transformed;
std::transform(filtered.begin(), filtered.end(), std::back_inserter(transformed),
               [](int x) { return x * x; });

// 取前 5 个
std::vector<int> taken(transformed.begin(),
                       transformed.begin() + std::min(5ul, transformed.size()));

// 排序
std::sort(taken.begin(), taken.end());

// 问题:
//  ① 3 个中间容器——每个都分配堆内存——O(N) 额外空间
//  ② filtered、transformed 遍历两次——一次生成、一次消费——O(2N) 时间
//  ③ 大量 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

# 1.2 filter + transform 的两次遍历——非惰性求值的性能陷阱

案例 1.1 的性能分析——5 个中间容器的代价:

传统 STL(急切求值):
  data → copy_if → filtered   (N 次迭代——分配新容器——~N 次 malloc)
  filtered → transform → transformed (N 次迭代——分配新容器——~N 次 malloc)
  transformed → take(5) → taken     (5 次迭代——分配小容器)
  taken → sort → taken              (5log5 ~ 12 次)

总迭代:~2N + 5 + 12 次元素访问
总分配:3 个中间容器(filtered N, transformed N, taken 5)
总内存:~2N + 5 个 int 的额外堆内存

Ranges(惰性求值):
  data | filter(even) | transform(square) | take(5) | sort
  每个元素一次性穿过整个管道——不需要任何中间容器

总迭代:N 次(只遍历源容器一次——直到管道产生 5 个输出)
总分配:0 个中间容器
总内存:0 额外堆内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 1.3 七个待解疑问

① range 和 view 有什么区别?为什么 view 可以惰性求值?                 → 第 2 / 第 3 章
② 管道 | 操作符是什么?怎么把 filter | transform 串成链?               → 第 4 章
③ std::ranges::sort 和 std::sort 哪里不同?projection 是什么?         → 第 5 章
④ ranges 的惰性求值在汇编层真的零开销吗?                               → 第 8 章
⑤ 自定义 view 怎么写?怎么扩展 ranges 生态?                            → 第 7 章
⑥ filter_view 的迭代器为什么不能随机访问?                              → 第 9 章
⑦ ranges 和 Boost.Range 的演进关系?C++20→C++23 加了什么?              → 第 7.2 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 range 的概念层级——从 input_range 到 contiguous_range

C++20 定义的 range 概念层级(从弱到强):

① std::ranges::input_range      — 可以遍历(单次消费)
② std::ranges::forward_range    — 可重复遍历(多遍)
③ std::ranges::bidirectional_range — 可反向遍历
④ std::ranges::random_access_range — 可随机访问 (O(1) 索引)
⑤ std::ranges::contiguous_range — 内存连续(和 vector/array 一样)

每个 range 通过其迭代器的能力自然获得对应的概念层级。
view 保持了源 range 的迭代器能力——除了某些适配器会降级:
  filter_view:源是 random_access → view 退化为 bidirectional
  transform_view:保持源的能力
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2 为何这么切

传统 STL:
  容器和算法通过迭代器对 (begin+end) 耦合——必须传两个参数
  sort(v.begin(), v.end())  ← 必须同类型的迭代器

Ranges:
  容器本身就是一个 range——算法接受一个 range 参数
  ranges::sort(v)  ← 简洁——且 sentinel 可以不同于 iterator

这一刀切在 C++ 最痛的痛点——迭代器对的繁琐和错误倾向:
  sort(v1.begin(), v2.end())  ← 编译通过!不同容器的迭代器在类型相同时也是合法参数
1
2
3
4
5
6
7
8
9
10

# 3. Views——惰性求值的核心

# 3.1 view 的编译器魔法——不拥有数据、O(1) 拷贝、惰性求值

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

// view = 数据的「观察窗」——不拥有数据——只持有引用/指针
auto even = data | std::views::filter([](int x) { return x % 2 == 0; });
// sizeof(even) ≈ 2 个指针(data.begin + predicate)——~16 字节
// even 不分配任何新内存——它只是「被过滤的 data 的视角」

// 惰性求值——在这个时间点 even 还没有被真的「过滤」
// 只有当你开始遍历 even——元素才被逐个检查
1
2
3
4
5
6
7
8
9

view 的核心性质:

性质 含义
不拥有数据 view 只是引用/指针指向源数据
O(1) 拷贝 拷贝 view 只是拷贝几个指针
O(1) 析构 析构 view 不需要释放任何资源
惰性求值 遍历 view 时才做变换

# 3.2 常用 view 适配器——filter / transform / take / drop / reverse

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

// filter — 只保留满足条件的元素
auto even = v | std::views::filter([](int x) { return x % 2 == 0; });
// even: 2, 4, 6, 8, 10

// transform — 对每个元素做变换
auto squares = v | std::views::transform([](int x) { return x * x; });
// squares: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100

// take — 只取前 N 个
auto first5 = v | std::views::take(5);
// first5: 1, 2, 3, 4, 5

// drop — 跳过前 N 个
auto after3 = v | std::views::drop(3);
// after3: 4, 5, 6, 7, 8, 9, 10

// reverse — 反向遍历(需要 bidirectional_range)
auto rev = v | std::views::reverse;
// rev: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.3 惰性求值 vs 急切求值——view 在整个管道中只遍历一次

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

// 管道组合——每个元素「一次性穿过」整个管道:
auto result = v
    | std::views::filter([](int x) { return x % 2 == 0; })    // ① 滤奇
    | std::views::transform([](int x) { return x * x; })        // ② 平方
    | std::views::take(3);                                      // ③ 取前 3

// 底层执行模型(pull model):
//   消费者请求下一个元素
//   → take(3) 向 transform 请求
//     → transform 向 filter 请求
//       → filter 向 v 请求——找到偶数 2
//     → transform 把 2 变成 4
//   → take(3) 拿到 4——计数 1/3

//   继续:
//     → filter 找到偶数 4 → transform 变 16 → take 计数 2/3
//     → filter 找到偶数 6 → transform 变 36 → take 计数 3/3
//   管道停止——v 中剩余 8,10 永不被访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3.4 为什么 view 的迭代器需要特殊处理——sentinel 替代 end iterator

// filter_view 的 end() —— 不是简单的 v.end()
// 因为「最后一个满足 filter 条件的元素」的位置不在编译期确定
// 需要用 sentinel 替代 iterator

auto even = v | std::views::filter(is_even);

// even.begin() → filter_view::iterator
// even.end()   → filter_view::sentinel(不是 iterator!)
// sentinel 和 iterator 可以比较——但类型不同

// 这就是 C++20 sentinel 的核心用途——
// 让 range 的 begin 和 end 可以是不同类型
1
2
3
4
5
6
7
8
9
10
11
12

# 4. 管道操作符 | ——从函数嵌套到从左到右的数据流

# 4.1 | 的本质——operator| 重载与适配器的链式组合

// ① 最底层——直接构造 view 对象:
auto v1 = std::ranges::filter_view(v, is_even);
auto v2 = std::ranges::transform_view(v1, square);
auto v3 = std::ranges::take_view(v2, 3);

// ② | 运算符——等价于上面的链:
auto v3 = v | std::views::filter(is_even)
            | std::views::transform(square)
            | std::views::take(3);
// operator| 被重载——左值 (a | b) = b(a)——把左边的 range 作为右边适配器的参数
1
2
3
4
5
6
7
8
9
10

# 4.2 管道 vs 传统 STL 算法链——可读性的量变引起质变

// ❌ 传统 STL——从右向左读(函数嵌套)或从下向上读(中间变量)
auto r1 = std::views::take(
              std::views::transform(
                  std::views::filter(v, is_even),
                  square),
              3);
// 读:filter → transform → take?还是从内到外?

// ✅ Ranges 管道——从左到右——和数据流方向一致
auto r2 = v | std::views::filter(is_even)
            | std::views::transform(square)
            | std::views::take(3);
// 读:v → 过滤偶数 → 平方 → 取前 3 —— 无比清晰
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4.3 管道中的类型推演——每一步 view 的完整类型签名

auto pipe = v                                    // vector<int>&
    | std::views::filter(is_even)                // filter_view<vector<int>&, Pred>
    | std::views::transform(square)              // transform_view<filter_view<...>, Fn>
    | std::views::take(3);                       // take_view<transform_view<...>>

// 不要尝试写出中间类型——用 auto——这是 ranges 的设计意图
// typeof(pipe) 是一个三层嵌套的模板类型——但作为 range 使用——完全透明
1
2
3
4
5
6
7

# 5. Ranges 算法——超越迭代器对的 std::ranges 命名空间

# 5.1 std::ranges::sort 和 std::sort 的区别——range 参数 vs 迭代器对

std::vector<int> v = {3, 1, 4, 1, 5};

// 传统——传两个迭代器
std::sort(v.begin(), v.end());

// Ranges——传 range(容器本身)
std::ranges::sort(v);

// Ranges 还支持投影(projection)——下次
1
2
3
4
5
6
7
8
9

# 5.2 算法返回值的增强——不仅返回迭代器、还返回 subrange

// 传统 std::find——返回一个迭代器
auto it = std::find(v.begin(), v.end(), 42);

// Ranges std::ranges::find——返回一个迭代器(同上)

// 更复杂的——std::ranges::unique
auto [first, last] = std::ranges::unique(v);  // 返回 subrange
v.erase(first, last);  // 删除重复元素——erasure idiom
1
2
3
4
5
6
7
8

# 5.3 投影(projection)——把「比什么」从比较器中解放出来

struct Employee { std::string name; int age; double salary; };
std::vector<Employee> staff = { ... };

// ❌ 传统——手写 lambda 提取 age 比较
std::sort(staff.begin(), staff.end(),
          [](const Employee& a, const Employee& b) { return a.age < b.age; });

// ✅ Ranges——投影:告诉算法「看这个字段」——比较器默认为 <
std::ranges::sort(staff, {}, &Employee::age);  // {} = 默认比较器 <
// 语义:比较员工——但每两个比较前——先投影到 &Employee::age

// 按薪水降序——投影 + 自定义比较器
std::ranges::sort(staff, std::greater{}, &Employee::salary);

// 按名字长度排序
std::ranges::sort(staff, {}, [](const std::string& s) { return s.size(); });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.4 projection 的汇编零开销——编译器内联整个 lambda 链

// 源码:
std::ranges::sort(staff, {}, &Employee::age);

// 编译器转化后的等价代码——projection 被完全内联:
std::ranges::sort(staff, [](const Employee& a, const Employee& b) {
    return a.age < b.age;   // &Employee::age 被内联——零额外调用
});

// 汇编中看不到对 projection 的函数调用——只看到 age 成员的直接比较
// 这就是 ranges 的零开销原则——projection 不是在运行时多调用一层
1
2
3
4
5
6
7
8
9
10

# 6. 组合 view 适配器——构建数据处理管道

# 6.1 经典六步管道——filter → transform → take → reverse → filter → join

std::vector<std::string> words = {"hello", "world", "cpp", "ranges", "pipe"};

auto result = words
    | std::views::filter([](const std::string& s) { return s.size() > 3; })
    | std::views::transform([](const std::string& s) {
          std::string copy = s;
          std::ranges::reverse(copy);
          return copy;
      })
    | std::views::take(3);

// step 1: {"hello", "world", "ranges", "pipe"}  (filter size > 3)
// step 2: {"olleh", "dlrow", "segnar", "epip"}  (reverse each)
// step 3: {"olleh", "dlrow", "segnar"}           (take 3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.2 管道的惰性求值模型——每个元素独立经过整个管道才产下一个

pull model(消费者驱动):

消费者(如 for 循环)请求 next()
  → 管道最后一层(take)请求前一层的 next()
    → 管道中间层(transform)请求前一层的 next()
      → 管道最前层(filter)从源容器拉取
        → 找到第一个满足条件的元素——逐层返回
  → 消费者拿到第一个「完全处理过」的元素

关键:管道中间没有任何中间容器——没有中间分配——纯函数调用链
1
2
3
4
5
6
7
8
9
10

# 6.3 与 Unix 管道的哲学同构——数据流从左到右、每步独立变换

# Unix 管道
cat data.txt | grep -v "^#" | awk '{print $1}' | sort | uniq

# C++ Ranges 管道
data | filter(non_comment) | transform(first_col) | sort | unique

# 同一条哲学:数据从左向右流动——每一步做一件事——组合而不是嵌套
1
2
3
4
5
6
7

# 7. 自定义 view 与适配器——扩展 ranges 生态

# 7.1 用 views::transform 函数对象替代手写 lambda

namespace views = std::views;

// 使用标准库函数对象——不需要手写 lambda
auto squares = v | views::transform([](int x) { return x * x; });  // 手写 lambda
auto squares2 = v | views::transform(square_fn);                     // 可复用的函数对象
1
2
3
4
5

# 7.2 C++23 新增的适配器——丰富 ranges 工具箱

适配器 C++ 版本 效果
views::zip C++23 把两个 range 的元素配对——(a[i], b[i])
views::zip_transform C++23 zip + transform——在 zip 的同时做变换
views::adjacent<N> C++23 滑动窗口——取相邻 N 个元素
views::stride C++23 每隔 N 个元素取一个
views::slide C++23 大小为 N 的滑动窗口
views::chunk C++23 把 range 分成固定大小的块
views::chunk_by C++23 根据谓词分组
views::join_with C++23 用分隔符 join

# 8. Ranges 的性能——汇编层验证零开销

# 8.1 filter|transform 管道 vs 手写 for 循环——汇编对比

// 手写 for 循环
int sum = 0;
for (int x : data)
    if (x % 2 == 0)
        sum += x * x;

// Ranges 管道
auto even_squares = data
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; });
int sum = std::ranges::fold_left(even_squares, 0, std::plus{});
1
2
3
4
5
6
7
8
9
10
11

GCC 13.2 -O2 汇编对比——两者生成的汇编几乎完全相同——都是直接的循环体内嵌条件和乘法。没有任何 view 的构造/析构开销。

# 8.2 ranges::sort + projection vs 手写 lambda——性能差异

// 手写 lambda 排序
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

// ranges 排序 + projection
std::ranges::sort(v, std::greater{});  // 默认投影 = 元素本身
1
2
3
4
5

性能:两者相同——projection 被内联——没有额外函数调用。

# 8.3 惰性求值的缓存友好性——单次遍历 vs 多次中间容器分配

传统(急切求值+中间容器):
  filter → 分配 N 个元素的中间容器 → transform → 分配 N 个元素的中间容器
  → 数据被搬移两次——每次搬移都触发 cache miss(加载新分配的内存)

Ranges(惰性求值):
  单次遍历源容器——每个元素在 L1 缓存中走完整个管道
  → 零中间分配 → 零额外的 cache miss
1
2
3
4
5
6
7

# 9. 常见陷阱与反模式

# 9.1 view 在源容器析构后使用——悬挂引用

auto get_view() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    return v | std::views::filter(is_even);  // ❌ v 析构后——view 变成悬挂引用
}
// view 持有的引用指向已析构的 v——UB

// ✅ 把数据移到外面——或把 view 的消费放在同一个作用域
1
2
3
4
5
6
7

# 9.2 试图给 view 做 sort——view 不拥有数据、不可修改

auto even = data | std::views::filter(is_even);
// std::ranges::sort(even);  // ❌ filter_view 不是 random_access_range——不可 sort

// ✅ 把 view 的结果收集到容器——再 sort
std::vector<int> result;
std::ranges::copy(even, std::back_inserter(result));
std::ranges::sort(result);
1
2
3
4
5
6
7

# 9.3 filter_view 的迭代器递增——可能多次 ++ 才找到一个有效元素

auto even = data | std::views::filter(is_even);
auto it = even.begin();  // ① 找到第一个偶数——可能遍历 N/2 个元素

++it;  // ② 找到下一个偶数——可能再次遍历多个元素
// 每次 ++it 的复杂度由 filter 中有效元素的间隔决定
// 平均:O(1) —— 但最坏:O(N)(只有一个偶数在末尾)
1
2
3
4
5
6

# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① view vs range? 第 2-3 章:range=可遍历、view=不拥有数据的 range——惰性求值
② 管道 操作符?
③ ranges::sort 区别? 第 5 章:接受 range 整体、返回 subrange、支持 projection
④ 汇编零开销? 第 8 章:view 的 lambda 被完全内联—和手写循环相同的指令
⑤ 自定义 view? 第 7 章:C++23 新增 zip/adjacent/slide/chunk 等适配器
⑥ filter 迭代器代价? 第 9.3:每次 ++ 可能需要跳过非匹配元素——均摊 O(1)
⑦ Boost.Range→C++20? 第 7.2:Boost.Range 是先驱——C++20 标准化了核心 view 适配器

案例①修复——迭代器对地狱:用 ranges 管道替代所有中间容器:

auto result = read_data()
    | std::views::filter(even)
    | std::views::transform(square)
    | std::views::take(5);
std::vector<int> v;
std::ranges::copy(result, std::back_inserter(v));
std::ranges::sort(v);
1
2
3
4
5
6
7

案例②修复——非惰性求值:用 ranges 管道——零中间容器——单个元素一次穿管——无额外 malloc。

# 10.2 一次管道求值的完整旅程——从容器到输出

管道:v | filter(is_even) | transform(square) | take(3)

消费:for (int x : result)

═══════ 消费者请求 1 ═══════

take(3)::iterator::operator*()
  → 调用 transform::iterator::operator*()
    → 调用 filter::iterator::operator*()
      → 迭代 v: 跳过 1 → 跳过 2? 2 是偶数!停在 2
    → filter 返回 2
  → transform 把 2 变换为 4
→ take 拿到 4——计数 1

═══════ 消费者请求 2 ═══════

take(3)::iterator::operator++() → operator*()
  → ... 同上——跳过 3 → 停在 4 → transform 变 16 → take 计数 2

═══════ 消费者请求 3 ═══════

  → ... 停在 6 → transform 变 36 → take 计数 3

═══════ 消费者请求 4 ═══════

take(3)::iterator 检测到 count == 3 → 返回 sentinel
→ 循环终止——管道停止——源容器中 8, 10 永不被访问
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

# 10.3 设计哲学回扣

哲学 1:Ranges 把 C++ 从「算法操作数据」变成了「数据流经管道」

传统 STL:算法是主动的——sort(v.begin(), v.end())——算法驱动数据。
Ranges:数据是主动的——v | filter | transform | take——数据从左到右流动。
这不仅是语法变化——是思维模型的范式转换。和 Unix 管道的哲学一致:数据是主语、操作是修饰。

哲学 2:惰性求值让组合成为可能——不构造中间容器就不需要管理它们的生命周期

v | filter | transform | take 在传统 STL 中需要 3 个中间容器——每一个都分配内存、都需要命名、都需要在作用域结束时析构。Ranges 的惰性求值消灭了这三个中间容器——代码从「3 个容器的管理」变成「1 条数据流链」。

哲学 3:Projection 把二维比较化为一维——比较什么、怎么比——分开编码

传统比较器需要手写 lambda 提取成员——a.age < b.age 把「取什么」和「怎么比」混在一起。Projection 把这两个维度分开——std::ranges::sort(staff, {}, &Employee::age)——投影回答「看什么」、比较器回答「怎么比」。两个维度正交——独立变化。 而且投影被编译器完全内联——零运行时开销。

哲学 4:实现复杂度不应暴露在接口上——auto 类型推演隐藏了 view 的深层嵌套类型

filter_view<transform_view<take_view<vector<int>&>>>——这是三层的嵌套类型。但用户从来不需要写这些类型——auto 让 ranges 的实现复杂度完全隐藏在编译器中。这和 Rust 的 impl Iterator、Haskell 的类型推演共享同一哲学——复杂类型对用户不可见。

# 10.4 速查表合集

常用 view 速查:

适配器 效果 降级迭代器?
views::filter 只保留满足谓词的元素 ✅ → bidirectional
views::transform 对每个元素做变换 ❌ 保持
views::take 取前 N 个 ❌ 保持
views::drop 跳过前 N 个 ❌ 保持
views::reverse 反向遍历 ❌ 保持(已经是双向)
views::join 展开嵌套 range ✅ → forward

管道 vs 传统 STL:

维度 传统 STL Ranges
参数 迭代器对 range 整体
组合方式 嵌套函数调用 operator| 管道
中间容器 需要(急切求值) 不需要(惰性求值)
比较 lambda 提取成员 projection
返回值 单个迭代器 迭代器 / subrange
end 类型 必须和 begin 同类型 可以用 sentinel

Ranges 算法 vs 传统算法:

// 传统
std::sort(v.begin(), v.end());
std::copy_if(v.begin(), v.end(), out, pred);

// Ranges
std::ranges::sort(v);
std::ranges::copy_if(v, out, pred);

// Ranges + projection
std::ranges::sort(staff, {}, &Employee::age);
std::ranges::find_if(staff, [](const auto& e) { return e.salary > 100000; }, &Employee::salary);
1
2
3
4
5
6
7
8
9
10
11

本篇小结:Ranges 是 C++20 对算法库的最大升级——把「迭代器对」抽象为 range、用惰性求值的 view 替代急切求值的中间容器、用 | 管道替代函数嵌套、用 projection 替代手写比较 lambda。惰性求值让每个元素一次性穿过整个管道——零中间分配、零额外的 cache miss。汇编层验证了 view 和 projection 的零开销——编译期内联的结果和手写 for 循环完全相同。

下一篇:数据处理管道说清了。下一篇进入 58.format与print体系——std::format 语法、扩展格式化、locale 无关、与 printf/iostream 性能对比——把 C++ 的输入输出也现代化。

上次更新: 2026/06/10, 11:13:41
异常机制底层原理
format与print体系

← 异常机制底层原理 format与print体系→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式