之前我们已经学习过C++20的概念与约束。今天我们就来学习继承了概念与约束的标准库新的功能库Range库
相关代码上传至gitee:
官方文档:Standard library header <ranges> (C++20) - cppreference.com
目录
[1. 核心概念(Concepts)------ 类型系统的基石](#1. 核心概念(Concepts)—— 类型系统的基石)
[2. 算法(Algorithms)------ std::ranges:: 命名空间](#2. 算法(Algorithms)—— std::ranges:: 命名空间)
[3. Range 适配器(Range Adaptors)------ 惰性管道](#3. Range 适配器(Range Adaptors)—— 惰性管道)
[4. 工厂(Factories / Range Factories)](#4. 工厂(Factories / Range Factories))
[投影(Projection)------无需 Lambda 包装器](#投影(Projection)——无需 Lambda 包装器)
[Concepts 约束------编译期安全带](#Concepts 约束——编译期安全带)
[对内置数组和 std::span 的原生支持](#对内置数组和 std::span 的原生支持)
Range库介绍
前言
Ranges 是对 STL 算法与迭代器体系的一次根本性重构。 传统 STL 算法用一对迭代器 [begin, end) 表示操作范围,而 Ranges 将这个"范围"抽象为一等公民------一个可以直接传递的对象。
核心变化:
cpp
// C++17 传统写法:必须传两个迭代器
std::sort(vec.begin(), vec.end());
// C++20 Ranges 写法:传一个 range 对象即可
std::ranges::sort(vec);
这个看似微小的语法变化背后,是整个泛型编程范式的升级。
包含内容
整个库基于 <ranges> 头文件,分为四大层次:
1. 核心概念(Concepts)------ 类型系统的基石
| Concept | 含义 |
|---|---|
std::ranges::range |
任何有 begin() 和 end() 的类型 |
std::ranges::sized_range |
可以在常数时间获取大小的 range |
std::ranges::view |
轻量级、O(1) 拷贝/移动的 range |
std::ranges::input_range |
至少支持输入迭代器的 range |
std::ranges::forward_range |
支持前向迭代器 |
std::ranges::bidirectional_range |
支持双向迭代器 |
std::ranges::random_access_range |
支持随机访问迭代器 |
std::ranges::contiguous_range |
内存连续布局(如 vector, array, span) |
std::ranges::common_range |
begin() 和 end() 返回相同类型的 range |
2. 算法(Algorithms)------ std::ranges:: 命名空间
所有传统 STL 算法的 Range 版本 ,约束在 std::ranges 命名空间下。与传统算法有两个关键区别:
-
第一个参数是 range 对象,而非迭代器对
-
返回类型更丰富:不再只返回一个迭代器,而是返回包含更多信息的结构化结果
// 传统 STL:返回迭代器,要找插入位置还得再算
auto it = std::find(vec.begin(), vec.end(), 42);// C++20 Ranges:接受 range,返回迭代器
auto it = std::ranges::find(vec, 42);
3. Range 适配器(Range Adaptors)------ 惰性管道
这是 Ranges 库最具革命性 的部分。适配器是惰性求值的视图转换,通过 | 管道运算符串联,构成声明式的数据处理流水线:
| 适配器 | 作用 |
|---|---|
std::views::filter(pred) |
按谓词过滤元素 |
std::views::transform(fn) |
对每个元素应用函数 |
std::views::take(n) |
取前 n 个元素 |
std::views::drop(n) |
跳过前 n 个元素 |
std::views::take_while(pred) |
取满足条件的连续前缀 |
std::views::drop_while(pred) |
丢弃满足条件的连续前缀 |
std::views::reverse |
反转范围 |
std::views::join |
展平嵌套 range |
std::views::split(delim) |
按分隔符切分 |
std::views::common |
转换为 common_range |
std::views::keys / values |
提取 pair/tuple 的 key 或 value |
std::views::iota |
生成递增序列 |
std::views::all |
将 range 包装为 view |
4. 工厂(Factories / Range Factories)
不需要输入 range,直接生成 range 的工具:
std::views::iota(start, end)--- 生成[start, end)整数序列std::views::empty<T>--- 空 rangestd::views::single(x)--- 只包含单一元素的 rangestd::views::istream<T>(stream)--- 从输入流读取
优势
消灭迭代器对------代码量减半
cpp
// C++17:写两遍容器名
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), pred);
// C++20:容器名只出现一次
std::ranges::sort(v);
auto it = std::ranges::find_if(v, pred);
这不是语法糖------它消除了迭代器不匹配的可能性,从类型系统层面杜绝了 sort(v1.begin(), v2.end()) 这类经典 bug。
管道组合------从命令式到声明式
这是最重要的心智模型转变。传统写法是命令式 的:你需要一步步告诉 CPU 要做什么。管道写法是声明式的:你描述数据应该经过怎样的变换。
cpp
// C++17 命令式:需要中间容器,多次遍历
std::vector<int> even;
std::copy_if(v.begin(), v.end(), std::back_inserter(even),
[](int x) { return x % 2 == 0; });
std::vector<int> squared;
std::transform(even.begin(), even.end(), std::back_inserter(squared),
[](int x) { return x * x; });
auto it = std::find_if(squared.begin(), squared.end(),
[](int x) { return x > 100; });
// C++20 声明式:一次遍历,零拷贝,惰性求值
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::drop_while([](int x) { return x <= 100; });
for (auto x : result) { /* 只在遍历时才真正计算 */ }
惰性求值------零开销抽象
管道中的每个适配器在定义时不执行任何计算 。计算被推迟到消费时才发生,且所有变换融合成单次遍历。没有中间分配,没有额外拷贝。
更丰富的返回值
传统 std::find 只返回一个迭代器,还需要重新计算到开头的距离等操作。Range 算法返回结构化的结果对象:
cpp
// C++17
auto it = std::find(v.begin(), v.end(), 42);
auto pos = std::distance(v.begin(), it); // 又遍历一次
// C++20:直接通过结构化绑定获取所需信息
struct find_result { iterator found; bool success; }; // 伪代码示例
// 实际例子------ranges::sort 返回 borrowed_iterator_t<R>
投影(Projection)------无需 Lambda 包装器
每个 Range 算法都支持一个 Projection 参数,可以在比较/操作之前对元素做变换,而无需手动写 lambda:
cpp
// C++17:lambda 包装比较
std::sort(v.begin(), v.end(),
[](const Person& a, const Person& b) { return a.age < b.age; });
// C++20:投影 + 默认比较
std::ranges::sort(v, std::less{}, &Person::age);
// ^^^^^^^^^ ^^^^^^^^^^^^
// 比较函数 投影(要对什么排序)
Concepts 约束------编译期安全带
Range 算法通过 Concept 对其输入做了严格约束。如果一个类型不满足 input_range,调用 ranges::find 在编译期就会给出清晰的错误信息,而不是在实例化后抛出几十行的模板错误。
int x = 42;
// std::ranges::sort(x); // 编译错误,清晰提示 int 不是 range
对内置数组和 std::span 的原生支持
cpp
int arr[] = {3, 1, 4, 1, 5};
std::ranges::sort(arr); // 直接对 C 数组排序,不需要 std::begin/std::end
Range视图
视图的核心特点与优势
视图 (View) 是 Ranges 库的核心抽象,具有以下关键特征:
- O(1) 拷贝/移动 --- 视图不拥有数据,只持有底层 range 的引用或轻量级状态
- 惰性求值 --- 定义管道时不执行任何计算,消费时才按需计算
- 融合遍历 --- 多个视图通过
|串联后,编译器将其融合为等效的单次手写循环 - 不修改原数据 --- 视图是"数据的观察窗口",变换操作生成新视图而不修改底层容器
- 编译期类型安全 --- 基于 Concepts 约束,视图间的非法连接会在编译期被拦截
适配器视图
需要输入 range,对数据做变换
| 适配器 | 功能 | 典型场景 |
|---|---|---|
filter(pred) |
保留满足谓词的元素 | 筛选偶数、有效记录 |
transform(fn) |
对每个元素应用映射函数 | 类型转换、提取字段 |
take(n) |
取前 n 个元素后停止 | 分页、TOP-N 查询 |
drop(n) |
跳过前 n 个元素 | 跳过表头、去除前导 |
take_while(pred) |
取满足条件的连续前缀 | 读取到终止标记 |
drop_while(pred) |
丢弃满足条件的连续前缀 | 去除前导空白 |
reverse |
反转 range(需 bidirectional) | 逆序遍历 |
split(delim) |
按分隔符切割(range of ranges) | 字符串分词 |
join |
展平嵌套 range | 二维转一维 |
keys |
提取 pair/tuple 的第一个元素 | 遍历 map 的 key |
values |
提取 pair/tuple 的第二个元素 | 遍历 map 的 value |
all |
将 range 包装为 view | 显式开启管道 |
common |
统一 begin() 和 end() 返回类型 | 适配遗留接口 |
代码示例:
cpp
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <map>
#include <list>
#include <sstream>
namespace r = std::ranges;
namespace v = std::views;
template<typename R>
void print_range(std::string_view name, R&& r) {
std::cout << name << ": ";
for (auto&& e : r) std::cout << e << ' ';
std::cout << '\n';
}
// ----------------------------------------------------------------------------
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// ================================================================
// 1. filter ------ 按谓词过滤,保留满足条件的元素
// ================================================================
{
auto even = data | v::filter([](int x) { return x % 2 == 0; });
print_range("filter(偶数)", even); // 2 4 6 8 10
}
// ================================================================
// 2. transform ------ 对每个元素施加映射函数
// ================================================================
{
auto squared = data | v::transform([](int x) { return x * x; });
print_range("transform(平方)", squared); // 1 4 9 16 25 36 49 64 81 100
}
// ================================================================
// 3. 管道组合:filter + transform ------ 惰性求值,单次遍历
// ================================================================
{
auto pipeline = data
| v::filter([](int x) { return x % 2 == 0; }) // 先过滤偶数
| v::transform([](int x) { return x * x; }); // 再平方
print_range("filter + transform(偶数的平方)", pipeline); // 4 16 36 64 100
}
// ================================================================
// 4. take(n) ------ 取前 n 个元素,达到 n 个后立即停止遍历
// drop(n) ------ 跳过前 n 个元素
// ================================================================
{
auto first3 = data | v::take(3);
print_range("take(3)", first3); // 1 2 3
auto skip3 = data | v::drop(3);
print_range("drop(3)", skip3); // 4 5 6 7 8 9 10
// 与 filter 组合:取前3个偶数
auto first3even = data
| v::filter([](int x) { return x % 2 == 0; })
| v::take(3);
print_range("前3个偶数", first3even); // 2 4 6
}
// ================================================================
// 5. take_while ------ 取满足条件的连续前缀
// drop_while ------ 丢弃满足条件的连续前缀
// ================================================================
{
auto prefix = data | v::take_while([](int x) { return x < 5; });
print_range("take_while(x < 5)", prefix); // 1 2 3 4
auto suffix = data | v::drop_while([](int x) { return x < 5; });
print_range("drop_while(x < 5)", suffix); // 5 6 7 8 9 10
}
// ================================================================
// 6. reverse ------ 反转 range(要求 bidirectional_range)
// ================================================================
{
auto reversed = data | v::reverse;
print_range("reverse", reversed); // 10 9 8 7 6 5 4 3 2 1
}
// ================================================================
// 7. split ------ 按分隔符切割 range
// 返回的是一个 range of ranges,每个子 range 是一段
// ================================================================
{
std::string text = "hello world cpp ranges";
auto words = text | v::split(' ');
std::cout << "split(' '): ";
for (auto word : words) {
std::cout << '\'';
for (char ch : word) std::cout << ch;
std::cout << "' ";
}
std::cout << '\n'; // 'hello' 'world' 'cpp' 'ranges'
}
// ================================================================
// 8. join ------ 展平嵌套的 range(range of ranges → flat range)
// ================================================================
{
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4, 5}, {6}};
auto flat = nested | v::join;
print_range("join(展平嵌套)", flat); // 1 2 3 4 5 6
// join 与 split 配合:先切割再展平(类似于去除分隔符)
std::string text = "a-b-c";
auto parts = text | v::split('-');
// 把 '-' 替换为 ' '
// 注意:这里 split 产生的 subrange 再 join 需要 C++23 的 join_with
// 此处演示 join 的基本用法
}
// ================================================================
// 9. keys / values ------ 提取关联容器的键/值
// ================================================================
{
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
std::cout << "keys: ";
for (auto k : m | v::keys) std::cout << k << ' '; // 1 2 3
std::cout << '\n';
std::cout << "values: ";
for (auto v : m | v::values) std::cout << v << ' '; // one two three
std::cout << '\n';
}
// ================================================================
// 10. all ------ 将任意 range 包装为 view
// 当你想使用管道语法但手上的容器不是 view 时,all 是桥梁
// ================================================================
{
auto v = data | v::all; // vector → view
auto r = v | v::reverse; // 管道串联
print_range("all | reverse", r); // 10 9 8 7 6 5 4 3 2 1
// 实际上 | 运算符已经隐式做了这个转换,显式使用 all 的场景较少
}
// ================================================================
// 11. common ------ 将 sentinel 类型与 iterator 类型统一
// 某些遗留代码要求 begin() 和 end() 返回相同类型
// ================================================================
{
auto filtered = data | v::filter([](int x) { return x > 5; });
// filtered 的 begin() 和 end() 类型不同(sentinel != iterator)
// static_assert(!r::common_range<decltype(filtered)>);
auto common = filtered | v::common;
// common 的 begin() 和 end() 返回相同类型
static_assert(r::common_range<decltype(common)>);
print_range("common", common); // 6 7 8 9 10
}
// ================================================================
// 12. 投射(Projection) ------ 不是适配器,但常与 Range 算法配合
// 所有 std::ranges 算法都支持 projection 参数
// ================================================================
{
struct Person { std::string name; int age; };
std::vector<Person> people = {
{"Charlie", 35}, {"Alice", 30}, {"Bob", 25}
};
// 按年龄排序,使用 projection 而非手写 lambda
r::sort(people, std::less{}, &Person::age);
std::cout << "projection(按age排序): ";
for (auto& p : people) std::cout << p.name << '(' << p.age << ") ";
std::cout << '\n';
}
return 0;
}
工厂视图
自身生成 range
| 工厂 | 功能 | 典型场景 |
|---|---|---|
iota(start) |
无限递增序列 [start, ∞) |
配合 take 生成索引 |
iota(start, end) |
有限递增序列 [start, end) |
生成数值区间 |
empty<T> |
空 range | 占位/空结果 |
single(x) |
只包含单一元素的 range | 插入单元素到管道 |
istream<T>(stream) |
从输入流惰性读取 | 处理大数据文件 |
counted(it, n) |
迭代器 + 计数构造 range | 对接指针+长度 API |
代码示例:
cpp
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <sstream>
#include <iterator>
#include <cassert>
namespace r = std::ranges;
namespace v = std::views;
template<typename R>
void print_range(std::string_view name, R&& r) {
std::cout << name << ": ";
for (auto&& e : r) std::cout << e << ' ';
std::cout << '\n';
}
// ----------------------------------------------------------------------------
int main() {
// ================================================================
// 1. iota ------ 生成递增序列,类似 Python 的 range()
// 语法: v::iota(start, end) 生成 [start, end) 的整数
// 也可: v::iota(start) 生成 [start, ∞) 的无界序列
// ================================================================
{
// 有界 iota:指定上限
auto seq = v::iota(1, 11); // 1, 2, 3, ..., 10
print_range("iota(1, 11)", seq); // 1 2 3 4 5 6 7 8 9 10
// 无界 iota:从某个值开始,无限延伸(必须配合 take 使用)
// 直接迭代无界 iota 会导致无限循环!
auto inf_seq = v::iota(100); // 100, 101, 102, ...
auto first5 = inf_seq | v::take(5);
print_range("iota(100) | take(5)", first5); // 100 101 102 103 104
}
// ================================================================
// 1b. iota 不只是整数 ------ 任何支持 ++ 和 < 的类型都可用
// ================================================================
{
// 生成大写字母序列
auto alpha = v::iota('A', 'H'); // A, B, C, D, E, F, G
std::cout << "iota('A','H'): ";
for (char c : alpha) std::cout << c << ' ';
std::cout << '\n';
}
// ================================================================
// 2. empty<T> ------ 生成空 range(不包含任何元素)
// 常用于需要"空结果"占位的场景
// ================================================================
{
auto e = v::empty<int>;
std::cout << "empty<int> size: " << r::distance(e) << '\n'; // 0
assert(r::empty(e)); // 确认是空 range
}
// ================================================================
// 3. single(x) ------ 生成只包含单一元素的 range
// 常与 join 配合使用,将单元素插入管道流
// ================================================================
{
auto s = v::single(42);
print_range("single(42)", s); // 42
static_assert(r::size(s) == 1);
assert(*s.begin() == 42);
}
// ================================================================
// 4. istream<T> ------ 从输入流读取,作为 range
// 按需从流中惰性读取,不一次性加载所有数据
// ================================================================
{
std::istringstream iss("10 20 30 40 50");
auto numbers = v::istream<int>(iss);
std::cout << "istream<int>: ";
for (int x : numbers) std::cout << x << ' '; // 10 20 30 40 50
std::cout << '\n';
}
// ================================================================
// 4b. istream 管道组合 ------ 从流读取后立即过滤/变换
// 无需先存到 vector,直接惰性处理
// ================================================================
{
std::istringstream iss("1 2 3 4 5 6 7 8 9 10");
auto result = v::istream<int>(iss)
| v::filter([](int x) { return x % 2 == 0; })
| v::transform([](int x) { return x * x; })
| v::take(3);
std::cout << "istream管道(偶数的平方前3个): ";
for (int x : result) std::cout << x << ' '; // 4 16 36
std::cout << '\n';
}
// ================================================================
// 5. counted ------ 从迭代器 + 计数构造 range
// 接收 (iterator, count) 对,生成大小固定的 range
// 常用于处理遗留代码中"指针+长度"的 API
// ================================================================
{
std::vector<int> v = {10, 20, 30, 40, 50, 60, 70, 80};
// 从第3个元素(30)开始,取4个
auto sub = v::counted(v.begin() + 2, 4);
print_range("counted(begin+2, 4)", sub); // 30 40 50 60
// 实质等价于 r::subrange(begin, begin + count)
}
// ================================================================
// 6. 综合示例:工厂 + 适配器 构建完整管道
// 生成前20个自然数,保留3的倍数,平方后取前5个
// ================================================================
{
auto pipeline = v::iota(1) // 工厂: 1,2,3,...
| v::filter([](int x) { return x % 3 == 0; }) // 保留3的倍数: 3,6,9,...
| v::transform([](int x) { return x * x; }) // 平方: 9,36,81,...
| v::take(5); // 取前5个
print_range("综合管道", pipeline); // 9 36 81 144 225
}
// ================================================================
// 6b. 综合示例:斐波那契数列生成
// 利用 iota 和 transform 可以生成任意序列
// ================================================================
{
// 技巧: iota(0) 提供索引 n=0,1,2,...,transform 将 n 映射为 Fib(n)
// 但这里需要状态,用单行管道做不直观,展示另一种思路:
// 用 iota 产生索引,通过迭代计算 Fib
// 更直接的 Fibonacci:使用结构化绑定 + iota
auto fib = v::iota(0)
| v::transform([a = 0ull, b = 1ull](int) mutable {
auto result = a;
a = b;
b = result + b;
return result;
})
| v::take(10);
std::cout << "Fibonacci(前10项): ";
for (auto f : fib) std::cout << f << ' '; // 0 1 1 2 3 5 8 13 21 34
std::cout << '\n';
}
return 0;
}
综合实践
按部分筛选员工然后加薪资
cpp
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <vector>
namespace r = std::ranges;
namespace v = std::views;
// ============================================================================
// 员工薪资分析系统 --- C++20 Ranges 综合应用
// ============================================================================
struct Employee {
std::string name;
std::string department;
double salary;
};
// ============================================================================
// 功能1: 按部门分组统计平均薪资
// ============================================================================
// C++20 没有 std::views::group_by(那是 C++23 的特性),因此采用 map 聚合,
// 配合 range-based for 遍历。每个部门的 salary 被累加后求均值。
void avgSalaryByDept(const std::vector<Employee>& employees) {
// <部门, {薪资总和, 人数}>
std::map<std::string, std::pair<double, int>> dept_stats;
for (const auto& emp : employees) {
auto& [total, count] = dept_stats[emp.department];
total += emp.salary;
++count;
}
std::cout << "\n===== 各部门平均薪资 =====\n";
std::cout << std::left;
for (const auto& [dept, stats] : dept_stats) {
const auto& [total, count] = stats;
std::cout << " " << std::setw(12) << dept
<< " : " << std::fixed << std::setprecision(2)
<< total / count << '\n';
}
}
// ============================================================================
// 功能2: 找出薪资最高的员工
// ============================================================================
// 使用 std::ranges::max_element,通过 projection 指定比较对象为 salary 成员。
// 比传统 lambda 版本更简洁: r::max_element(v, less{}, &Employee::salary)
void findHighestPaid(const std::vector<Employee>& employees) {
auto it = r::max_element(employees, std::less{}, &Employee::salary);
std::cout << "\n===== 薪资最高员工 =====\n";
if (it == employees.end()) {
std::cout << " (无数据)\n";
return;
}
std::cout << " " << it->name
<< " (" << it->department << ")"
<< " : " << std::fixed << std::setprecision(2)
<< it->salary << '\n';
}
// ============================================================================
// 功能3: 筛选技术部员工并加薪10%,然后打印
// ============================================================================
// filter 视图筛选技术部,展开 for 循环中修改 salary 后输出新旧对比。
// 注意: filter 返回的是 view,对 view 中元素的修改会反映回原容器。
void raiseITDept(std::vector<Employee>& employees) {
std::cout << "\n===== 技术部加薪10% =====\n";
std::cout << std::left;
// 管道: 保留技术部 → 遍历修改
auto it_emps = employees
| v::filter([](const Employee& e) { return e.department == "技术部"; });
for (auto& emp : it_emps) {
double old_salary = emp.salary;
emp.salary *= 1.10;
std::cout << " " << std::setw(10) << emp.name
<< " : " << std::setw(8) << old_salary
<< " -> " << std::fixed << std::setprecision(2)
<< emp.salary << '\n';
}
}
// ============================================================================
// 统一入口: 依次执行上述三项分析
// ============================================================================
void analyzeEmployees(std::vector<Employee>& employees) {
avgSalaryByDept(employees);
findHighestPaid(employees);
raiseITDept(employees);
}
// ============================================================================
// main
// ============================================================================
int main() {
std::vector<Employee> staff = {
{"张三", "技术部", 25000.0},
{"李四", "技术部", 28000.0},
{"王五", "技术部", 22000.0},
{"赵六", "市场部", 18000.0},
{"孙七", "市场部", 20000.0},
{"周八", "人事部", 15000.0},
{"吴九", "技术部", 35000.0},
{"郑十", "人事部", 16000.0},
{"冯十一", "市场部", 19000.0},
};
std::cout << std::fixed << std::setprecision(2);
std::cout << "===== 员工薪资分析系统 =====\n";
std::cout << "共 " << staff.size() << " 名员工\n";
analyzeEmployees(staff);
}
本期内容到这里就结束了,喜欢请点个赞谢谢
封面图自取:
