商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 全面深入了解C++20 范围库(std::ranges)

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

  发布于2026-07-05 阅读(0)

扫一扫,手机访问

1.核心基础概念

C++20 正式引入的范围库(std::ranges),可以看作是对传统 STL 迭代器与算法体系的一次重大升级。它建立在五大核心能力之上:概念(Concept)、范围(Range)、视图(View)、管道语法,以及投影(Projection)。这些能力专门为了解决传统 STL 写法的几个老大难问题——代码繁琐、组合困难、容易产生不必要的临时对象。最终实现的效果是:线性链式调用、惰性求值,以及零开销的视图操作。

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

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),是范围库中一个核心概念,它极大地泛化了传统的迭代器模型。它彻底改变了序列结束位置的表示方式——允许迭代器与结束标记的类型不同。这让无界范围、自定义终止条件、优化边界判断等场景成为可能,为现代 C++ 的序列处理提供了极大的灵活性。

简单来说,哨兵是一个标记范围结束位置的对象,它满足 std::sentinel_for 这个概念,这里的 I 是对应的起始迭代器类型。它的核心工作就是与迭代器进行相等性比较,来判断遍历是否结束,而不必要求它与迭代器是同一个类型。

在 C++20 中,一个对象只要提供 begin()(返回迭代器)和 end()(返回哨兵),并且满足 sentinel_for,那它就是一个合法的范围。

来看看它和传统迭代器的根本区别:

特性传统 STL 迭代器对C++20 迭代器 + 哨兵
类型要求begin() 和 end() 必须返回同类型迭代器end() 可返回任意类型哨兵,只要满足 sentinel_for 概念
结束判断迭代器相等比较迭代器与哨兵的自定义比较逻辑
适用场景有限、已知边界的序列有限 / 无限序列、自定义终止条件、输入流等
性能边界判断固定为指针比较可优化为更高效的判断逻辑(如直接检查值)

这里有两个核心概念需要理解:sentinel_for 和 sized_sentinel_for。

  • std::sentinel_for:这是核心约束,要求 S 类型的对象能与 I 类型的迭代器进行比较(operator==),从而判断是否到达序列末尾。
  • std::sized_sentinel_for:这是增强约束,要求哨兵与迭代器之间可以计算距离(operator-),这样就能支持 std::ranges::distance 这类算法。

看一个简化示例:值边界哨兵

这是一个更简洁的哨兵实现,用于固定值边界判断:

#include 
#include 

// 边界哨兵:当迭代器值等于bound时终止
template 
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{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 专属的链式语法,格式是这样的:

原始范围 | 适配器1 | 适配器2 | 适配器3

它等价于函数嵌套:适配器3(适配器2(适配器1(原始范围))),但代码是从左到右依次执行,可读性更强。

2.std::ranges 整体分类

范围库全部位于 std::ranges 命名空间下,主要分为四大模块:

  • 范围算法:对标传统 STL 算法,但增强了参数,支持范围 / 投影;
  • 视图适配器:基于已有范围生成新视图(过滤、变换、截取等);
  • 视图工厂:不依赖原有范围,直接生成全新的范围(比如连续数列);
  • 范围概念:模板约束(range/view/input_range 等,用于模板编程)。

3.范围算法(std::ranges 算法)

3.1.语法差异

形式1:范围重载(推荐,日常首选)

传统 STL 算法必须传递首尾迭代器:

std::sort(vec.begin(), vec.end());

C++20 Ranges 算法可以直接传入整个范围:

// 模板原型
template 
void 算法名(R&& range, 其他参数...);

std::ranges::sort(vec);

形式2:迭代器 + 哨兵重载(兼容传统 / 自定义哨兵)

这种形式兼容旧式迭代器写法,同时原生支持迭代器与哨兵类型不同(C++20 哨兵特性),而传统 STL 算法强制首尾迭代器同类型。

// 模板原型
template  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,这是非常高频率的用法。

语法规则(通用):

std::ranges::算法(范围, 谓词, 投影函数);
  • 投影函数:接收单个元素,返回用于比较或判断的值;
  • 投影不会修改原元素,只做“取值映射”。

三种主流写法:

1.成员函数指针(最常用,结构体优先)

struct Student { std::string name; int score; };
// 投影:提取 score 成员
std::ranges::sort(stus, std::ranges::less<>, &Student::score);

2.Lambda 表达式(自定义计算 / 复杂取值)

// 投影:取元素绝对值参与比较
std::ranges::sort(vec, std::ranges::less<>, [](int x){ return std::abs(x); });

3.普通函数 / 函数对象(逻辑复用)

核心特点:

  • 投影不修改原元素,只做“值映射”;
  • 算法会先执行投影,再用投影结果做比较或判断;
  • 所有谓词、比较器接收的都是投影后的值。

3.3.算法详解

绝大多数范围算法遵循固定的参数顺序,按功能可以分为 5 大类,记住这个规则,就不用死记硬背函数签名了:

算法分类通用参数顺序代表算法
等值匹配类范围, 目标值, 投影 = std::identitycount / find / contains
谓词判断类范围, 谓词函数, 投影 = std::identityall_of / find_if / count_if
比较排序类范围, 比较器 = ranges::less<>, 投影 = std::identitysort / max_element / partition
复制 / 变换类源范围, 目标迭代器, [谓词/变换函数]copy / copy_if / transform
条件删除类(专属)容器, [目标值/谓词], 投影 = std::identityerase / erase_if

3.3.1.遍历 & 逻辑判断算法

这些是纯只读操作,不会修改原范围,用于遍历元素、批量判断整体条件。

1.std::ranges::for_each 遍历执行

对每个元素执行自定义操作,等价于传统的 std::for_each。

int main() {
    std::vector 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
struct Student {
    std::string name;
    int score;
};

int main() {
    std::vector 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):查找第一个不满足谓词的元素
int main() {
    std::vector 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 需要手写反向迭代器,这里写法大幅简化了。

std::vector vec = {2, 1, 2, 3, 2};
auto last_two = std::ranges::find_last(vec, 2); // 最后一个 2

3.contains 存在性判断(极简接口)

直接返回 bool,判断范围是否包含目标值,无需判断迭代器或子范围。

std::vector 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 统计条件元素

统计满足谓词的元素个数。

struct User {
    std::string name;
    int age;
};

int main() {
    std::vector 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 一次遍历就能同时获取最值,效率更高。

struct Player {
    std::string name;
    int level;
};

int main() {
    std::vector 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)

struct Goods {
    std::string name;
    double price;
    int stock;
};

int main() {
    std::vector 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”的场景,性能优于全排序。

std::vector 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。

std::vector vec = {9,5,1,7,3};
// 让第2个位置(下标2)为全局第3小元素
std::ranges::nth_element(vec, vec.begin() + 2);

4.partition / stable_partition 分区

按条件将范围分为两部分:满足条件的在前,不满足的在后。

  • partition:不稳定分区
  • stable_partition:稳定分区,保留分组内原有的顺序
std::vector 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
int main() {
    std::vector 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:视图,只改变遍历顺序,原数据不变。
std::vector vec = {1,2,3};
std::ranges::reverse(vec); // 原地变为 {3,2,1}

2.rotate 序列旋转

以指定迭代器为新起点,循环平移整个序列。

std::vector vec = {1,2,3,4,5};
// 从第3个元素开始旋转 → 3 4 5 1 2
std::ranges::rotate(vec, vec.begin() + 2);

3.unique 相邻去重

只删除相邻的重复元素(和传统行为一致),重复元素会被移到尾部,通常配合 erase 来彻底删除。

std::vector 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:只复制满足条件的元素
std::vector src = {1,2,3,4,5};
std::vector dst;

// 复制所有偶数
std::ranges::copy_if(src, std::back_inserter(dst), [](int x){ return x%2 == 0; });

2.transform 元素变换

对每个元素执行映射转换,支持单范围和双范围重载。

std::vector src = {1,2,3};
std::vector dst;

// 所有元素 *2 后复制
std::ranges::transform(src, std::back_inserter(dst), [](int x){ return x * 2; });

3.fill / generate 批量填充

  • fill:用固定值填充范围
  • generate:用生成器函数动态生成元素
std::vector 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 删除指定值

删除容器中所有等于目标值的元素,直接修改容器。

std::vector vec = {2,1,2,3,2};
std::ranges::erase(vec, 2); // 删除所有 2 → {1,3}

2.std::ranges::erase_if 条件删除

删除所有满足谓词的元素,支持投影(结构体场景的利器)。

struct User { std::string name; int age; };
std::vector 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:对称差集(只在其中一个集合出现)
int main() {
    std::vector a = {1,2,3,4};
    std::vector b = {3,4,5,6};
    std::vector 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 的元素。

std::vector 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 元素变换

对每个元素执行映射转换。

// 所有元素 * 2
auto double_view = vec | std::ranges::transform([](int x) { return x * 2; });

3.take / take_while 截取前缀

  • take(N):取前 N 个元素;
  • take_while(谓词):直到条件不成立为止。
// 取前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(谓词):跳过开头满足条件的元素。
auto drop2 = vec | std::ranges::drop(2); // 3 4 5 6

5.reverse 逆序视图

只改变遍历顺序,不修改原容器(注意和 std::ranges::reverse 算法区分)。

auto rev_view = vec | std::ranges::reverse;

6.keys / values 键值视图

专门用于 std::map、std::pair 序列,能快速提取键或值。

#include 
std::map mp = {{"a",1}, {"b",2}};

// 提取所有 key
auto keys = mp | std::ranges::keys;
// 提取所有 value
auto vals = mp | std::ranges::values;

7.split 分割范围

按分隔符拆分序列,返回范围的范围(二维视图),常配合 join 使用。

#include 
std::string str = "hello,world,cpp20";

// 按逗号分割
auto split_view = str | std::ranges::split(',');

8.join 扁平化嵌套范围

将二维或多层视图拍平成一维。

// 分割后扁平化遍历
for (auto sub : str | std::ranges::split(',') | std::ranges::join) {
    std::cout << sub;
}

9.unique 相邻去重视图

只去除相邻的重复元素(和 std::unique 行为一致)。

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

1.std::ranges::iota 连续数列

生成递增的整数序列,支持有限范围和无限范围。

// 有限范围:[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():空视图。

4.3.链式管道组合

多适配器串联,一行代码就能实现复杂逻辑,全程没有临时容器:

#include 
#include 
#include 

int main() {
    std::vector 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 遍历

示例如下:

std::vector 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 字段)

struct Student {
    std::string name;
    int score;
};

传统 STL 写法: Lambda 内部手动提取成员,元素多时代码冗余

std::vector 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 写法: 使用投影直接指定成员指针,谓词只关心业务逻辑

std::vector 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:

users.erase(std::remove_if(users.begin(), users.end(),
    [](const User& u){ return u.age < 18; }),
    users.end());

Ranges 写法(搭配投影)

// 投影 age,谓词直接接收年龄值
std::ranges::erase_if(users, [](int a){ return a < 18; }, &User::age);
  • 传统 erase+remove 嵌套层级深,新手很容易写错迭代器;
  • Ranges 的 erase/erase_if 语义直白,一眼就能看懂逻辑。

3.多步序列流水线(过滤 + 变换 + 截取)

需求:从序列中 过滤奇数 → 元素 ×3 → 跳过前 2 个 → 取前 3 个

每一步都必须新建 vector 存储中间结果,多次内存分配 + 拷贝,性能损耗明显

int main() {
    std::vector src = {1,2,3,4,5,6,7,8,9,10};

    // 第1步:过滤奇数(临时容器1)
    std::vector temp1;
    std::copy_if(src.begin(), src.end(), std::back_inserter(temp1),
        [](int x){ return x % 2 == 1; });

    // 第2步:元素 *3(临时容器2)
    std::vector temp2;
    std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
        [](int x){ return x * 3; });

    // 第3步:跳过前2个,取前3个(最终结果)
    std::vector 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) + 管道 | 链式组合,全程没有临时容器、惰性求值,只在遍历的瞬间才执行逻辑。

int main() {
    std::vector 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 典型应用)

需求:遍历以 结尾的 C 字符串

传统写法:手动循环判断结束符 ,或依赖迭代器指针

const char* str = "Hello C++";
while (*str != '') {
    std::cout << *str;
    ++str;
}

Ranges 写法(结合默认哨兵 default_sentinel):

将 C 字符串包装为标准范围,哨兵自动识别 ,无需手动判断终止条件。

#include 
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、 等场景。

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++ 序列处理的新标准,它替代了传统迭代器 + 算法的模式;
  • 核心优势:语法简洁、链式可读、惰性求值(高性能)、原生投影、组合能力极强;
  • 使用原则:优先用视图做变换,必要时再物化容器;一定要区分「视图(只读)」和「算法(原地修改)」。
本文转载于:https://www.jb51.net/program/366590vao.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。
  • using namespace 使用中遇到的问题怎么解决 正版软件
    using namespace 使用中遇到的问题怎么解决
    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,
    9天前 0
  • c语言函数递归 实操经验总结:这些技巧很实用 正版软件
    c语言函数递归 实操经验总结:这些技巧很实用
    理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接
    9天前 0
  • c语言函数递归 怎么选?常见方案对比分析 正版软件
    c语言函数递归 怎么选?常见方案对比分析
    递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的
    9天前 0
  • Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解 正版软件
    Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
    理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的
    9天前 0
  • 如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏 正版软件
    如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
    理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de
    9天前 0