全面深入了解C++20 范围库(std::ranges)

目录

1.核心基础概念

1.1.范围 (std::ranges::range)

1.2.哨兵 (Sentinel)

1.3.视图 (std::ranges::view)

1.4.借用范围 (borrowed_range)

[1.5.管道语法 |](#1.5.管道语法 |)

[2.std::ranges 整体分类](#2.std::ranges 整体分类)

[3.范围算法(std::ranges 算法)](#3.范围算法(std::ranges 算法))

3.1.语法差异

3.2.核心增强:投影 (Projection)

3.3.算法详解

[3.3.1.遍历 & 逻辑判断算法](#3.3.1.遍历 & 逻辑判断算法)

3.3.2.查找算法

3.3.3.计数算法

3.3.4.最值算法

[3.3.5.排序 & 分区算法](#3.3.5.排序 & 分区算法)

3.3.6.二分查找算法(要求范围必须升序有序)

3.3.7.反转、旋转、相邻去重(原地修改)

3.3.8.复制、变换、填充算法

[3.3.9.元素删除算法(C++20 重磅优化)](#3.3.9.元素删除算法(C++20 重磅优化))

3.3.10.有序集合算法(两个输入范围都必须升序有序)

[4.视图适配器 & 视图工厂](#4.视图适配器 & 视图工厂)

[4.1.常用视图适配器(C++20 标准)](#4.1.常用视图适配器(C++20 标准))

4.2.视图工厂(生成新范围)

4.3.链式管道组合

[5.std::ranges::subrange 子范围](#5.std::ranges::subrange 子范围)

[6.C++20 Ranges vs 传统 STL 写法 对比](#6.C++20 Ranges vs 传统 STL 写法 对比)

6.1.整体对比

6.2.分场景代码对比

6.3.核心特性深度对比(底层能力差异)

6.4.性能对比

7.迁移与使用建议

[8.C++23 对 Ranges 的扩展(补充)](#8.C++23 对 Ranges 的扩展(补充))

9.总结


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 算法直接接收整个范围,写法大幅简化。

C++标准库之std::begin、std::end、std::pre和std::next

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_forsized_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所有元素 满足条件 → true
  • any_of至少一个元素 满足条件 → true
  • none_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.最值算法

查找最大 / 最小元素,返回 subrangeminmax_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()); // 彻底清理

全局去重标准流程:sortuniqueerase

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::mapstd::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

  • 传统:原始迭代器,跨函数传递、二次使用易失效、语义模糊;
  • Rangessubrange 是独立范围类型,支持遍历、判空、再次接入算法 / 视图,链式能力更强

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++ 序列处理的新标准,替代传统迭代器 + 算法;
  • 核心优势:语法简洁、链式可读、惰性求值(高性能)、原生投影、组合能力极强;
  • 使用原则 :优先用视图做变换,必要时物化容器;区分「视图(只读)」和「算法(原地修改)」;

推荐阅读:

范围库 (C++20 起) - cppreference.com

相关推荐
小小龙学IT3 天前
C++20 协程深度解析:从原理到高性能异步框架实战
junit·c++20
楼田莉子7 天前
C++20新特性:协程
开发语言·c++·后端·学习·c++20
Trouvaille ~8 天前
【Redis篇】Redis 哨兵(Sentinel):高可用自动故障转移
数据库·redis·缓存·中间件·sentinel·高可用·哨兵
ouliten10 天前
C++笔记:C++20风格线程池
c++·笔记·c++20
眠りたいです11 天前
现代C++:C++17中的新库特性
开发语言·c++·c++20·c++17
Fcy64813 天前
Linux下 进程间通信详解(一)管道、进程池与简单的Linux 进程间聊天室
linux·服务器·管道·进程间通信·进程池
楼田莉子18 天前
C++20新特性:Range库
开发语言·c++·后端·学习·c++20
楼田莉子19 天前
C++20现代特性:概念与约束
开发语言·c++·后端·学习·c++20
aluluka19 天前
C++ 20 协程的探索
c++·c++20