C++20/23 Ranges:从「迭代器对」到「可组合管道」

Ranges 把「两个迭代器表示一段序列」提升为一等公民:range 是有 begin() / end()(或满足 std::ranges::range 概念)的对象;views 在管道里惰性变换;range 算法 统一用整段序列调用。

本文面向已经会写 for (auto it = v.begin(); it != v.end(); ++it) 的读者,讲清核心心智模型、常用视图、与算法配合的写法,以及几个真实项目里最容易踩的坑。


1. 为什么要有 Ranges?

经典 STL 写法是「算法 + 两个迭代器」:

cpp 复制代码
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });

问题不在「能不能用」,而在于:

  1. 重复写 begin/end,噪声大,也容易把两个容器的迭代器配错对。
  2. 中间结果常要落到临时容器里,才能继续链式处理,既冗长又可能多一次分配。
  3. 组合逻辑不直观:「先过滤再映射再取前 N 个」在循环或多次算法调用里读起来费劲。

Ranges 的方向是:

  • 算法 接受「整段 range」,例如 std::ranges::sort(v)
  • 视图(view)描述一种惰性的序列变换:在遍历时才算下一个元素,通常不拥有底层数据。
  • 管道 | 把视图串起来,读起来接近数据流。
cpp 复制代码
#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6};

    auto r = v
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });

    for (int x : r) std::cout << x << ' ';  // 4 16 36
}

2. 三个词先分清:Range、View、Adaptor

术语 直觉 备注
Range 「能从头到尾遍历的东西」 vectorstringspan、视图本身,只要满足 std::ranges::range
View 一种轻量、通常不拥有数据的 range 复制应便宜;惰性求值;很多在 std::views:: 里。
Range adaptor 把管道左侧「再包一层」的函数对象 例如 std::views::filter(pred) 返回 adaptor,`rng

惰性 的含义:上面例子里的 filter + transform 不会在 r 构造时扫完整张 vector;只有你迭代 r(或交给需要遍历的算法)时,才会按需从 v 里拉取元素。


3. 头文件与命名空间习惯

  • 核心定义在 <ranges>;很多算法有 range 重载 ,在 <algorithm> 里,位于 std::ranges::
  • 视图工厂一般在 std::views:: (就是 std::ranges::views:: 的别名)。

典型写法:

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

void demo(std::vector<int>& v) {
    std::ranges::sort(v);
    auto w = v | std::views::reverse;
    (void)w;
}

4. 常用视图(C++20 为主,顺带 C++23)

下面只列日常最高频 的一批;完整列表以 cppreference · Range adaptors 为准。

4.1 filter / transform

过滤与映射,相当于惰性版「先筛再映」:

cpp 复制代码
auto r = vec
    | std::views::filter([](const auto& x) { return x > 0; })
    | std::views::transform([](int x) { return x * 2; });

4.2 take / drop

只取前 N 个、丢掉前 N 个:

cpp 复制代码
auto first3 = v | std::views::take(3);
auto rest   = v | std::views::drop(3);

4.3 keys / valuesmap 上很好用)

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

void print_keys(const std::map<int, std::string>& m) {
    for (int k : m | std::views::keys)
        std::cout << k << '\n';
}

4.4 iota / iota + take

生成无限递增序列的视图,务必take 等截断,否则自己 for 循环别写错条件:

cpp 复制代码
for (int i : std::views::iota(1, 11))  // [1, 11)
    std::cout << i << ' ';

4.5 zip(C++23)

并行遍历多个 range(长度取最短):

cpp 复制代码
// C++23
for (auto [a, b] : std::views::zip(xs, ys))
    use(a, b);

若你仍在 C++20,可以用手写索引或第三方实现;标准库要到 23 才补齐。

4.6 split / lazy_split(按分隔「切」视图)

适合简单协议解析、按字符切分等;注意返回的是子 range 的视图,底层仍是原字符串的生命周期。


5. Range 版算法:少写一对迭代器

很多算法提供 std::ranges::xxx(rng, ...) 形式,例如:

cpp 复制代码
std::ranges::sort(v);
if (std::ranges::find(v, 42) != v.end()) { /* ... */ }

带投影(projection)的重载(C++20)可以把「比较键」从谓词里拆出来,例如按结构体某一字段排序:

cpp 复制代码
struct Person { std::string name; int age; };
std::vector<Person> people;

std::ranges::sort(people, {}, &Person::age);           // 升序按 age
std::ranges::sort(people, std::greater{}, &Person::age); // 降序

第三个参数是 projection :在比较前先「投影」成参与比较的值。比手写 lambda 比较字段更短、更不容易写错。


6. 视图什么时候要「物化」?

视图不拥有数据,很多算法又需要「有明确端点的序列」或对同一序列遍历多次。此时用 std::ranges::to(C++23)最干净:

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

std::vector<int> materialize(auto&& r) {
    return r | std::ranges::to<std::vector<int>>();
}

C++20 可以退而求其次:std::vector<int> out; std::ranges::copy(r, std::back_inserter(out)); 或手写 reserve + assign

经验法则

  • 只遍历一次、且逻辑简单 → 保持 view,省钱。
  • 需要 size()、随机访问、多次遍历、或传给只认具体容器的 API → 物化vector 等。

7. 生命周期与悬空:Ranges 的头号坑

视图里往往存的是引用或迭代器指向外部数据。下面这种模式是未定义行为:

cpp 复制代码
auto bad() {
    std::string s = "hello world";
    return s | std::views::split(' ');
}  // s 已销毁,调用方迭代返回的 view → 悬空

// 正确:让拥有者的生命周期覆盖 view 的使用期
std::string s = "hello world";
auto tokens = s | std::views::split(' ');
for (auto t : tokens) { /* 使用 t,注意 t 也是 subrange */ }

另一常见坑是 join :若外层 range 里的内层 range 是临时的,join 后迭代也可能悬空。解决办法同样是:保证每一层被 join 的 range 都活着,或先物化成稳定容器。

原则 :把 views 看成「悬挂在别物上的窗口」------窗口本身很轻,但被看的东西不能先没了。


8. 与「手写 for」如何取舍?

场景 更倾向
单次线性处理、条件分支多、需要早退与复杂状态 手写循环往往更清晰
数据管线清晰、变换可命名、复用多 views + ranges:: 算法可读性更好
性能敏感热点路径 实测为准;view 组合有时增加间接层,有时又省掉中间容器

Ranges 不是「取代所有循环」,而是给声明式、可组合的序列处理多一套一等工具。


9. 小结

  • Range 统一了「一整段序列」的表达;view 提供惰性、可管道的变换。
  • rng \| std::views::xxx 读起来像 Unix 管道,适合过滤 / 映射 / 截取 / 配对等组合逻辑。
  • std::ranges:: 算法减少迭代器噪音,投影参数能简化按字段排序/查找。
  • 生命周期 :视图不延长被引用对象的生命周期;跨函数返回 view 要特别小心;需要稳定迭代或多次遍历时用物化

把 Ranges 用熟之后,再读 <ranges> 里概念分层(rangeviewborrowed_range 等)会轻松很多;日常写业务代码,掌握本文几类视图 + range 算法 + 生命周期意识,就足够应付大部分场景。

相关推荐
Shan12054 天前
实例分析:C++20的std::jthread
c++20
charlie1145141914 天前
基于开源项目的现代C++工程实践——OnceCallback 前置知识(下):C++20/23 高级特性
c++·开源·c++20
Hical_W5 天前
Hical 踩坑实录五部曲(二):MSVC / GCC / Clang 三平台 C++20 编译差异
linux·windows·经验分享·嵌入式硬件·macos·开源·c++20
Shan12056 天前
C++20中带有约束条件的new
c++20
Hical_W9 天前
用 Hical + MySQL 5 分钟搭建 CRUD API(C++20 协程版)
数据库·mysql·c++20
Hical_W10 天前
从 io_context 出发,掌握 C++20 协程式异步 I/O,学会 TCP 服务器、定时器和多线程模型,结合 Hical 框架实战解读
服务器·tcp/ip·开源·c++20
c++之路14 天前
C++20概述
java·开发语言·c++20
故事还在继续吗15 天前
C++20关键特性
开发语言·c++·c++20
熊文豪16 天前
FinceptTerminal 深度解析:用 C++20 + Qt6 + Python 打造的开源 Bloomberg 终端
python·开源·c++20·bloomberg·finceptterminal