C++学习:六个月从基础到就业------C++20:范围(Ranges)基础
本文是我C++学习之旅系列的第五十一篇技术文章,也是第三阶段"现代C++特性"的第十三篇,介绍C++20引入的范围(Ranges)库的基础知识。查看完整系列目录了解更多内容。
引言
STL算法和容器是C++编程中最强大的工具之一,但传统的STL算法接口存在一些使用上的不便:需要显式传递迭代器对、难以组合多个算法操作、代码可读性不佳等。C++20引入的范围(Ranges)库重新设计了算法接口,引入了视图(Views)的概念,并提供了方便的管道操作符,显著改善了这些问题。
想象一下,如果你想获取一个集合中所有偶数的平方,传统STL算法需要这样写:
cpp
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> result;
// 筛选偶数
std::copy_if(numbers.begin(), numbers.end(),
std::back_inserter(result),
[](int n) { return n % 2 == 0; });
// 计算平方
std::transform(result.begin(), result.end(),
result.begin(),
[](int n) { return n * n; });
而使用Ranges库,代码变得简洁明了:
cpp
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 使用结果
for (int n : result) {
std::cout << n << " "; // 输出: 4 16 36 64 100
}
本文将介绍C++20 Ranges库的基础知识,包括其核心概念、范围算法、视图(Views)和管道操作,帮助你理解和应用这一强大的新特性。
目录
范围库概述
传统STL算法的局限
传统STL算法虽然功能强大,但存在几个明显的使用不便:
- 迭代器对耦合:必须同时提供起始和结束迭代器
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::sort(v.begin(), v.end()); // 必须提供两个迭代器
- 错误风险:容易传递不匹配的迭代器对
cpp
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {4, 5, 6};
// 潜在风险:传递了不匹配的迭代器对
std::sort(v1.begin(), v2.end()); // 未定义行为
- 组合算法困难:算法间组合需要中间容器,代码冗长
cpp
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> temp;
std::vector<int> result;
// 筛选
std::copy_if(source.begin(), source.end(),
std::back_inserter(temp),
[](int n) { return n > 2; });
// 转换
std::transform(temp.begin(), temp.end(),
std::back_inserter(result),
[](int n) { return n * n; });
- 返回值不便:许多算法返回迭代器,需要检查合法性
cpp
auto it = std::find(v.begin(), v.end(), 42);
if (it != v.end()) { // 必须检查是否有效
// 使用*it
} else {
// 未找到
}
范围库的设计理念
C++20范围库基于几个核心理念设计:
- 范围作为统一概念:将容器或迭代器对视为一个整体的"范围"
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
// 将容器作为范围直接传递
std::ranges::sort(v);
- 组合操作 :通过管道操作符(
|
)组合多个操作
cpp
auto result = container
| std::views::filter(pred)
| std::views::transform(func);
- 惰性求值:视图不立即执行操作,而是在需要结果时才计算
cpp
// 创建视图只是定义操作,不执行实际计算
auto even_squares = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 只有遍历时才实际执行操作
for (int n : even_squares) {
// 在这里才真正执行过滤和转换
std::cout << n << " ";
}
- 更清晰的语义:代码更具声明性,意图更加明确
cpp
// 传统方式
std::vector<int> result;
std::copy_if(v.begin(), v.end(), std::back_inserter(result),
[](int n) { return n > 0; });
// Ranges方式
auto positive = v | std::views::filter([](int n) { return n > 0; });
核心组件
范围库包含四个主要组件:
- 范围概念(Range Concepts):定义何为范围的约束
cpp
template<typename T>
concept range = requires(T& t) {
std::ranges::begin(t);
std::ranges::end(t);
};
- 范围算法(Range Algorithms):接受范围作为参数的算法
cpp
// 算法直接接受范围参数
std::ranges::sort(container);
std::ranges::find(container, value);
- 视图(Views):轻量级、不修改原始数据的范围适配器
cpp
// 基本视图
std::views::all // 整个范围的视图
std::views::filter // 根据谓词筛选元素
std::views::transform // 转换元素
std::views::take // 取前N个元素
std::views::drop // 跳过前N个元素
- 范围适配器(Range Adaptors):修改范围属性的工具
cpp
// 范围适配器
std::views::reverse // 反转范围
std::views::join // 连接嵌套范围
std::views::split // 分割范围
范围概念
什么是范围
在C++20中,范围(Range)是一个抽象概念,表示元素序列。更具体地说,任何可以通过调用std::ranges::begin()
和std::ranges::end()
得到有效迭代器对的类型都是范围。
cpp
#include <ranges>
#include <vector>
#include <list>
#include <string>
#include <iostream>
void demonstrate_ranges() {
// 这些都是范围
std::vector<int> vec = {1, 2, 3, 4, 5}; // 向量是范围
std::list<double> lst = {1.1, 2.2, 3.3}; // 列表是范围
std::string str = "Hello"; // 字符串是范围
int arr[] = {10, 20, 30, 40, 50}; // 数组是范围
// 普通指针对也可以是范围
const char* cstr = "C-string";
auto ptr_range = std::ranges::subrange(cstr, cstr + 8);
// 视图也是范围
auto even = vec | std::views::filter([](int n) { return n % 2 == 0; });
// 使用范围变量的示例
std::cout << "Vector elements:";
for (int n : vec) std::cout << " " << n;
std::cout << std::endl;
std::cout << "Even numbers:";
for (int n : even) std::cout << " " << n;
std::cout << std::endl;
}
范围的关键特性:
- 提供了表示元素序列的统一抽象
- 支持对整个序列而非迭代器对进行操作
- 可以是有限的(如容器)或无限的(如生成器)
- 可以是普通容器、视图或迭代器对
范围类别和需求
C++20定义了多种范围概念,根据底层迭代器的能力形成层次结构:
cpp
#include <ranges>
#include <vector>
#include <list>
#include <forward_list>
#include <iostream>
template<typename R>
void print_range_capabilities() {
std::cout << "- range: " << std::ranges::range<R> << std::endl;
std::cout << "- input_range: " << std::ranges::input_range<R> << std::endl;
std::cout << "- forward_range: " << std::ranges::forward_range<R> << std::endl;
std::cout << "- bidirectional_range: " << std::ranges::bidirectional_range<R> << std::endl;
std::cout << "- random_access_range: " << std::ranges::random_access_range<R> << std::endl;
std::cout << "- contiguous_range: " << std::ranges::contiguous_range<R> << std::endl;
std::cout << "- sized_range: " << std::ranges::sized_range<R> << std::endl;
std::cout << "- view: " << std::ranges::view<R> << std::endl;
}
int main() {
std::cout << "std::vector capabilities:" << std::endl;
print_range_capabilities<std::vector<int>>();
std::cout << "\nstd::list capabilities:" << std::endl;
print_range_capabilities<std::list<int>>();
std::cout << "\nstd::forward_list capabilities:" << std::endl;
print_range_capabilities<std::forward_list<int>>();
std::cout << "\nFilter view capabilities:" << std::endl;
print_range_capabilities<decltype(std::vector<int>{} |
std::views::filter([](int) { return true; }))>();
return 0;
}
主要范围类别:
std::ranges::range
:基本范围概念,支持begin()
和end()
std::ranges::input_range
:可以读取元素的范围std::ranges::forward_range
:可以多次遍历的范围std::ranges::bidirectional_range
:可以双向遍历的范围std::ranges::random_access_range
:支持随机访问的范围std::ranges::contiguous_range
:内存连续存储的范围std::ranges::sized_range
:可以常数时间获取大小的范围std::ranges::view
:轻量级、非拥有元素的范围
不同容器支持不同级别的范围能力:
std::vector
支持所有范围能力(除了view
)std::list
是双向范围但不支持随机访问std::forward_list
是前向范围但不支持双向遍历- 视图(如filter view)通常继承底层范围的能力,但始终是
view
迭代器与哨兵
C++20范围库引入了"哨兵"(Sentinel)的概念,允许迭代器和终止条件类型不同:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
int main() {
// 传统方式:begin和end类型相同
std::vector<int> v = {1, 2, 3, 4, 5};
auto it_begin = v.begin(); // std::vector<int>::iterator
auto it_end = v.end(); // 同样类型
// 范围库:允许不同类型的终止条件
std::string str = "Hello, world!";
// 自定义视图:到null字符为止
auto null_terminated = std::ranges::subrange(
str.c_str(), // const char*
std::unreachable_sentinel // 特殊哨兵类型
);
// 计算长度(不包括null终止符)
std::cout << "String length: "
<< std::ranges::distance(null_terminated) << std::endl;
// 查找特定字符的视图
auto until_comma = std::ranges::subrange(
str.begin(),
std::ranges::find(str, ',') // 迭代器指向逗号
);
std::cout << "Text before comma: ";
for (char c : until_comma) {
std::cout << c;
}
std::cout << std::endl;
return 0;
}
哨兵概念的优势:
- 更灵活的范围表示:终止条件可以是谓词而非具体位置
- 无限序列支持:可以表示无限序列,只在需要时检查终止条件
- 懒惰计算:哨兵可以推迟终止条件的计算
- 优化机会:编译器可以针对特定哨兵类型优化代码
范围算法
算法改进
C++20为标准库算法提供了对应的范围版本,带来几个主要改进:
- 直接接受范围参数,而非迭代器对
cpp
#include <ranges>
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {5, 3, 1, 4, 2};
// 传统STL算法
std::sort(numbers.begin(), numbers.end());
// 范围版本算法
std::ranges::sort(numbers); // 更简洁
return 0;
}
- 返回更有意义的结果
cpp
#include <ranges>
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 传统STL查找
auto it = std::find(numbers.begin(), numbers.end(), 3);
if (it != numbers.end()) {
std::cout << "Found: " << *it << std::endl;
}
// 范围版本查找
auto it_ranges = std::ranges::find(numbers, 3);
if (it_ranges != numbers.end()) {
std::cout << "Found with ranges: " << *it_ranges << std::endl;
}
// 更复杂的例子:找出最大和最小值
auto [min_it, max_it] = std::ranges::minmax_element(numbers);
std::cout << "Min: " << *min_it << ", Max: " << *max_it << std::endl;
return 0;
}
- 支持投影(Projections):在应用算法前变换元素
cpp
#include <ranges>
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// 使用投影按年龄排序
std::ranges::sort(people, {}, &Person::age);
// 输出排序后的结果
for (const auto& person : people) {
std::cout << person.name << ": " << person.age << std::endl;
}
// 查找年龄最大的人
auto oldest = std::ranges::max_element(people, {}, &Person::age);
if (oldest != people.end()) {
std::cout << "Oldest person: " << oldest->name
<< " (" << oldest->age << ")" << std::endl;
}
return 0;
}
- 概念约束:算法明确指定了对输入类型的要求
cpp
// std::ranges::sort的简化定义
template<std::random_access_range R,
typename Comp = std::ranges::less,
typename Proj = std::identity>
requires std::sortable<std::ranges::iterator_t<R>, Comp, Proj>
constexpr auto sort(R&& r, Comp comp = {}, Proj proj = {}) -> ...;
常用范围算法
C++20范围库包含了标准库中大部分算法的范围版本:
cpp
#include <ranges>
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
void demonstrate_range_algorithms() {
std::vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 4, 6};
std::vector<int> dest(numbers.size());
// 排序算法
std::ranges::sort(numbers);
std::cout << "排序后: ";
for (int n : numbers) std::cout << n << " ";
std::cout << std::endl;
// 查找算法
auto it = std::ranges::find(numbers, 7);
if (it != numbers.end()) {
std::cout << "找到7,位置: " << std::distance(numbers.begin(), it) << std::endl;
}
// 计数算法
int count = std::ranges::count_if(numbers, [](int n) { return n % 2 == 0; });
std::cout << "偶数个数: " << count << std::endl;
// 复制算法
std::ranges::copy_if(numbers, dest.begin(), [](int n) { return n > 5; });
std::cout << "大于5的数: ";
for (int i = 0; i < std::ranges::count(numbers, true, [](int n) { return n > 5; }); ++i) {
std::cout << dest[i] << " ";
}
std::cout << std::endl;
// 变换算法
std::ranges::transform(numbers, dest.begin(), [](int n) { return n * n; });
std::cout << "平方值: ";
for (int i = 0; i < numbers.size(); ++i) {
std::cout << dest[i] << " ";
}
std::cout << std::endl;
// 其他常用算法
auto [min, max] = std::ranges::minmax(numbers);
std::cout << "最小值: " << min << ", 最大值: " << max << std::endl;
std::ranges::reverse(numbers);
std::cout << "反转后: ";
for (int n : numbers) std::cout << n << " ";
std::cout << std::endl;
bool all_positive = std::ranges::all_of(numbers, [](int n) { return n > 0; });
std::cout << "全部为正: " << (all_positive ? "是" : "否") << std::endl;
}
常见范围算法分类:
- 非修改序列算法 :
ranges::find
,ranges::count
,ranges::all_of
等 - 修改序列算法 :
ranges::copy
,ranges::transform
,ranges::replace
等 - 排序和相关算法 :
ranges::sort
,ranges::partial_sort
,ranges::nth_element
等 - 数值算法 :
ranges::accumulate
(注意:部分数值算法尚未有范围版本) - 集合算法 :
ranges::set_union
,ranges::set_intersection
等
投影参数
范围算法的一个重要特性是支持投影(Projections),允许在实际应用算法前转换元素:
cpp
#include <ranges>
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Student {
std::string name;
std::vector<int> scores;
// 计算平均分
double average() const {
if (scores.empty()) return 0;
int sum = 0;
for (int score : scores) sum += score;
return static_cast<double>(sum) / scores.size();
}
};
int main() {
std::vector<Student> students = {
{"Alice", {85, 90, 82}},
{"Bob", {76, 88, 95}},
{"Charlie", {90, 92, 98}},
{"David", {65, 75, 80}}
};
// 使用投影基于平均分排序
std::ranges::sort(students, {}, [](const Student& s) { return s.average(); });
// 显示结果
std::cout << "学生按平均分排序:" << std::endl;
for (const auto& student : students) {
std::cout << student.name << ": " << student.average() << std::endl;
}
// 找出平均分最高的学生
auto top_student = std::ranges::max_element(students, {},
[](const Student& s) { return s.average(); });
std::cout << "\n平均分最高的学生: " << top_student->name
<< " (" << top_student->average() << ")" << std::endl;
// 找出有满分(100)的学生
auto perfect_student = std::ranges::find_if(students,
[](const std::vector<int>& scores) {
return std::ranges::any_of(scores, [](int score) { return score == 100; });
},
&Student::scores // 投影:获取学生的分数向量
);
if (perfect_student != students.end()) {
std::cout << "\n有满分的学生: " << perfect_student->name << std::endl;
} else {
std::cout << "\n没有学生获得满分" << std::endl;
}
return 0;
}
投影的优势:
- 代码简洁性:无需创建单独的比较器函数
- 语义清晰:明确表达对哪些属性进行操作
- 可组合性:可以与其他算法参数(如比较器)组合
- 易于维护:当数据结构变化时,只需更改投影函数
视图(Views)基础
视图的概念
视图(View)是范围库中的核心概念,表示一个轻量级、非拥有元素的范围:
cpp
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 创建视图
auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// 视图不修改原始数据
std::cout << "原始数据:";
for (int n : numbers) std::cout << " " << n;
std::cout << std::endl;
std::cout << "视图内容:";
for (int n : even_numbers) std::cout << " " << n;
std::cout << std::endl;
// 修改原始数据会影响视图
numbers[0] = 0; // 将1改为0
std::cout << "修改后视图:";
for (int n : even_numbers) std::cout << " " << n;
std::cout << std::endl;
return 0;
}
视图的关键特性:
- 轻量级:创建视图通常是O(1)操作,不涉及元素复制
- 惰性求值:只在遍历时才执行操作
- 非拥有:视图不拥有元素,只引用原始范围
- 可组合:可以通过管道操作符组合多个视图
基本视图类型
C++20提供了多种预定义视图,位于std::views
命名空间:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
void demonstrate_basic_views() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// all:整个范围的视图
auto all_view = std::views::all(numbers);
// filter:过滤元素
auto even = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// transform:变换元素
auto squares = numbers | std::views::transform([](int n) { return n * n; });
// take:取前N个元素
auto first_five = numbers | std::views::take(5);
// drop:丢弃前N个元素
auto skip_five = numbers | std::views::drop(5);
// reverse:反转范围
auto reversed = numbers | std::views::reverse;
// 输出各种视图
std::cout << "原始数据:";
for (int n : all_view) std::cout << " " << n;
std::cout << std::endl;
std::cout << "偶数:";
for (int n : even) std::cout << " " << n;
std::cout << std::endl;
std::cout << "平方值:";
for (int n : squares) std::cout << " " << n;
std::cout << std::endl;
std::cout << "前5个:";
for (int n : first_five) std::cout << " " << n;
std::cout << std::endl;
std::cout << "跳过前5个:";
for (int n : skip_five) std::cout << " " << n;
std::cout << std::endl;
std::cout << "反转:";
for (int n : reversed) std::cout << " " << n;
std::cout << std::endl;
// iota:生成整数序列
auto sequence = std::views::iota(1, 6); // 1到5
std::cout << "整数序列:";
for (int n : sequence) std::cout << " " << n;
std::cout << std::endl;
// 字符串相关视图
std::string text = "Hello,World,C++,Ranges";
auto words = text | std::views::split(',');
std::cout << "分割字符串:" << std::endl;
for (auto word : words) {
for (char c : word) std::cout << c;
std::cout << std::endl;
}
}
常用预定义视图:
std::views::all
:引用整个范围std::views::filter
:根据谓词筛选元素std::views::transform
:变换每个元素std::views::take
:取前N个元素std::views::take_while
:取满足条件的前缀元素std::views::drop
:丢弃前N个元素std::views::drop_while
:丢弃满足条件的前缀元素std::views::reverse
:反转范围std::views::elements
:获取元组元素std::views::keys
/std::views::values
:获取键值对的键或值std::views::iota
:生成连续递增的整数序列std::views::split
:按分隔符分割范围std::views::join
:连接嵌套范围
视图与容器的区别
视图和容器有几个关键区别:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <chrono>
void compare_view_and_container() {
std::vector<int> numbers(1'000'000);
// 填充数据
for (int i = 0; i < numbers.size(); ++i) {
numbers[i] = i;
}
// 计时创建副本
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> filtered_container;
for (int n : numbers) {
if (n % 2 == 0) {
filtered_container.push_back(n);
}
}
auto mid = std::chrono::high_resolution_clock::now();
// 创建视图
auto filtered_view = numbers | std::views::filter([](int n) { return n % 2 == 0; });
auto end = std::chrono::high_resolution_clock::now();
// 计算时间
auto container_time = std::chrono::duration_cast<std::chrono::milliseconds>
(mid - start).count();
auto view_time = std::chrono::duration_cast<std::chrono::milliseconds>
(end - mid).count();
std::cout << "创建容器副本时间: " << container_time << " ms" << std::endl;
std::cout << "创建视图时间: " << view_time << " ms" << std::endl;
// 内存使用对比
std::cout << "容器元素数量: " << filtered_container.size() << std::endl;
std::cout << "视图元素数量: " << std::ranges::distance(filtered_view) << std::endl;
std::cout << "容器内存占用: " << filtered_container.size() * sizeof(int) << " 字节" << std::endl;
std::cout << "视图理论内存占用: < 100 字节" << std::endl;
// 修改原始数据影响
numbers[0] = 1; // 改为奇数
// 容器不受影响
std::cout << "修改原始数据后:" << std::endl;
std::cout << "容器首元素: " << filtered_container[0] << std::endl;
std::cout << "视图首元素: " << *filtered_view.begin() << std::endl;
}
主要区别:
-
内存所有权
- 容器拥有并管理其元素的内存
- 视图不拥有元素,只引用其他范围
-
创建成本
- 创建容器副本需要复制元素,时间和空间成本与元素数成正比
- 创建视图是O(1)操作,几乎没有开销
-
修改传播
- 修改容器副本不影响原始数据
- 修改原始数据会反映在引用它的视图中
-
惰性求值
- 容器中的元素是预先计算的
- 视图元素是按需计算的,可能多次计算
-
适用场景
- 容器适用于需要存储和拥有数据的场景
- 视图适用于临时转换和处理数据的场景
管道操作
管道语法
Ranges库引入了管道操作符(|
),使数据处理更加直观:
cpp
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用管道语法
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; }) // 偶数
| std::views::transform([](int n) { return n * n; }); // 平方
// 输出结果
std::cout << "偶数的平方:";
for (int n : result) std::cout << " " << n;
std::cout << std::endl;
// 管道操作符使用规则:
// 1. 传统函数调用风格
auto even_func = std::views::filter(numbers, [](int n) { return n % 2 == 0; });
// 2. 管道风格 (通常更易读)
auto even_pipe = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// 两种方式等效
std::cout << "函数调用风格:";
for (int n : even_func) std::cout << " " << n;
std::cout << std::endl;
std::cout << "管道风格:";
for (int n : even_pipe) std::cout << " " << n;
std::cout << std::endl;
return 0;
}
管道语法的优势:
- 可读性:从左到右的数据流更符合人类思维习惯
- 组合性:轻松组合多个操作,无需中间变量
- 简洁性:减少样板代码
- 表达力:清晰表达数据转换意图
视图组合
Ranges库的强大之处在于可以轻松组合多个视图:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype> // for toupper
int main() {
std::vector<std::string> words = {
"apple", "banana", "cherry", "date", "elderberry",
"fig", "grape", "honeydew"
};
// 组合多个视图
auto processed = words
| std::views::filter([](const std::string& s) {
return s.length() > 5; // 只要长词
})
| std::views::transform([](std::string s) {
// 转换为大写
for (char& c : s) c = std::toupper(c);
return s;
})
| std::views::take(3); // 只取前3个
// 输出结果
std::cout << "处理结果:" << std::endl;
for (const auto& word : processed) {
std::cout << word << std::endl;
}
// 更复杂的组合示例
std::vector<std::vector<int>> nested = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
auto flattened = nested
| std::views::join // 展平嵌套容器
| std::views::filter([](int n) { return n % 2 != 0; }) // 奇数
| std::views::transform([](int n) { return n * n; }) // 平方
| std::views::reverse; // 反转顺序
std::cout << "\n展平处理:";
for (int n : flattened) std::cout << " " << n;
std::cout << std::endl;
return 0;
}
视图组合的关键点:
- 顺序重要性:操作按照管道中指定的顺序执行
- 效率考虑:某些组合可能比其他组合更高效
- 视图延迟特性:无论多少操作组合,都只在遍历时执行
- 表达能力:复杂的数据转换可以简洁地表达
惰性求值
视图的一个核心特征是惰性求值,只在需要结果时才执行操作:
cpp
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 创建一个包含多个操作的视图
std::cout << "创建视图..." << std::endl;
auto result = numbers
| std::views::filter([](int n) {
std::cout << "过滤: " << n << std::endl;
return n % 2 == 0;
})
| std::views::transform([](int n) {
std::cout << "变换: " << n << std::endl;
return n * n;
});
std::cout << "视图创建完成,尚未执行任何操作" << std::endl;
// 获取首个元素
std::cout << "\n获取第一个元素..." << std::endl;
auto it = result.begin();
std::cout << "第一个元素: " << *it << std::endl;
// 获取下一个元素
std::cout << "\n获取下一个元素..." << std::endl;
++it;
std::cout << "下一个元素: " << *it << std::endl;
// 遍历剩余元素
std::cout << "\n遍历剩余元素..." << std::endl;
for (; it != result.end(); ++it) {
std::cout << "元素: " << *it << std::endl;
}
// 重新遍历整个视图
std::cout << "\n再次遍历(注意操作会重新执行)..." << std::endl;
for (int n : result) {
std::cout << "结果: " << n << std::endl;
}
return 0;
}
惰性求值的特点和优势:
- 按需计算:只计算实际需要的元素
- 节省内存:不需要存储中间结果
- 支持无限序列:可以处理理论上无限的数据流
- 避免不必要的工作:如果只需要前几个结果,不会处理所有元素
- 潜在缺点:多次遍历会重复计算,有时需要具体化(materialize)结果
实际应用示例
数据过滤与转换
Ranges库特别适合数据过滤和转换操作:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
struct Product {
std::string name;
double price;
int stock;
bool discontinued;
};
void product_processing() {
std::vector<Product> products = {
{"Laptop", 1200.0, 5, false},
{"Smartphone", 800.0, 12, false},
{"Tablet", 400.0, 8, false},
{"MP3 Player", 50.0, 0, true},
{"Headphones", 120.0, 20, false},
{"Camera", 600.0, 3, false},
{"Printer", 250.0, 0, false},
{"DVD Player", 80.0, 1, true}
};
// 查找可购买的产品(有库存且未停产)
auto available = products
| std::views::filter([](const Product& p) {
return p.stock > 0 && !p.discontinued;
});
std::cout << "可购买的产品:" << std::endl;
for (const auto& product : available) {
std::cout << product.name << " - $" << product.price
<< " (库存: " << product.stock << ")" << std::endl;
}
// 找出价格在一定范围内的产品
auto mid_range = products
| std::views::filter([](const Product& p) {
return p.price >= 100 && p.price <= 500;
})
| std::views::transform([](const Product& p) {
// 返回产品名称和价格
return std::make_pair(p.name, p.price);
});
std::cout << "\n中等价位产品:" << std::endl;
for (const auto& [name, price] : mid_range) {
std::cout << name << " - $" << price << std::endl;
}
// 计算所有可用产品的总库存价值
double total_value = 0.0;
for (const auto& p : available) {
total_value += p.price * p.stock;
}
std::cout << "\n总库存价值: $" << total_value << std::endl;
// 按价格排序并显示前3名最贵的产品
auto top_priced = products
| std::views::filter([](const Product& p) {
return p.stock > 0;
})
| std::ranges::to<std::vector>() // 具体化为向量
| std::views::transform([](const Product& p) {
return std::make_pair(p.name, p.price);
});
// 注意:视图不保证保留原始顺序,需要排序
std::vector<std::pair<std::string, double>> sorted_prices(top_priced.begin(), top_priced.end());
std::ranges::sort(sorted_prices, std::ranges::greater{}, &std::pair<std::string, double>::second);
std::cout << "\n价格最高的3个有库存产品:" << std::endl;
for (const auto& [name, price] : sorted_prices | std::views::take(3)) {
std::cout << name << " - $" << price << std::endl;
}
}
字符串处理
Ranges库使字符串处理更加简洁优雅:
cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
#include <cctype>
void string_processing() {
std::string text = "The quick brown fox jumps over the lazy dog";
// 将字符串拆分为单词
auto words = text | std::views::split(' ');
std::cout << "单词列表:" << std::endl;
for (auto word : words) {
// 将视图转换为string以便打印
std::string w(word.begin(), word.end());
std::cout << w << std::endl;
}
// 找出所有长度大于3的单词
auto long_words = text
| std::views::split(' ')
| std::views::filter([](auto word) {
return std::ranges::distance(word) > 3;
});
std::cout << "\n长度大于3的单词:" << std::endl;
for (auto word : long_words) {
std::string w(word.begin(), word.end());
std::cout << w << std::endl;
}
// 将每个单词的首字母大写
std::string capitalized;
bool new_word = true;
for (char c : text) {
if (new_word && std::isalpha(c)) {
capitalized += std::toupper(c);
new_word = false;
} else {
capitalized += c;
if (c == ' ') new_word = true;
}
}
std::cout << "\n首字母大写: " << capitalized << std::endl;
// 计算文本中不同字母的出现频率(不区分大小写)
std::array<int, 26> letter_counts = {0};
for (char c : text | std::views::filter(::isalpha) | std::views::transform(::tolower)) {
letter_counts[c - 'a']++;
}
std::cout << "\n字母频率:" << std::endl;
for (int i = 0; i < 26; ++i) {
if (letter_counts[i] > 0) {
std::cout << static_cast<char>('a' + i) << ": " << letter_counts[i] << std::endl;
}
}
}
数值计算
Ranges库可以简化数值计算和数据分析:
cpp
#include <ranges>
#include <vector>
#include <iostream>
#include <numeric>
#include <cmath>
#include <iomanip>
void numerical_calculations() {
// 生成1到100的序列
auto numbers = std::views::iota(1, 101);
// 计算平均值
double sum = std::accumulate(numbers.begin(), numbers.end(), 0.0);
double mean = sum / std::ranges::distance(numbers);
std::cout << "平均值: " << mean << std::endl;
// 计算平方和
double sum_squares = 0.0;
for (int n : numbers) {
sum_squares += n * n;
}
std::cout << "平方和: " << sum_squares << std::endl;
// 生成斐波那契数列的前20个数
std::vector<int> fibonacci;
fibonacci.reserve(20);
fibonacci.push_back(0);
fibonacci.push_back(1);
for (int i = 2; i < 20; ++i) {
fibonacci.push_back(fibonacci[i-1] + fibonacci[i-2]);
}
std::cout << "\n斐波那契数列:";
for (int n : fibonacci) std::cout << " " << n;
std::cout << std::endl;
// 查找小于1000的所有斐波那契数中的偶数
auto even_fibs = fibonacci
| std::views::filter([](int n) { return n < 1000 && n % 2 == 0; });
std::cout << "小于1000的偶数斐波那契数:";
for (int n : even_fibs) std::cout << " " << n;
std::cout << std::endl;
// 生成前10个质数
std::vector<int> primes;
for (int n = 2; primes.size() < 10; ++n) {
bool is_prime = true;
for (int i = 2; i <= std::sqrt(n); ++i) {
if (n % i == 0) {
is_prime = false;
break;
}
}
if (is_prime) primes.push_back(n);
}
std::cout << "\n前10个质数:";
for (int p : primes) std::cout << " " << p;
std::cout << std::endl;
// 计算每个质数与下一个质数的差
auto prime_gaps = primes
| std::views::slide(2) // C++23功能,在某些编译器可能不可用
| std::views::transform([](auto pair) {
return *std::next(pair.begin()) - *pair.begin();
});
// 如果slide不可用,可以使用替代方法
std::vector<int> gaps;
for (size_t i = 0; i < primes.size() - 1; ++i) {
gaps.push_back(primes[i+1] - primes[i]);
}
std::cout << "质数间隔:";
for (int gap : gaps) std::cout << " " << gap;
std::cout << std::endl;
}
总结
C++20的范围(Ranges)库彻底改变了我们处理集合和算法的方式,为C++带来了更现代、更函数式的编程风格。主要优势包括:
- 更简洁的语法:直接对容器操作,无需显式传递迭代器对
- 更好的可读性:代码表达数据流向,更符合人类思维模式
- 组合能力:通过管道操作符轻松组合多个操作
- 惰性求值:按需执行操作,提高效率
- 视图抽象:轻量级引用原始数据,避免不必要的复制
- 投影参数:简化复杂数据类型的处理
范围库的基础概念、范围算法和基本视图类型为处理各种数据集合提供了强大的工具。通过管道操作和惰性求值,我们可以构建高效的数据处理流程,使代码更加简洁明了。
在下一篇文章中,我们将探讨Ranges库的更多高级特性,包括复杂视图组合、自定义视图、性能优化技巧以及与其他C++20特性的结合使用。
这是我C++学习之旅系列的第五十一篇技术文章。查看完整系列目录了解更多内容。