为什么需要 Ranges?
传统 STL 处理数据序列冗长且易错:
1 2 3 4 5 6
| std::vector<int> result; std::copy_if(nums.begin(), nums.end(), std::back_inserter(result), [](int n) { return n % 2 == 0; }); std::transform(result.begin(), result.end(), result.begin(), [](int n) { return n * n; });
|
Ranges 用管道式、声明式语法替代:
1 2 3 4
| auto result = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }) | std::ranges::to<std::vector>();
|
核心优势:
- 管道式操作:
| 链式组合,像 Unix 管道 - 懒惰求值:按需计算,无中间容器
- 直接操作容器:无需
begin()/end() - 投影支持:
&Person::age 直接按成员操作 - 类型安全:编译期检查,防止悬空迭代器
核心概念
| 概念 | 含义 |
|---|
| Range | 任何提供 begin()/end() 的可迭代序列(容器、视图、C 数组) |
| View | 轻量、懒惰的范围表示,不复制数据,用 ` |
| Adaptor | 修改范围不改底层数据,如 filter/transform/take/reverse |
| Algorithm | Ranges 版算法,直接接受范围并支持投影 |
头文件与命名空间
Ranges 相关工具主要来自 <ranges>:
<ranges> 提供:
std::ranges::range、std::ranges::view 等 conceptsstd::ranges::ref_view、std::ranges::owning_view 等 view 类型std::views::filter、std::views::transform、std::views::take 等 view adaptorsstd::ranges::to(C++23,转换为容器)
std::views 是标准库提供的短命名空间,基本等价于:
1
| namespace std::views = std::ranges::views;
|
所以这两种写法通常等价:
1 2
| auto a = nums | std::views::filter(pred); auto b = nums | std::ranges::views::filter(pred);
|
实际代码推荐写短的 std::views::...。
注意:Ranges 算法在 <algorithm>:
1 2 3 4 5
| #include <algorithm>
std::ranges::sort(nums); std::ranges::find(nums, 42); std::ranges::contains(nums, 42);
|
容器、字符串、span 仍然包含各自头文件:
1 2 3
| #include <vector> #include <string> #include <span>
|
快速上手
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <print> #include <vector> #include <ranges>
int main() { std::vector<int> nums = {1, 2, 3, 4, 5, 6};
auto even_squares = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; });
std::println("{}", even_squares); }
|
| 从左到右组合视图- 计算是懒惰的,迭代时才执行
even_squares 是视图,不拥有数据
投影:按结构体成员操作
1 2 3 4 5 6 7 8 9
| struct Person { std::string name; int age; }; std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; });
std::ranges::sort(people, {}, &Person::age);
|
&Person::age 指定比较成员,{} 用默认(升序)比较。投影适用于所有 Ranges 算法(find、max_element 等)。
收集结果到容器(C++23)
1 2 3
| auto evens = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::ranges::to<std::vector>();
|
Lambda 参数:auto&& 是默认首选
在 adaptor 的 lambda 中,元素经迭代器传递,参数类型影响性能与正确性:
| 传递方式 | 适用场景 |
|---|
auto | 小型内置类型(int),值拷贝可接受 |
const auto& | 只读大型对象;无法处理代理对象(vector<bool>、zip) |
auto&& | 默认首选:万能绑定、零拷贝、支持代理对象 |
auto&& 是转发引用(万能引用),能绑定左值和右值,是处理代理对象的唯一通用方式。实测对大对象,auto(拷贝)比 const auto&/auto&&(零拷贝)慢约两个数量级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| auto result = nums | std::views::filter([](auto&& n) { return n % 2 == 0; }) | std::views::transform([](auto&& n) { return n * n; });
std::vector<bool> flags = {true, false, true}; auto r = flags | std::views::filter([](auto&& flag) { return flag; });
auto zipped = std::views::zip(a, b); auto r2 = zipped | std::views::filter([](auto&& pair) { auto& [num, str] = pair; return num > 1; });
|
决策树:需要修改元素或处理代理对象 → auto&&;小型只读 → auto;大型只读且无代理对象 → const auto&。
陷阱:
auto&& 允许修改,filter 中别误改原始数据(应保持纯函数)- 视图不延长底层数据生命周期,跨函数返回前用
ranges::to 收集 - filter 中无需
std::forward;transform 需要转发移动语义时才用
views::split 的 char vs string 陷阱
1 2 3 4 5 6 7 8
| std::string text{"hello world"};
auto parts{text | std::views::split(" ")};
auto parts{text | std::views::split(' ')}; auto parts{text | std::views::split(" "sv)};
|
C++23 重构了 split:
lazy_split(C++20 旧行为):支持输入范围(如文件流),子范围非 contiguous_rangesplit(C++23):要求前向范围,子范围保留连续特性,可直接构造 string_view
1
| auto vec{s | std::views::split(',') | std::ranges::to<std::vector<std::string>>()};
|
常用 Adaptors 速查
| Adaptor | 功能 | 示例 |
|---|
filter | 过滤 | nums | filter(even) |
transform | 转换 | nums | transform(square) |
take / drop | 取前 N / 跳过前 N | nums | take(3) |
reverse | 反转 | nums | reverse |
keys / values | 提取 map 键/值 | map | keys |
iota | 生成序列 | iota(1, 10) → 1…9 |
zip | 合并为元组 | zip(a, b) |
join | 展平嵌套 | nested | join |
1 2 3 4 5 6 7 8
| auto result = std::views::iota(1) | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::take(10);
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}}; auto flat = nested | std::views::join;
|
各 Adaptor 使用要点
filter
谓词可能被调用多次,不应依赖副作用,最好是纯判断——相同输入返回相同结果:
1 2 3 4
| int count = 0; auto bad = nums | std::views::filter([&](int) { return ++count % 2 == 0; });
|
返回引用则后续视图继续操作原对象,返回值则每次迭代按需生成临时值:
1 2 3 4 5
| auto names = users | std::views::transform(&User::name);
auto upper = users | std::views::transform([](const User& u) { return to_upper(u.name()); });
|
take / drop
取前 N 个或跳过前 N 个,常用于截断无限序列(见 懒惰求值):
1 2
| auto first_10 = nums | std::views::take(10); auto after_10 = nums | std::views::drop(10);
|
reverse
反向观察支持双向迭代的 range,不会真的倒置容器,只是反转迭代方向:
1
| auto reversed = nums | std::views::reverse;
|
keys / values
观察 map、pair 序列中的 key 或 value:
1 2 3 4 5 6 7 8
| std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 82}};
for (std::string_view name : scores | std::views::keys) { std::println("{}", name); } for (int score : scores | std::views::values) { std::println("{}", score); }
|
views::all 与 ref_view / owning_view
std::views::all(x) 把一个对象转成“适合管道使用的 view”,根据传入对象类型选择不同包装:
| 输入 | 结果倾向 | 含义 |
|---|
| 已经是 view | 原样或轻量包装 | 直接使用 |
| 左值容器 | std::ranges::ref_view | 引用外部容器 |
| 右值可借用 range | 直接使用 | 不会悬空 |
| 右值非借用 range | std::ranges::owning_view | view 拥有这个临时 range |
1 2 3 4
| std::vector<int> nums = {1, 2, 3};
auto a = std::views::all(nums); auto b = std::views::all(std::vector{1, 2});
|
这也是为什么一些管道表达式可以安全地从临时容器开始:
1 2
| auto view = std::vector{1, 2, 3} | std::views::transform([](int n) { return n * 2; });
|
现代 ranges 会尽量避免明显悬空,但不能替你解决所有生命周期问题,尤其是 lambda 捕获引用、返回 view、保存 view 时。
std::ranges::view 到底是什么
std::ranges::view 是一个 concept,大致可以理解为:
1 2 3 4 5
| template <class T> concept view = std::ranges::range<T> && std::movable<T> && std::ranges::enable_view<T>;
|
重点只有三点:
- 它首先是 range:能
begin() / end() - 它至少可移动:view 可以安全放进管道里传递
- 它是轻量范围对象:适合拷贝 / 移动 / 按值返回
std::views::filter 里的 views 是命名空间;std::ranges::view 里的 view 是 concept。名字很像,但一个用来构造视图,一个用来约束 / 判断类型。
注意:view 不等于永远不拥有数据。多数 view 只是观察外部数据,但也有拥有数据或状态的 view。
| view | 是否拥有数据 | 说明 |
|---|
ref_view | 否 | 引用外部 range |
owning_view | 是 | 持有右值 range |
single_view | 是 | 持有一个元素 |
iota_view | 是 | 持有起点 / 终点状态 |
filter_view | 部分拥有 | 持有底层 view 和谓词 |
view 和 range 的区别
1 2 3 4 5 6 7
| std::vector<int> nums = {1, 2, 3};
static_assert(std::ranges::range<decltype(nums)>); static_assert(!std::ranges::view<decltype(nums)>);
auto v = nums | std::views::take(2); static_assert(std::ranges::view<decltype(v)>);
|
容器负责存储,view 负责“看待这段数据的方式”。view 通常不分配、不复制元素,只保存迭代器、引用、谓词、转换函数等小状态。
view 的两个常见形态
立即可迭代的 view 对象:
1 2 3 4 5
| auto first_three = nums | std::views::take(3);
for (int n : first_three) { std::println("{}", n); }
|
等待接收 range 的 adaptor closure:
1 2 3 4 5
| auto only_even = std::views::filter([](int n) { return n % 2 == 0; });
auto r = nums | only_even | std::views::take(3);
|
std::views::filter(pred) 单独出现时还不是最终数据序列,它是“后面要接一个 range”的管道组件。
view 的可复制性别想当然
标准要求 view 可移动,不要求所有 view 都好复制。多数标准 view 拷贝成本很低,但业务代码不要依赖“随便复制复杂 view 没代价”。
1 2 3 4 5
| auto pipeline = nums | std::views::filter(is_valid) | std::views::transform(to_record);
auto records = pipeline | std::ranges::to<std::vector>();
|
需要跨线程、跨对象成员、跨函数长期保存时,优先保存容器结果,或者保存原始数据和查询参数,临时组合 view。
单次遍历 view
不是所有 view 都能反复遍历。基于输入流的 range 通常是单次读取:
1 2 3 4 5
| std::istringstream input{"1 2 3"}; auto ints = std::views::istream<int>(input);
for (int n : ints) { } for (int n : ints) { }
|
如果后续逻辑要多次遍历,先收集:
1 2
| auto values = std::views::istream<int>(input) | std::ranges::to<std::vector>();
|
返回 view 的简单规则
返回 view 可以,但要保证它引用的数据活得够久:
1 2 3 4 5 6
| auto active_users(std::span<User> users) { return users | std::views::filter([](const User& user) { return user.active(); }); }
|
这里安全是因为 std::span 只是调用者传进来的视图,调用者负责保证底层 User 数组仍然存在。不要返回引用局部容器的 view:
1 2 3 4
| auto bad() { std::vector<int> nums = {1, 2, 3}; return nums | std::views::filter([](int n) { return n > 1; }); }
|
subrange
std::ranges::subrange 是由迭代器和 sentinel 组成的 view,适合把一段 [begin, end) 包装成 range。常用于算法返回迭代器后,把后续区间重新变成可管道处理的 range:
1 2 3 4 5 6 7
| auto first_even = std::ranges::find_if(nums, [](int n) { return n % 2 == 0; });
auto rest = std::ranges::subrange(first_even, nums.end()) | std::views::take(3) | std::views::transform([](int n) { return n * 10; });
|
Ranges 算法 vs 传统 STL
| 操作 | 传统 STL | Ranges |
|---|
| 排序 | sort(v.begin(), v.end()) | ranges::sort(v) |
| 按成员查找 | find_if(..., [](auto& x){ return x.id==42; }) | ranges::find(v, 42, &X::id) |
| 查找 | find(v.begin(), v.end(), 42) | ranges::find(v, 42) |
1 2 3 4 5 6
| auto result = vec | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }) | std::views::take(3) | std::ranges::to<std::vector>();
|
懒惰求值
视图在迭代时才计算,元素逐个流过管道:
1 2 3 4
| auto filtered = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; });
|
优势:只算需要的元素、可处理无限序列、组合不产生中间结果。
1 2 3 4
| auto squares = std::views::iota(1) | std::views::transform([](int n) { return n * n; }); for (int sq : squares | std::views::take(5)) { }
|
迭代器要求与陷阱
| 概念 | 典型容器 |
|---|
input_iterator | istream_iterator(单次读) |
forward_iterator | forward_list、unordered_map |
bidirectional_iterator | list、map、set |
random_access_iterator | vector、deque、array |
contiguous_iterator | vector、array、string_view |
Adaptor 最低要求:reverse 需双向、filter/split 需前向、transform/take 仅需输入。
1 2 3
| std::forward_list<int> list = {1, 2, 3};
|
悬空保护与 borrowed_range:std::ranges::find(std::vector{1,2,3}, 42) 返回 dangling,无法解引用——编译期阻止对临时容器的悬空引用。
Ranges 用 std::ranges::borrowed_range 处理悬空迭代器:如果一个 range 的迭代器可以在 range 对象销毁后继续安全使用,它就是 borrowed range。
std::span<T>、std::string_view 是 borrowed range——迭代器指向外部数据。std::vector<T> 右值不是 borrowed range——临时 vector 销毁后元素也没了。
1 2
| auto it1 = std::ranges::find(std::span{data}, 42); auto it2 = std::ranges::find(std::vector{1, 2, 3}, 2);
|
std::ranges::dangling 是标准库在类型层面对你的提醒:这个迭代器不能安全返回。
C++23 新特性
1 2 3 4 5 6
| auto m = pairs | std::ranges::to<std::map>(); auto v = pairs | std::ranges::to<std::vector>();
bool found = std::ranges::contains(vec, 3);
|
C++23 还给 views 补了一批实用工具:
| view | 作用 |
|---|
std::views::zip | 并行遍历多个 range |
std::views::enumerate | 遍历时附带下标 |
std::views::chunk | 按固定大小分块 |
std::views::slide | 滑动窗口 |
std::views::join | 展平一层嵌套 range |
1 2 3 4 5 6
| std::vector<std::string> names = {"Alice", "Bob"}; std::vector<int> scores = {95, 82};
for (auto [name, score] : std::views::zip(names, scores)) { std::println("{}: {}", name, score); }
|
实际可用性取决于标准库实现版本——编译器支持 C++23 不等于所有 C++23 library 组件都完整可用。
实战案例
1 2 3 4 5 6 7 8 9 10
| auto windows = data | std::views::slide(3); for (auto window : windows) { }
std::generator<int> fibonacci() { int a = 0, b = 1; while (true) { co_yield a; std::tie(a, b) = std::make_tuple(b, a + b); } } auto fibs = fibonacci() | std::views::take(10);
|
自定义一个简单 view
多数业务代码不需要手写 view,用标准 adaptor 组合就够了。如果要封装可复用的懒惰序列,可以继承 std::ranges::view_interface。下面是一个简单的 repeat_n_view:重复生成同一个值 N 次。
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| #include <cstddef> #include <ranges>
template <typename T> class repeat_n_view : public std::ranges::view_interface<repeat_n_view<T>> { public: repeat_n_view() = default;
repeat_n_view(T value, std::size_t count) : value_(std::move(value)), count_(count) {}
class iterator { public: using value_type = T; using difference_type = std::ptrdiff_t;
iterator() = default;
iterator(const T* value, std::size_t index) : value_(value), index_(index) {}
const T& operator*() const { return *value_; }
iterator& operator++() { ++index_; return *this; }
void operator++(int) { ++*this; }
bool operator==(const iterator&) const = default;
private: const T* value_ = nullptr; std::size_t index_ = 0; };
iterator begin() const { return iterator{&value_, 0}; }
iterator end() const { return iterator{&value_, count_}; }
private: T value_{}; std::size_t count_ = 0; };
|
使用:
1 2 3 4 5
| repeat_n_view hello{"hi", 3};
for (const auto& word : hello) { std::println("{}", word); }
|
这个例子为了说明结构刻意保持简单。真实标准库 view 还要考虑 const 迭代、sentinel、range category、异常保证、借用语义等问题。业务代码里更推荐先写普通函数返回标准 view 组合:
1 2 3 4 5 6 7
| auto valid_names(std::span<const User> users) { return users | std::views::filter([](const User& user) { return user.active(); }) | std::views::transform(&User::name); }
|
Ranges View 的生命周期陷阱
视图最重要的规则:视图不能比它观察的数据活得更久(通用视图生命周期的完整讨论见 C++ 视图)。Ranges View 有两个典型陷阱:
小心 lambda 捕获引用
1 2 3 4 5
| auto make_filtered(std::vector<int>& nums, int limit) { return nums | std::views::filter([&](int n) { return n > limit; }); }
|
limit 是局部参数,函数返回后引用悬空。应按值捕获:
1 2 3 4 5
| auto make_filtered(std::vector<int>& nums, int limit) { return nums | std::views::filter([limit](int n) { return n > limit; }); }
|
不要长期缓存复杂 view
复杂 view 类型会编码整个管道,类型长、生命周期关系复杂。短局部变量没问题,跨函数或成员变量保存时要谨慎。如果结果要跨作用域长期使用,通常更清晰的做法是物化:
1 2 3 4
| auto records = nums | std::views::filter(is_valid) | std::views::transform(to_record) | std::ranges::to<std::vector>();
|
最佳实践
✅ 推荐
- 优先用 Ranges 算法:
ranges::sort(vec)、ranges::contains(vec, x) - 用投影简化:
ranges::sort(people, {}, &Person::age) - lambda 参数默认
auto&& - 管道中间阶段用 view,边界处再物化成容器
- 跨函数返回结果前用
ranges::to 收集,避免悬空
❌ 避免
- 对临时容器取返回迭代器(得到
dangling) - 对不支持的操作用视图(
forward_list + reverse) - 忘记视图是懒惰的——不迭代就不执行
- 过度组合长管道:需多次遍历时先用
ranges::to 落盘
总结
Ranges 的核心价值:可读性(声明式)、组合性(管道)、性能(懒惰求值 + 零成本抽象)、安全性(编译期检查)、简洁性(投影消除样板)。