C++20 Ranges 范围库
Ranges库与视图组合
前置知识
- 模板元编程:建议先完成前一篇的学习
学习目标
- 掌握「概述」的核心机制、典型用法与常见陷阱
- 掌握「基础概念」的核心机制、典型用法与常见陷阱
- 掌握「快速上手」的核心机制、典型用法与常见陷阱
- 掌握「详细用法」的核心机制、典型用法与常见陷阱
- 掌握「常见场景」的核心机制、典型用法与常见陷阱
概述
C++20 引入的 Ranges 库为标准库算法带来了范式级的变革。它将算法与数据源解耦,通过视图(View)实现惰性求值的链式管道操作,使数据处理代码更加声明式和高效。传统 STL 算法依赖迭代器对,而 Ranges 以”范围”为基本抽象,配合管道操作符 | 实现数据流的组合变换,代码可读性显著提升。
Ranges 的核心设计理念是:算法不应关心数据的存储方式,数据变换应该像流水线一样可组合。视图是轻量级的范围适配器,不会复制底层数据,仅在迭代时按需计算。
基础概念
范围(Range)
范围是对可迭代数据的抽象,任何提供 begin() 和 end() 的类型都是范围。C++20 通过 std::ranges::range 概念约束范围类型,包括容器、数组、初始化列表等。
视图(View)
视图是惰性求值的范围适配器,具有以下特性:
- 不拥有数据,仅引用底层范围
- 复制、赋值和销毁的复杂度为 O(1)
- 惰性求值,仅在迭代时才执行计算
- 可通过管道操作符
|链式组合
常用视图一览
| 视图 | 说明 |
|---|---|
filter | 根据谓词过滤元素 |
transform | 对每个元素应用转换函数 |
take | 取前 n 个元素 |
drop | 跳过前 n 个元素 |
reverse | 反转元素顺序 |
sort | 排序(C++20 ranges 算法) |
unique | 去除连续重复元素 |
join | 展平嵌套范围 |
zip | 合并多个范围(C++23) |
enumerate | 带索引遍历(C++23) |
chunk | 分块(C++23) |
slide | 滑动窗口(C++23) |
快速上手
管道式数据变换
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 链式管道:过滤偶数 -> 平方 -> 取前5个
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(5);
for (int n : result) {
std::cout << n << " "; // 输出: 4 16 36 64 100
}
return 0;
}
使用 iota 生成序列
#include <ranges>
// 生成 1 到 99 的整数序列
auto nums = std::views::iota(1, 100);
// 过滤偶数并求平方,取前5个
auto even_squared = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(5);
for (int n : even_squared) {
std::cout << n << " "; // 输出: 4 16 36 64 100
}
详细用法
filter — 条件过滤
#include <ranges>
#include <vector>
#include <string>
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> students = {
{"张三", 85}, {"李四", 62}, {"王五", 91}, {"赵六", 58}
};
// 筛选成绩及格的学生
auto passed = students
| std::views::filter([](const Student& s) { return s.score >= 60; });
for (const auto& s : passed) {
std::cout << s.name << ": " << s.score << std::endl;
}
return 0;
}
transform — 元素转换
#include <ranges>
#include <vector>
int main() {
std::vector<std::string> words = {"hello", "world", "cpp20"};
// 转换为大写(简化示例,实际需逐字符转换)
auto lengths = words
| std::views::transform([](const std::string& s) { return s.size(); });
for (size_t len : lengths) {
std::cout << len << " "; // 输出: 5 5 4
}
return 0;
}
take 和 drop — 截取与跳过
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {10, 20, 30, 40, 50, 60, 70};
// 取前3个
auto first3 = data | std::views::take(3);
for (int n : first3) std::cout << n << " "; // 10 20 30
std::cout << std::endl;
// 跳过前3个
auto skip3 = data | std::views::drop(3);
for (int n : skip3) std::cout << n << " "; // 40 50 60 70
std::cout << std::endl;
// 组合使用:取中间部分
auto middle = data | std::views::drop(2) | std::views::take(3);
for (int n : middle) std::cout << n << " "; // 30 40 50
return 0;
}
reverse 和 keys/values
#include <ranges>
#include <map>
#include <iostream>
int main() {
std::map<std::string, int> scores = {
{"语文", 90}, {"数学", 85}, {"英语", 92}
};
// 遍历键
for (const auto& key : std::views::keys(scores)) {
std::cout << key << " "; // 数学 英语 语文(按字典序)
}
std::cout << std::endl;
// 遍历值并反转
auto reversed_values = std::views::values(scores) | std::views::reverse;
for (int v : reversed_values) {
std::cout << v << " "; // 92 85 90
}
return 0;
}
join — 展平嵌套范围
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4, 5}, {6}};
// 展平为一维
auto flat = nested | std::views::join;
for (int n : flat) {
std::cout << n << " "; // 输出: 1 2 3 4 5 6
}
return 0;
}
常见场景
数据清洗管道
#include <ranges>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> raw_data = {0, -3, 5, 0, 8, -1, 12, 0, 7};
// 清洗流程:去除零值 -> 过滤负数 -> 排序 -> 去重
auto cleaned = raw_data
| std::views::filter([](int n) { return n != 0; })
| std::views::filter([](int n) { return n > 0; });
// 排序和去重需要复制到容器(sort 会修改原数据)
std::vector<int> result(cleaned.begin(), cleaned.end());
std::ranges::sort(result);
result.erase(std::ranges::unique(result).begin(), result.end());
for (int n : result) {
std::cout << n << " "; // 输出: 5 7 8 12
}
return 0;
}
字符串分割与处理
#include <ranges>
#include <string>
#include <iostream>
int main() {
std::string text = "hello world from cpp20 ranges";
// 按空格分割字符串(C++20 lazy split)
auto words = std::views::split(text, ' ');
for (const auto& word : words) {
// word 是一个子范围,需要构造为 string
std::string s(word.begin(), word.end());
std::cout << s << std::endl;
}
return 0;
}
注意事项
- 视图是惰性求值的,每次迭代都会重新计算,如果需要多次遍历结果,应将视图复制到容器中
- 视图不拥有数据,底层容器被销毁后视图将变为悬空引用,使用时需确保底层容器的生命周期
std::views::filter和std::views::transform返回的视图不满足common_range,其end()返回哨兵而非迭代器,某些需要双向迭代的场景需注意- 部分视图(如
filter)的迭代器性能略低于原生循环,在对性能极其敏感的热路径中应进行基准测试 std::views::split在 C++20 中返回的子范围类型使用不便,C++23 的std::views::split改进了接口- Ranges 算法(如
std::ranges::sort)会直接修改原容器,与视图的惰性语义不同,使用时需区分
进阶用法
自定义视图适配器
#include <ranges>
#include <concepts>
// 自定义视图:每 N 个元素取一个(采样)
template<std::ranges::view V>
struct SampleView : std::ranges::view_interface<SampleView<V>> {
V base_;
std::size_t step_;
// 迭代器实现(简化版)
class iterator {
std::ranges::iterator_t<V> current_;
std::ranges::sentinel_t<V> end_;
std::size_t step_;
public:
iterator(std::ranges::iterator_t<V> cur,
std::ranges::sentinel_t<V> end,
std::size_t step)
: current_(cur), end_(end), step_(step) {}
auto operator*() const { return *current_; }
iterator& operator++() {
// 前进 step 步
for (std::size_t i = 0; i < step_ && current_ != end_; ++i) {
++current_;
}
return *this;
}
};
auto begin() { return iterator{base_.begin(), base_.end(), step_}; }
auto end() { return std::default_sentinel; }
};
// 适配器闭包对象(支持管道语法)
struct SampleFn {
std::size_t step;
template<std::ranges::viewable_range R>
auto operator()(R&& r) const {
return SampleView<std::views::all_t<R>>{
std::views::all(std::forward<R>(r)), step
};
}
};
// 使用方式
// auto sampled = data | SampleFn{3}; // 每3个取1个
C++23 新增视图
#include <ranges>
// enumerate: 带索引遍历
std::vector<std::string> items = {"apple", "banana", "cherry"};
for (auto [index, value] : std::views::enumerate(items)) {
std::cout << index << ": " << value << std::endl;
}
// zip: 合并多个范围
std::vector<int> ids = {1, 2, 3};
std::vector<std::string> names = {"张三", "李四", "王五"};
for (auto [id, name] : std::views::zip(ids, names)) {
std::cout << id << " - " << name << std::endl;
}
// chunk: 分块处理
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7};
for (auto chunk : std::views::chunk(data, 3)) {
// 第一轮: {1, 2, 3},第二轮: {4, 5, 6},第三轮: {7}
for (int n : chunk) std::cout << n << " ";
std::cout << std::endl;
}
// slide: 滑动窗口
for (auto window : std::views::slide(data, 3)) {
// {1,2,3}, {2,3,4}, {3,4,5}, {4,5,6}, {5,6,7}
for (int n : window) std::cout << n << " ";
std::cout << std::endl;
}
Ranges 与投影(Projection)
Ranges 算法支持投影参数,避免编写简单的 lambda:
#include <ranges>
#include <algorithm>
#include <vector>
struct Employee {
std::string name;
int age;
double salary;
};
int main() {
std::vector<Employee> employees = {
{"张三", 28, 15000.0},
{"李四", 35, 22000.0},
{"王五", 24, 12000.0}
};
// 按年龄排序,使用投影替代 lambda
std::ranges::sort(employees, {}, &Employee::age);
// 按薪资降序排序
std::ranges::sort(employees, std::greater{}, &Employee::salary);
// 查找薪资最高的员工
auto it = std::ranges::max_element(employees, {}, &Employee::salary);
std::cout << "最高薪资: " << it->name << std::endl;
return 0;
}
视图基础
基本写法:filter 过滤
<range> | std::views::filter(<谓词>);
// 过滤出偶数
auto evens = v | std::views::filter([](int x) { return x % 2 == 0; });
基本写法:transform 转换
<range> | std::views::transform(<函数>);
// 每个元素乘以 2
auto doubled = v | std::views::transform([](int x) { return x * 2; });
基本写法:take 取前 N 个
<range> | std::views::take(<数量>);
// 取前 5 个元素
auto first5 = v | std::views::take(5);
基本写法:drop 跳过前 N 个
<range> | std::views::drop(<数量>);
// 跳过前 3 个元素
auto rest = v | std::views::drop(3);
基本写法:reverse 反转
<range> | std::views::reverse;
// 反转顺序
auto reversed = v | std::views::reverse;
链式组合
基本写法:管道操作符组合
<range> | std::views::filter(<谓词>) | std::views::transform(<函数>);
// 过滤偶数并乘以 10
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 10; });
基本写法:取前 N 个并反转
<range> | std::views::take(<数量>) | std::views::reverse;
// 取前 3 个并反转
auto result = v | std::views::take(3) | std::views::reverse;
生成视图
基本写法:iota 递增序列
std::views::iota(<起始> [, <结束>]);
// 生成 0 到 9
auto nums = std::views::iota(0, 10);
// 生成无限序列(配合 take)
auto infinite = std::views::iota(0) | std::views::take(100);
基本写法:repeat 重复
std::views::repeat(<值> [, <次数>]);
// 重复 5 五次(无限需配合 take)
auto fives = std::views::repeat(5, 3);
基本写法:empty 单元素视图
std::views::single(<值>);
// 包含单个元素的视图
auto one = std::views::single(42);
元素访问
基本写法:遍历 range
for (auto&& <元素> : <range>) { ... }
// 遍历过滤后的视图
for (int x : v | std::views::filter([](int n) { return n > 0; })) {
std::cout << x;
}
常用算法
基本写法:ranges::sort 排序
std::ranges::sort(<range> [, <比较器>]);
// 对容器排序
std::ranges::sort(v);
// 降序
std::ranges::sort(v, std::greater<int>());
基本写法:ranges::find 查找
std::ranges::find(<range>, <值>);
// 查找值为 5 的元素
auto it = std::ranges::find(v, 5);
基本写法:ranges::min_element 最小值
std::ranges::min_element(<range>);
// 查找最小元素
auto it = std::ranges::min_element(v);
基本写法:ranges::count 计数
std::ranges::count(<range>, <值>);
// 统计值为 3 的个数
auto n = std::ranges::count(v, 3);
基本写法:ranges::transform 转换
std::ranges::transform(<range>, <输出迭代器>, <函数>);
// 转换并存入另一容器
std::ranges::transform(v, std::back_inserter(dest), [](int x) { return x * 2; });