文章目录
- [C++20 Ranges 全面详解](#C++20 Ranges 全面详解)
-
- [一、为什么 C++20 要引入 Ranges?](#一、为什么 C++20 要引入 Ranges?)
-
- [1. 无 Ranges 时代的具体问题](#1. 无 Ranges 时代的具体问题)
- [2. Ranges 的核心价值](#2. Ranges 的核心价值)
- [二、Ranges 核心基本概念](#二、Ranges 核心基本概念)
-
- [1. 核心定义](#1. 核心定义)
- [2. 核心概念检查示例](#2. 核心概念检查示例)
- [三、Range 视图(Views)](#三、Range 视图(Views))
-
- [1. 视图的核心特性](#1. 视图的核心特性)
- [2. 常用视图及示例](#2. 常用视图及示例)
- [3. 视图基础示例(逐行解析)](#3. 视图基础示例(逐行解析))
- [4. 自定义视图(扩展)](#4. 自定义视图(扩展))
- [四、Range 算法](#四、Range 算法)
-
- [1. Range 算法的核心改进](#1. Range 算法的核心改进)
- [2. 常用 Range 算法示例](#2. 常用 Range 算法示例)
- [五、Ranges 综合应用样例](#五、Ranges 综合应用样例)
- 六、总结
C++20 Ranges 全面详解
详细讲解 C++20 Ranges 的核心概念、视图、算法及综合应用,并对比没有 Ranges 之前的编程痛点,同时对示例代码进行逐段解析,帮助你深入理解这一 C++20 核心特性。
一、为什么 C++20 要引入 Ranges?
在 Ranges 出现之前,C++ 标准库的迭代器和算法体系存在诸多痛点,这也是 Ranges 被引入的核心原因:
1. 无 Ranges 时代的具体问题
(1)算法调用冗余且不直观
传统 STL 算法需要手动传递 begin()/end() 迭代器对,代码冗长且语义不连贯:
cpp
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8};
// 找出第一个大于5的元素:必须手动传begin/end
auto it = std::find(vec.begin(), vec.end(), 5);
if (it != vec.end()) {
std::cout << *it << std::endl;
}
// 过滤+转换:需要临时容器,多轮遍历
std::vector<int> temp;
// 第一步:过滤偶数
std::copy_if(vec.begin(), vec.end(), std::back_inserter(temp),
[](int x) { return x % 2 == 0; });
// 第二步:每个元素乘2
std::vector<int> result;
std::transform(temp.begin(), temp.end(), std::back_inserter(result),
[](int x) { return x * 2; });
return 0;
}
(2)无法组合操作,性能/内存开销大
传统算法是"一次性"的,多个操作需要借助临时容器存储中间结果,既浪费内存,又增加多次遍历的开销(如上例中的 temp 容器)。
(3)迭代器与容器强耦合
算法只能接收迭代器对,无法直接操作"可遍历的对象"(如生成器、无限序列),扩展性差。
(4)错误处理不友好
若传递的 begin()/end() 不属于同一个容器,或迭代器类型不匹配,只能在运行时崩溃,无法在编译期检测。
2. Ranges 的核心价值
- 简化语法 :直接操作容器/序列,无需手动传递
begin()/end() - 惰性求值:视图(View)仅在需要时计算,无中间容器开销
- 可组合性:多个 Range 操作可以链式调用,语义更连贯
- 编译期安全:迭代器不匹配等问题在编译期暴露
- 扩展性:支持自定义 Range 类型,适配更多数据源
二、Ranges 核心基本概念
1. 核心定义
- Range(范围) :任何可以提供迭代器对(
begin()/end())的对象,满足std::ranges::range概念。
常见类型:std::vector、std::string、std::array、原生数组、视图(View)等。 - Iterator(迭代器) :Ranges 重新定义了迭代器概念层级(比传统迭代器更精细),如:
input_iterator:可读,单向遍历forward_iterator:可读,双向遍历random_access_iterator:随机访问contiguous_iterator:连续内存迭代器
- Sentinel(哨位) :替代传统
end()迭代器,可以是不同类型(如数值、特殊标记),只要能判断迭代器是否到达末尾。
2. 核心概念检查示例
cpp
#include <vector>
#include <ranges>
#include <iostream>
int main() {
std::vector<int> vec = {1,2,3};
int arr[] = {4,5,6};
// 检查是否是Range
std::cout << std::boolalpha;
std::cout << "vector is range: " << std::ranges::range<decltype(vec)> << std::endl; // true
std::cout << "array is range: " << std::ranges::range<decltype(arr)> << std::endl; // true
std::cout << "int is range: " << std::ranges::range<int> << std::endl; // false
// 检查迭代器类型
using VecIt = decltype(vec.begin());
std::cout << "vector iterator is random access: "
<< std::ranges::random_access_iterator<VecIt> << std::endl; // true
return 0;
}
代码解释:
std::ranges::range<T>是一个概念(Concept),用于判断类型T是否是合法的 Range;- 容器/数组满足 Range 概念,基础类型(如
int)不满足; std::ranges::random_access_iterator检查迭代器是否支持随机访问(如vector的迭代器支持,list的迭代器不支持)。
三、Range 视图(Views)
1. 视图的核心特性
- 惰性求值:视图不存储数据,仅记录"如何转换原 Range",只有在遍历/访问元素时才计算;
- 轻量级:视图对象本身占用极少内存(通常是几个指针/参数);
- 可组合:多个视图可以链式调用,形成复杂的转换逻辑;
- 不可变(通常):视图本身不修改原数据,仅提供转换后的"视图"。
2. 常用视图及示例
C++20 提供了丰富的视图(位于 <ranges> 头文件),核心视图如下:
| 视图类型 | 作用 | 示例 |
|---|---|---|
views::filter |
过滤元素 | 保留偶数:`vec |
views::transform |
转换元素 | 元素乘2:`vec |
views::take |
取前N个元素 | 取前3个:`vec |
views::drop |
跳过前N个元素 | 跳过前2个:`vec |
views::reverse |
反转序列 | 反转:`vec |
views::iota |
生成无限/有限整数序列 | 生成1-10:views::iota(1, 11) |
views::join |
扁平化嵌套Range | 扁平化二维数组:`vec2d |
views::slice |
切片(C++23) | 取[2,5)区间:`vec |
3. 视图基础示例(逐行解析)
cpp
#include <vector>
#include <ranges>
#include <iostream>
int main() {
// 原始数据
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 组合视图:过滤偶数 → 乘2 → 取前3个 → 反转
auto processed = nums
| std::views::filter([](int x) { return x % 2 == 0; }) // 过滤偶数:2,4,6,8,10
| std::views::transform([](int x) { return x * 2; }) // 乘2:4,8,12,16,20
| std::views::take(3) // 取前3个:4,8,12
| std::views::reverse; // 反转:12,8,4
// 遍历视图(此时才真正计算)
std::cout << "处理结果:";
for (int x : processed) {
std::cout << x << " "; // 输出:12 8 4
}
std::cout << std::endl;
// 原始数据未被修改
std::cout << "原始数据:";
for (int x : nums) {
std::cout << x << " "; // 输出:1 2 3 ... 10
}
std::cout << std::endl;
// 无限序列视图(views::iota)
auto infinite_seq = std::views::iota(1); // 生成1,2,3,...无限序列
std::cout << "无限序列前5个:";
for (int x : infinite_seq | std::views::take(5)) {
std::cout << x << " "; // 输出:1 2 3 4 5
}
std::cout << std::endl;
return 0;
}
代码关键解析:
- 管道运算符
|:Ranges 重载了|,用于将 Range 传递给视图/算法,语义类似 Unix 管道,更符合阅读习惯; - 惰性求值验证 :
processed视图定义时不会执行任何计算,只有在for循环遍历它时,才会逐元素执行过滤→转换→取前3→反转; - 原数据不可变 :所有视图操作都是"只读"的,不会修改原始
nums容器; - 无限序列 :
views::iota(1)生成无限序列,但结合views::take(5)后仅取前5个,避免无限循环。
4. 自定义视图(扩展)
你可以通过 std::ranges::view_interface 实现自定义视图,示例如下(生成斐波那契数列):
cpp
#include <ranges>
#include <iostream>
// 自定义斐波那契视图
class FibView : public std::ranges::view_interface<FibView> {
private:
size_t max_size_; // 最大元素个数
public:
// 迭代器类型
struct iterator {
using iterator_category = std::input_iterator_tag;
using value_type = unsigned long long;
using difference_type = std::ptrdiff_t;
using pointer = value_type*;
using reference = value_type&;
size_t idx_ = 0;
value_type a_ = 0, b_ = 1;
// 解引用
value_type operator*() const {
return (idx_ == 0) ? 0 : (idx_ == 1) ? 1 : a_ + b_;
}
// 自增
iterator& operator++() {
if (idx_ >= 2) {
auto temp = a_ + b_;
a_ = b_;
b_ = temp;
}
idx_++;
return *this;
}
// 相等判断(哨位)
bool operator==(const iterator& other) const {
return idx_ == other.idx_;
}
};
// 构造函数
explicit FibView(size_t max_size) : max_size_(max_size) {}
// 开始迭代器
iterator begin() const { return iterator{}; }
// 结束迭代器(哨位)
iterator end() const {
iterator it;
it.idx_ = max_size_;
return it;
}
};
int main() {
// 使用自定义斐波那契视图
FibView fib(10); // 生成前10个斐波那契数
std::cout << "斐波那契前10项:";
for (auto x : fib) {
std::cout << x << " "; // 输出:0 1 1 2 3 5 8 13 21 34
}
std::cout << std::endl;
// 组合内置视图:取偶数项
auto even_fib = fib | std::views::filter([](auto x) { return x % 2 == 0; });
std::cout << "偶数项:";
for (auto x : even_fib) {
std::cout << x << " "; // 输出:0 2 8 34
}
std::cout << std::endl;
return 0;
}
核心要点:
- 自定义视图需继承
std::ranges::view_interface,简化迭代器接口实现; - 必须实现
begin()/end()方法,返回符合迭代器概念的对象; - 自定义视图可与内置视图无缝组合。
四、Range 算法
1. Range 算法的核心改进
C++20 为 <algorithm> 提供了 Ranges 版本(位于 std::ranges 命名空间),相比传统算法:
- 直接接收 Range 对象,无需手动传
begin()/end(); - 支持返回更有用的结果(如
std::ranges::find返回迭代器,而传统std::find也返回迭代器,但 Range 版本编译期更安全); - 与视图无缝配合;
- 支持更灵活的投影(Projection)参数,简化元素访问。
2. 常用 Range 算法示例
cpp
#include <vector>
#include <ranges>
#include <algorithm>
#include <iostream>
#include <string>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 20}
};
// 1. 排序(Range版本)
std::ranges::sort(nums); // 无需传begin/end
std::cout << "排序后nums:";
for (int x : nums) std::cout << x << " "; // 1 1 2 3 4 5 6 9
std::cout << std::endl;
// 2. 查找(Range版本)
auto it = std::ranges::find(nums, 5);
if (it != nums.end()) {
std::cout << "找到5,索引:" << std::ranges::distance(nums.begin(), it) << std::endl; // 索引5
}
// 3. 投影(Projection):按Person的age排序
// 投影参数:指定按哪个成员/函数排序,无需手动写lambda
std::ranges::sort(people, std::ranges::less{}, &Person::age);
std::cout << "按年龄排序的人员:\n";
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")\n"; // Charlie(20) → Alice(25) → Bob(30)
}
// 4. 算法+视图组合:找出大于5的元素并计数
auto count = std::ranges::count_if(nums | std::views::filter([](int x){return x>0;}),
[](int x) { return x > 5; });
std::cout << "大于5的元素个数:" << count << std::endl; // 3(6,9)
// 5. 生成(generate)
std::vector<int> vec(5);
std::ranges::generate(vec, []() { static int x=0; return x++; }); // 生成0,1,2,3,4
std::cout << "generate结果:";
for (int x : vec) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
代码关键解析:
- 投影参数 :
std::ranges::sort(people, std::ranges::less{}, &Person::age)中的第三个参数是"投影",表示按Person::age成员排序,替代了传统的[](const auto& a, const auto& b){return a.age < b.age;},简化代码; - 算法+视图 :
nums | views::filter(...)生成的视图可直接作为算法的输入,无需临时容器; - 安全性 :Range 算法会在编译期检查 Range 的迭代器是否满足算法要求(如
sort需要随机访问迭代器,若传入std::list会编译报错,而传统std::sort仅在运行时崩溃)。
五、Ranges 综合应用样例
场景:数据处理流水线
需求:
- 读取一个整数列表;
- 过滤掉负数;
- 转换为平方值;
- 按降序排序;
- 取前5个元素;
- 计算这些元素的总和;
- 输出结果及原始数据。
cpp
#include <vector>
#include <ranges>
#include <algorithm>
#include <numeric>
#include <iostream>
#include <random>
// 生成随机整数(辅助函数)
std::vector<int> generate_random_nums(int count, int min_val, int max_val) {
std::vector<int> nums(count);
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(min_val, max_val);
std::ranges::generate(nums, [&]() { return dist(gen); });
return nums;
}
int main() {
// 步骤1:生成随机数据(-10到20之间的20个整数)
auto raw_nums = generate_random_nums(20, -10, 20);
std::cout << "原始数据:";
for (int x : raw_nums) std::cout << x << " ";
std::cout << "\n" << std::endl;
// 步骤2-5:构建数据处理流水线(惰性求值)
auto processed = raw_nums
| std::views::filter([](int x) { return x >= 0; }) // 过滤负数
| std::views::transform([](int x) { return x * x; }) // 平方
| std::views::sort(std::ranges::greater{}) // 降序排序
| std::views::take(5); // 取前5个
// 步骤6:计算总和(触发求值)
// 注意:std::accumulate暂无Range版本,需手动传begin/end
auto sum = std::accumulate(processed.begin(), processed.end(), 0);
// 步骤7:输出结果
std::cout << "处理后前5个平方值(降序):";
for (int x : processed) std::cout << x << " ";
std::cout << "\n总和:" << sum << std::endl;
// 扩展:统计原始数据中正数的个数
auto positive_count = std::ranges::count_if(raw_nums, [](int x) { return x > 0; });
std::cout << "原始数据中正数个数:" << positive_count << std::endl;
return 0;
}
输出示例:
原始数据:-5 12 8 -3 18 7 -9 15 4 11 -2 9 19 6 -7 10 1 17 -4 13
处理后前5个平方值(降序):361 324 289 225 169
总和:1368
原始数据中正数个数:14
核心解析:
- 流水线设计 :所有转换操作通过
|链式调用,语义清晰,符合"数据流式处理"的思维; - 惰性求值 :
processed视图在定义时不计算,仅在遍历(for循环)和accumulate时才执行实际计算; - 无临时容器:整个处理过程无需创建任何中间容器,内存开销极小;
- 代码简洁性:相比传统 STL 实现(需要多个临时容器、多次遍历),Ranges 版本代码量减少50%以上,可读性大幅提升。
六、总结
核心要点回顾
- Ranges 的诞生背景:解决传统 STL 算法需要手动传迭代器、无法组合操作、中间容器开销大等痛点,提供更直观、高效的序列处理方式;
- 核心概念:Range 是可遍历的序列,视图(View)是轻量级、惰性求值的 Range 转换,Range 算法直接操作 Range 且更安全;
- 关键特性 :
- 视图的惰性求值:仅在访问时计算,无中间容器开销;
- 操作的可组合性 :通过
|链式调用形成处理流水线; - 编译期安全性:迭代器不匹配等问题提前暴露;
- 投影参数:简化按成员/函数的排序、查找等操作;
- 应用价值:大幅简化序列处理代码,提升可读性和性能,支持自定义扩展(如自定义视图)。
扩展建议
- C++23 对 Ranges 进行了扩展(如
views::zip、views::slice),可关注后续标准; - 实际项目中,优先使用 Range 算法替代传统 STL 算法,减少迭代器传递错误;
- 视图适合高频次、多步骤的序列转换场景,避免临时容器的内存/性能开销。
通过 Ranges,C++ 终于拥有了现代化的序列处理能力,其设计理念与 Rust 的迭代器、Python 的生成器异曲同工,是 C++20 最具价值的特性之一。