目录
[1.5.管道语法 |](#1.5.管道语法 |)
[2.std::ranges 整体分类](#2.std::ranges 整体分类)
[3.范围算法(std::ranges 算法)](#3.范围算法(std::ranges 算法))
[3.3.1.遍历 & 逻辑判断算法](#3.3.1.遍历 & 逻辑判断算法)
[3.3.5.排序 & 分区算法](#3.3.5.排序 & 分区算法)
[3.3.9.元素删除算法(C++20 重磅优化)](#3.3.9.元素删除算法(C++20 重磅优化))
[4.视图适配器 & 视图工厂](#4.视图适配器 & 视图工厂)
[4.1.常用视图适配器(C++20 标准)](#4.1.常用视图适配器(C++20 标准))
[5.std::ranges::subrange 子范围](#5.std::ranges::subrange 子范围)
[6.C++20 Ranges vs 传统 STL 写法 对比](#6.C++20 Ranges vs 传统 STL 写法 对比)
[8.C++23 对 Ranges 的扩展(补充)](#8.C++23 对 Ranges 的扩展(补充))
1.核心基础概念
C++20 正式引入范围库 (std::ranges) ,是对传统 STL 迭代器 + 算法体系的重大升级。它基于概念 (Concept) 、范围 (Range) 、视图 (View) 、管道语法 、投影 (Projection) 五大核心能力,解决了传统 STL 写法繁琐、组合困难、易产生临时对象等问题,实现线性链式调用、惰性求值、零开销视图。
1.1.范围 (std::ranges::range)
范围 是满足 range 概念的类型:只要类型拥有合法的 begin() 和 end(),返回迭代器 / 哨兵,就是一个范围。
- 所有标准容器(
std::vector/std::string/std::map/ 数组)天然是范围; - 范围描述一个可遍历的元素序列,不一定拥有实际数据。
传统 STL 必须传 begin()/end(),Ranges 算法直接接收整个范围,写法大幅简化。
1.2.哨兵 (Sentinel)
C++20 哨兵 (Sentinel) 是范围库 (Ranges) 引入的核心概念之一,是对传统迭代器模型的重大泛化 。它彻底改变了序列结束位置的表示方式,允许迭代器与结束标记类型不同,从而支持无界范围、自定义终止条件、优化边界判断等场景,为现代 C++ 序列处理提供了极大灵活性。
哨兵 是标记范围 (Range) 结束位置的对象,满足 std::sentinel_for<I> 概念,其中 I 是对应的起始迭代器类型。它的核心作用是与迭代器进行相等性比较,判断遍历是否结束,而不必与迭代器是同一类型。
C++20 范围的定义:一个对象只要提供 begin()(返回迭代器)和 end()(返回哨兵),且满足 sentinel_for<decltype(end()), decltype(begin())>,就是合法的范围。
与传统迭代器的根本区别:
| 特性 | 传统 STL 迭代器对 | C++20 迭代器 + 哨兵 |
|---|---|---|
| 类型要求 | begin() 和 end() 必须返回同类型迭代器 |
end() 可返回任意类型 哨兵,只要满足 sentinel_for 概念 |
| 结束判断 | 迭代器相等比较 | 迭代器与哨兵的自定义比较逻辑 |
| 适用场景 | 有限、已知边界的序列 | 有限 / 无限序列、自定义终止条件、输入流等 |
| 性能 | 边界判断固定为指针比较 | 可优化为更高效的判断逻辑(如直接检查值) |
核心概念:sentinel_for 与 sized_sentinel_for:
std::sentinel_for<S, I>:核心约束,要求S类型对象可与I类型迭代器比较(operator==),判断是否到达序列末尾std::sized_sentinel_for<S, I>:增强约束,要求哨兵与迭代器之间可计算距离(operator-),支持std::ranges::distance等算法
简化示例:值边界哨兵
更简洁的哨兵实现,用于固定值边界判断:
cpp
#include <ranges>
#include <iostream>
// 边界哨兵:当迭代器值等于bound时终止
template <typename T>
struct BoundSentinel {
T bound;
// 比较函数:迭代器值 == 边界值时终止
friend bool operator==(const auto& it, const BoundSentinel& sentinel) {
return *it == sentinel.bound;
}
friend bool operator!=(const auto& it, const BoundSentinel& sentinel) {
return !(it == sentinel);
}
};
int main() {
int arr[] = {1, 2, 3, 4, 5, 0, 6, 7}; // 0作为终止标记
auto range = std::ranges::subrange(std::begin(arr), BoundSentinel<int>{0});
for (int x : range) {
std::cout << x << " "; // 输出 1 2 3 4 5
}
return 0;
}
1.3.视图 (std::ranges::view)
视图是 Ranges 最核心的组件,也是和普通范围最大的区别:
- 轻量级、非拥有 :视图不拷贝、不持有原始数据,仅对原有范围做 "遍历规则包装";
- 廉价拷贝 / 移动:拷贝视图几乎无开销;
- 惰性求值 (Lazy) :仅在实际遍历时才计算元素,链式组合不会产生中间临时容器;
- 不修改原数据:所有视图都是只读变换。
区分:范围 = 序列本身 ,视图 = 序列的 "透视滤镜"。
1.4.借用范围 (borrowed_range)
用于保证临时范围可以被视图安全引用,避免悬垂引用。
例如 std::vector{1,2,3} | filter(...),临时容器生命周期会被视图合理托管。
1.5.管道语法 |
Ranges 专属链式语法,格式:
cpp
原始范围 | 适配器1 | 适配器2 | 适配器3
等价于函数嵌套:适配器3(适配器2(适配器1(原始范围))) ,从左到右依次执行,代码线性可读。
2.std::ranges 整体分类
范围库全部位于 std::ranges 命名空间下,分为四大模块:
- 范围算法:对标传统 STL 算法,增强参数、支持范围 / 投影;
- 视图适配器:基于已有范围生成新视图(过滤、变换、截取等);
- 视图工厂:不依赖原有范围,直接生成全新范围(如连续数列);
- 范围概念 :模板约束(
range/view/input_range等,用于模板编程)。
3.范围算法(std::ranges 算法)
3.1.语法差异
形式1:范围重载(推荐,日常首选)
传统 STL 算法:必须传递首尾迭代器
cpp
std::sort(vec.begin(), vec.end());
C++20 Ranges 算法:直接传入整个范围
cpp
// 模板原型
template <std::ranges::input_range R, ...>
void 算法名(R&& range, 其他参数...);
std::ranges::sort(vec);
形式2:迭代器 + 哨兵重载(兼容传统 / 自定义哨兵)
兼容旧式迭代器写法,同时原生支持迭代器与哨兵类型不同(C++20 哨兵特性),传统 STL 算法强制首尾迭代器同类型。
cpp
// 模板原型
template <std::input_iterator I, std::sentinel_for<I> S, ...>
void 算法名(I first, S last, 其他参数...);
// 示例:传统迭代器 + 标准哨兵
std::ranges::sort(vec.begin(), vec.end());
// 示例:迭代器 + 自定义哨兵(C字符串)
auto cstr = "hello";
std::ranges::for_each(cstr, std::default_sentinel, [](char c){});
3.2.核心增强:投影 (Projection)
投影 是一个可调用对象,接收单个元素,返回参与算法逻辑的值 ,全程只读、不修改原数据。 专门用于提取结构体成员、计算衍生值,大幅简化结构体 /std::pair 的排序、查找、统计逻辑。
绝大多数 Ranges 算法新增投影参数 ,用于提取元素的成员 / 属性,替代繁琐的 Lambda,是高频用法。
语法规则(通用):
cpp
std::ranges::算法(范围, 谓词, 投影函数);
- 投影函数:接收单个元素,返回用于比较 / 判断的值;
- 投影不会修改原元素,仅做 "取值映射"。
三种主流写法:
1.成员函数指针(最常用,结构体优先)
cpp
struct Student { std::string name; int score; };
// 投影:提取 score 成员
std::ranges::sort(stus, std::ranges::less<>, &Student::score);
2.Lambda 表达式(自定义计算 / 复杂取值)
cpp
// 投影:取元素绝对值参与比较
std::ranges::sort(vec, std::ranges::less<>, [](int x){ return std::abs(x); });
3.普通函数 / 函数对象(逻辑复用)
核心特点:
- 投影不修改原元素,仅做「值映射」;
- 算法先执行投影,再用投影结果做比较 / 判断;
- 所有谓词、比较器接收的都是投影后的值。
3.3.算法详解
绝大多数范围算法遵循固定参数顺序,按功能分为 5 大类,记住规则就不用死记函数签名:
| 算法分类 | 通用参数顺序 | 代表算法 |
|---|---|---|
| 等值匹配类 | 范围, 目标值, 投影 = std::identity |
count / find / contains |
| 谓词判断类 | 范围, 谓词函数, 投影 = std::identity |
all_of / find_if / count_if |
| 比较排序类 | 范围, 比较器 = ranges::less<>, 投影 = std::identity |
sort / max_element / partition |
| 复制 / 变换类 | 源范围, 目标迭代器, [谓词/变换函数] |
copy / copy_if / transform |
| 条件删除类(专属) | 容器, [目标值/谓词], 投影 = std::identity |
erase / erase_if |
3.3.1.遍历 & 逻辑判断算法
纯只读操作,不修改原范围,用于遍历元素、批量判断整体条件。
1.std::ranges::for_each 遍历执行
对每个元素执行自定义操作,等价传统 std::for_each。
cpp
int main() {
std::vector<int> vec = {1,2,3,4,5};
// 基础遍历
std::ranges::for_each(vec, [](int x){
std::cout << x << " ";
});
return 0;
}
2.all_of / any_of / none_of 全局条件判断
all_of:所有元素 满足条件 →trueany_of:至少一个元素 满足条件 →truenone_of:没有元素 满足条件 →true
cpp
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> stus = {{"A", 85}, {"B", 59}, {"C", 90}};
// 投影取 score:判断是否全部及格(>=60)
bool all_pass = std::ranges::all_of(stus, [](int s){ return s >= 60; }, &Student::score);
// 判断是否存在满分
bool has_full = std::ranges::any_of(stus, [](int s){ return s == 100; }, &Student::score);
// 判断是否无人不及格
bool no_fail = std::ranges::none_of(stus, [](int s){ return s < 60; }, &Student::score);
std::cout << std::boolalpha << all_pass << " " << has_full << " " << no_fail;
return 0;
}
3.3.2.查找算法
定位元素位置,全部返回 subrange,支持投影,分为正向查找、反向查找、存在性判断。
1.find / find_if / find_if_not 正向查找
find(range, val):查找第一个等于目标值的元素find_if(range, pred):查找第一个满足谓词的元素find_if_not(range, pred):查找第一个不满足谓词的元素
cpp
int main() {
std::vector<int> vec = {10,20,30,40,50};
// 查找值为30的元素
auto r1 = std::ranges::find(vec, 30);
if (!r1.empty()) std::cout << "找到:" << *r1.begin() << "\n";
// 查找第一个大于25的元素
auto r2 = std::ranges::find_if(vec, [](int x){ return x > 25; });
return 0;
}
2.find_last / find_last_if 反向查找(C++20 新增)
从尾部向前查找,传统 STL 需要手写反向迭代器,此处写法大幅简化。
cpp
std::vector<int> vec = {2, 1, 2, 3, 2};
auto last_two = std::ranges::find_last(vec, 2); // 最后一个 2
3.contains 存在性判断(极简接口)
直接返回 bool,判断范围是否包含目标值,无需判断迭代器 / 子范围。
cpp
std::vector<int> vec = {1,2,3,4};
bool has_3 = std::ranges::contains(vec, 3); // true
bool has_9 = std::ranges::contains(vec, 9); // false
3.3.3.计数算法
统计符合条件的元素数量,支持投影。
1.count 统计等值元素
统计等于目标值的元素个数。
2.count_if 统计条件元素
统计满足谓词的元素个数。
cpp
struct User {
std::string name;
int age;
};
int main() {
std::vector<User> users = {{"Tom", 18}, {"Lily", 22}, {"Jack", 18}};
// 投影 age:统计 18 岁人数
int cnt18 = std::ranges::count(users, 18, &User::age);
// 统计年龄 > 20 的人数
int cntAdult = std::ranges::count_if(users, [](int a){ return a > 20; }, &User::age);
std::cout << "18岁人数:" << cnt18 << "\n";
return 0;
}
3.3.4.最值算法
查找最大 / 最小元素,返回 subrange;minmax_element 一次遍历同时获取最值,效率更高。
cpp
struct Player {
std::string name;
int level;
};
int main() {
std::vector<Player> players = {{"P1", 10}, {"P2", 20}, {"P3", 15}};
// 等级最高的玩家
auto maxLv = std::ranges::max_element(players, std::ranges::less<>, &Player::level);
// 同时获取最小、最大等级
auto [minR, maxR] = std::ranges::minmax_element(players, std::ranges::less<>, &Player::level);
std::cout << "最高等级:" << maxR->level;
return 0;
}
3.3.5.排序 & 分区算法
原地修改范围顺序,投影在此类算法中价值最高,是工程最常用算法。
1.sort / stable_sort 排序
sort:快速排序,不稳定排序(相等元素相对位置改变)stable_sort:稳定排序,保留相等元素原有相对位置
语法:
sort(范围, 比较器 = ranges::less<>, 投影 = identity)
cpp
struct Goods {
std::string name;
double price;
int stock;
};
int main() {
std::vector<Goods> goods = {
{"Apple", 5.5, 100},
{"Banana", 3.2, 200},
{"Orange", 4.0, 80}
};
// 1. 按价格【升序】(默认 less<>)
std::ranges::sort(goods, std::ranges::less<>, &Goods::price);
// 2. 按库存【降序】(使用 greater<>)
std::ranges::sort(goods, std::ranges::greater<>, &Goods::stock);
return 0;
}
2.partial_sort 局部排序
仅对前 N 个元素排序,剩余元素无序,适合「取 TopN」场景,性能优于全排序。
cpp
std::vector<int> vec = {5,3,9,1,7,2};
// 只排序前 3 个元素
std::ranges::partial_sort(vec, vec.begin() + 3);
// 结果:1 2 3 | 9 7 5(后半段无序)
3.nth_element 第 N 元素定位
将第 N 个位置放置全局第 N 小元素,左侧都 ≤ 它,右侧都 ≥ 它,两侧无序。常用于快速取中位数、TopK。
cpp
std::vector<int> vec = {9,5,1,7,3};
// 让第2个位置(下标2)为全局第3小元素
std::ranges::nth_element(vec, vec.begin() + 2);
4.partition / stable_partition 分区
按条件将范围分为两部分:满足条件在前,不满足在后。
partition:不稳定分区stable_partition:稳定分区,保留分组内原有顺序
cpp
std::vector<int> vec = {1,2,3,4,5,6};
// 偶数放前面,奇数放后面
std::ranges::partition(vec, [](int x){ return x % 2 == 0; });
3.3.6.二分查找算法(要求范围必须升序有序)
基于二分查找,时间复杂度 (O(log N)),无序范围使用会导致未定义行为。
| 算法 | 功能 | 返回值 |
|---|---|---|
binary_search |
判断元素是否存在 | bool |
lower_bound |
第一个 ≥ 目标值 的位置 | subrange |
upper_bound |
第一个 > 目标值 的位置 | subrange |
equal_range |
所有等于目标值的连续区间 | subrange |
cpp
int main() {
std::vector<int> vec = {1,2,3,4,5,6}; // 必须有序
bool exist = std::ranges::binary_search(vec, 4); // true
auto left = std::ranges::lower_bound(vec, 3);
auto right = std::ranges::upper_bound(vec, 3);
auto eqRange = std::ranges::equal_range(vec, 3);
return 0;
}
3.3.7.反转、旋转、相邻去重(原地修改)
1.reverse 原地反转
std::ranges::reverse(vec):算法,原地修改容器顺序;vec | std::ranges::reverse:视图,仅改变遍历顺序,原数据不变。
cpp
std::vector<int> vec = {1,2,3};
std::ranges::reverse(vec); // 原地变为 {3,2,1}
2.rotate 序列旋转
以指定迭代器为新起点,循环平移整个序列。
cpp
std::vector<int> vec = {1,2,3,4,5};
// 从第3个元素开始旋转 → 3 4 5 1 2
std::ranges::rotate(vec, vec.begin() + 2);
3.unique 相邻去重
仅删除相邻重复元素 (和传统行为一致),重复元素会被移到尾部,通常配合 erase 彻底删除。
cpp
std::vector<int> vec = {1,1,2,2,3,3};
auto newEnd = std::ranges::unique(vec); // 去重,尾部残留重复值
vec.erase(newEnd.begin(), newEnd.end()); // 彻底清理
全局去重标准流程:
sort→unique→erase
3.3.8.复制、变换、填充算法
用于元素拷贝、类型转换、批量赋值,常配合 std::back_inserter 动态扩容目标容器。
1.copy / copy_if 复制
copy:完整复制所有元素copy_if:仅复制满足条件的元素
cpp
std::vector<int> src = {1,2,3,4,5};
std::vector<int> dst;
// 复制所有偶数
std::ranges::copy_if(src, std::back_inserter(dst), [](int x){ return x%2 == 0; });
2.transform 元素变换
对每个元素执行映射转换,支持单范围 和双范围重载。
cpp
std::vector<int> src = {1,2,3};
std::vector<int> dst;
// 所有元素 *2 后复制
std::ranges::transform(src, std::back_inserter(dst), [](int x){ return x * 2; });
3.fill / generate 批量填充
fill:用固定值填充范围generate:用生成器函数动态生成元素
cpp
std::vector<int> vec(5);
std::ranges::fill(vec, 0); // 全部填 0
std::ranges::generate(vec, [](){ // 生成 1,2,3,4,5
static int n = 1;
return n++;
});
3.3.9.元素删除算法(C++20 重磅优化)
彻底淘汰传统 erase + remove 老旧范式,一键删除,代码极简。
1.std::ranges::erase 删除指定值
删除容器中所有等于目标值的元素,直接修改容器。
cpp
std::vector<int> vec = {2,1,2,3,2};
std::ranges::erase(vec, 2); // 删除所有 2 → {1,3}
2.std::ranges::erase_if 条件删除
删除所有满足谓词的元素,支持投影(结构体场景利器)。
cpp
struct User { std::string name; int age; };
std::vector<User> users = {{"U1", 16}, {"U2", 20}, {"U3", 15}};
// 投影 age:删除年龄 < 18 的用户
std::ranges::erase_if(users, [](int a){ return a < 18; }, &User::age);
对比传统写法(冗余且易错):
// 传统 STL 写法(已淘汰) vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
3.3.10.有序集合算法(两个输入范围都必须升序有序)
对两个有序范围做集合运算,结果输出到目标迭代器。
set_union:并集set_intersection:交集set_difference:差集(A - B)set_symmetric_difference:对称差集(只在其中一个集合出现)
cpp
int main() {
std::vector<int> a = {1,2,3,4};
std::vector<int> b = {3,4,5,6};
std::vector<int> res;
// 求交集 {3,4}
std::ranges::set_intersection(a, b, std::back_inserter(res));
return 0;
}
4.视图适配器 & 视图工厂
视图是 Ranges 组合能力的核心,惰性求值、无临时内存分配 ,通过管道 | 链式组合。
- 视图适配器:作用在已有范围上,生成变换后的视图;
- 视图工厂:不依赖原有范围,直接生成新序列。
4.1.常用视图适配器(C++20 标准)
1.std::ranges::filter 过滤
保留谓词返回 true 的元素。
cpp
std::vector<int> vec = {1,2,3,4,5,6};
// 过滤偶数
auto even_view = vec | std::ranges::filter([](int x) { return x % 2 == 0; });
for (int x : even_view) std::cout << x << " "; // 2 4 6
2.std::ranges::transform 元素变换
对每个元素执行映射转换。
cpp
// 所有元素 * 2
auto double_view = vec | std::ranges::transform([](int x) { return x * 2; });
3.take / take_while 截取前缀
take(N):取前N个元素;take_while(谓词):直到条件不成立为止。
cpp
// 取前3个元素
auto take3 = vec | std::ranges::take(3); // 1 2 3
// 取小于4的元素
auto take_less4 = vec | std::ranges::take_while([](int x){ return x < 4; });
4.drop / drop_while 跳过前缀
drop(N):跳过前N个元素;drop_while(谓词):跳过开头满足条件的元素。
cpp
auto drop2 = vec | std::ranges::drop(2); // 3 4 5 6
5.reverse 逆序视图
仅改变遍历顺序,不修改原容器 (和 std::ranges::reverse 算法区分)。
cpp
auto rev_view = vec | std::ranges::reverse;
6.keys / values 键值视图
专门用于 std::map、std::pair 序列,快速提取键 / 值。
cpp
#include <map>
std::map<std::string, int> mp = {{"a",1}, {"b",2}};
// 提取所有 key
auto keys = mp | std::ranges::keys;
// 提取所有 value
auto vals = mp | std::ranges::values;
7.split 分割范围
按分隔符拆分序列,返回范围的范围 (二维视图),常配合 join 使用。
cpp
#include <string>
std::string str = "hello,world,cpp20";
// 按逗号分割
auto split_view = str | std::ranges::split(',');
8.join 扁平化嵌套范围
将二维 / 多层视图拍平为一维。
cpp
// 分割后扁平化遍历
for (auto sub : str | std::ranges::split(',') | std::ranges::join) {
std::cout << sub;
}
9.unique 相邻去重视图
仅去除相邻重复 元素(和 std::unique 行为一致)。
4.2.视图工厂(生成新范围)
1.std::ranges::iota 连续数列
生成递增整数序列,支持有限范围、无限范围。
cpp
// 有限范围:[0, 10)
auto nums = std::ranges::iota(0, 10);
// 无限范围:从0开始无限递增(必须搭配 take 截断)
auto inf_nums = std::ranges::iota(0) | std::ranges::take(5);
2.single_view / empty_view
single_view(x):仅包含单个元素的视图;empty_view<T>():空视图。
4.3.链式管道组合
多适配器串联,一行实现复杂逻辑,全程无临时容器:
cpp
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 逻辑:过滤奇数 → 元素*3 → 跳过前2个 → 取前3个 → 逆序
auto res_view = vec
| std::ranges::filter([](int x) { return x % 2 == 1; }) // 奇数:1,3,5,7,9
| std::ranges::transform([](int x) { return x * 3; }) // 3,9,15,21,27
| std::ranges::drop(2) // 15,21,27
| std::ranges::take(3)
| std::ranges::reverse;
for (int x : res_view) {
std::cout << x << " "; // 27 21 15
}
return 0;
}
5.std::ranges::subrange 子范围
绝大多数查找 / 区间算法返回 subrange,它是轻量级范围包装器:
- 内部仅存储一对「迭代器 / 哨兵」,不拷贝数据;
- 本身是合法
range,可直接遍历、接入管道视图、调用范围算法; - 常用成员:
.empty():判断是否为空(推荐,替代迭代器比较).begin()/.end():获取首尾迭代器 / 哨兵- 支持范围
for遍历
示例如下:
cpp
std::vector<int> vec = {1,2,3,4,5,6};
// 找到 3 之后的所有元素,再过滤偶数
auto sub = std::ranges::find(vec, 3);
for (int x : sub | std::ranges::filter([](int x){ return x%2==0; })) {
std::cout << x << " "; // 4 6
}
6.C++20 Ranges vs 传统 STL 写法 对比
C++20 范围库(std::ranges)完全兼容传统 STL 算法语义,但在调用形式、语法简洁度、组合能力、运行效率、容错性上做了全面革新。
6.1.整体对比
| 对比维度 | 传统 STL(C++11~C++17) | C++20 std::ranges | |
|---|---|---|---|
| 调用参数 | 强制传 begin() + end() 迭代器对 |
支持直接传整个容器 / 范围,也兼容迭代器对 | |
| 成员取值 | 依赖 Lambda 内部取结构体 / 成员,代码臃肿 | 原生支持投影 (Projection),一行指定字段 | |
| 结束标记 | begin/end 迭代器必须同类型 |
支持哨兵 (Sentinel),迭代器与结束标记可不同类型 | |
| 多步组合 | 多步处理必须创建临时容器存中间结果 | 视图 (View) 惰性求值,无临时容器,管道 ` | ` 链式串联 |
| 返回值 | 查找类返回原始迭代器 | 查找 / 区间类统一返回 std::ranges::subrange(可继续遍历 / 链式调用) |
|
| 删除逻辑 | 固定 erase + remove 嵌套范式,易错 |
专用 erase/erase_if,一键删除,语法极简 |
|
| 代码风格 | 嵌套多、模板长、重复代码多 | 线性链式、声明式风格,逻辑从上到下一目了然 | |
| 运行开销 | 多步变换伴随容器拷贝 / 内存分配 | 视图零拷贝、惰性执行,性能等价手写循环 | |
| 边界判断 | 手动比较迭代器,语义晦涩 | subrange.empty() 直观判空,哨兵封装终止逻辑 |
6.2.分场景代码对比
1.结构体(投影核心场景)
需求 :判断学生是否全部及格(按 score 字段)
cpp
struct Student {
std::string name;
int score;
};
传统 STL 写法: Lambda 内部手动提取成员,元素多时代码冗余
cpp
std::vector<Student> stus = {{"A",85}, {"B",66}, {"C",92}};
bool all_pass = std::all_of(stus.begin(), stus.end(), [](const Student& s){
return s.score >= 60; // 每次都要手动取成员
});
C++20 Ranges 写法: 使用投影直接指定成员指针,谓词只关心业务逻辑
cpp
std::vector<Student> stus = {{"A",85}, {"B",66}, {"C",92}};
// 投影 &Student::score:算法自动提取分数,谓词接收 int 类型
bool all_pass = std::ranges::all_of(stus, [](int s){
return s >= 60;
}, &Student::score);
差异总结:结构体场景下,投影彻底简化成员取值逻辑,Lambda 职责更单一。
2.元素删除(经典 erase-remove 范式)
这是传统 STL 最易错、最繁琐的场景之一,C++20 做了彻底优化。
传统STL:
cpp
users.erase(std::remove_if(users.begin(), users.end(),
[](const User& u){ return u.age < 18; }),
users.end());
Ranges 写法(搭配投影)
cpp
// 投影 age,谓词直接接收年龄值
std::ranges::erase_if(users, [](int a){ return a < 18; }, &User::age);
- 传统
erase+remove嵌套层级深,新手极易写错迭代器; - Ranges
erase/erase_if语义直白,一眼看懂逻辑。
3.多步序列流水线(过滤 + 变换 + 截取)
需求:从序列中 过滤奇数 → 元素 ×3 → 跳过前 2 个 → 取前 3 个
每一步都必须新建 vector 存储中间结果,多次内存分配 + 拷贝,性能损耗明显
cpp
int main() {
std::vector<int> src = {1,2,3,4,5,6,7,8,9,10};
// 第1步:过滤奇数(临时容器1)
std::vector<int> temp1;
std::copy_if(src.begin(), src.end(), std::back_inserter(temp1),
[](int x){ return x % 2 == 1; });
// 第2步:元素 *3(临时容器2)
std::vector<int> temp2;
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
[](int x){ return x * 3; });
// 第3步:跳过前2个,取前3个(最终结果)
std::vector<int> res;
auto first = temp2.begin() + 2;
auto last = first + 3;
std::copy(first, last, std::back_inserter(res));
// 遍历输出
for (int x : res) std::cout << x << " ";
return 0;
}
C++20 Ranges 视图 + 管道写法:
使用视图 (View) + 管道 | 链式组合,全程无临时容器、惰性求值,只有遍历瞬间才执行逻辑
cpp
int main() {
std::vector<int> src = {1,2,3,4,5,6,7,8,9,10};
// 链式流水线:从上到下顺序执行,无中间拷贝
auto pipeline = src
| std::ranges::filter([](int x){ return x % 2 == 1; }) // 过滤奇数
| std::ranges::transform([](int x){ return x * 3; }) // 元素*3
| std::ranges::drop(2) // 跳过前2个
| std::ranges::take(3); // 取前3个
// 仅遍历这一刻才执行所有逻辑
for (int x : pipeline) std::cout << x << " ";
return 0;
}
- 传统:多层临时容器,内存开销大、代码碎片化;
- Ranges:视图轻量包装、零拷贝,代码线性可读、性能更高;
- 视图是惰性:定义流水线时不执行,遍历才执行。
4.C 风格字符串(哨兵 Sentinel 典型应用)
需求 :遍历 \0 结尾的 C 字符串
传统写法:手动循环判断结束符 \0,或依赖迭代器指针
cpp
const char* str = "Hello C++";
while (*str != '\0') {
std::cout << *str;
++str;
}
Ranges 写法(结合默认哨兵 default_sentinel):
将 C 字符串包装为标准范围,哨兵自动识别 \0,无需手动判断终止条件
cpp
#include <ranges>
const char* str = "Hello C++";
auto str_range = std::ranges::subrange(str, std::default_sentinel);
std::ranges::for_each(str_range, [](char c){ std::cout << c; });
差异总结:哨兵抹平了 "原生指针序列" 和 "标准容器" 的遍历差异。
6.3.核心特性深度对比(底层能力差异)
1.投影 (Projection)
- 传统:无原生支持,所有成员取值、字段比较都塞在 Lambda 中,逻辑耦合;
- Ranges :算法内置投影参数,数据提取、业务判断、比较逻辑三者分离,代码复用率大幅提升。
2. 哨兵 (Sentinel)
- 传统 :强制
begin/end同类型,无法表达 "自定义终止条件、无限序列、C 字符串 / 流"; - Ranges :迭代器与结束标记可异构,原生支持无界范围、EOF、
\0等场景。
3. 视图 (View) 与 临时对象
- 传统:多步变换 → 必产生临时容器,触发内存分配、元素拷贝;
- Ranges :视图是轻量包装器 ,不持有数据、惰性执行,零额外内存开销,性能接近手写循环。
4. 管道语法 |
- 传统:算法只能嵌套调用,多层处理后代码向右 "阶梯式缩进",可读性差;
- Ranges:管道从左到右线性串联,逻辑顺序和代码书写顺序完全一致。
5. 返回值 subrange
- 传统:原始迭代器,跨函数传递、二次使用易失效、语义模糊;
- Ranges :
subrange是独立范围类型,支持遍历、判空、再次接入算法 / 视图,链式能力更强
6.4.性能对比
- 纯算法层 :
std::ranges算法底层复用传统 STL 实现,性能完全持平; - 多步序列处理 :Ranges 视图完胜,消除临时容器拷贝,内存占用更低;
- 结构体场景 :投影只是编译期映射,无运行时开销;
- 哨兵场景:部分场景可优化边界判断逻辑,小幅提升效率。
一句话:Ranges 是语法糖 + 架构升级,不牺牲性能,只提升开发效率与代码质量。
7.迁移与使用建议
1.新项目
直接全面使用 std::ranges:
- 容器遍历、排序、删除、筛选优先用范围算法;
- 多步变换优先用视图 + 管道;
- 结构体排序 / 统计优先使用投影。
2.老项目逐步迁移
- 简单算法(
for_each/sort/find):直接替换为std::ranges::xxx,改动极小; erase+remove范式:优先改成ranges::erase/erase_if,减少 bug;- 复杂多步处理:局部改用视图管道,不改动原有架构。
3.避坑要点
- 区分 范围算法(原地修改) 和 视图(只读);
- 无限视图(如
iota(0))必须用take/take_while截断,防止死循环; - 视图不持有数据,保证原始范围生命周期长于视图;
- 二分、集合算法依然要求范围有序,规则和传统一致;
- 迭代器失效规则与传统 STL 完全相同。
8.C++23 对 Ranges 的扩展(补充)
C++23 在 C++20 基础上新增大量实用适配器,进一步强化组合能力:
zip/zip_transform:多范围并行打包;chunk:按指定大小分块;adjacent:相邻元素视图;slide:滑动窗口视图;- 更多算法、投影、常量表达式支持。
9.总结
- 定位:C++20 Ranges 是现代 C++ 序列处理的新标准,替代传统迭代器 + 算法;
- 核心优势:语法简洁、链式可读、惰性求值(高性能)、原生投影、组合能力极强;
- 使用原则 :优先用视图做变换,必要时物化容器;区分「视图(只读)」和「算法(原地修改)」;
推荐阅读: