在 C++20 中,范围(ranges)和视图(views)极大地提升了标准库的表达力和可组合性。通过使用视图,程序员可以像对待容器一样对数据流进行组合、变换和过滤,但所有操作都是懒执行、无副作用的。本文将重点介绍三大范围适配器:filter、transform 和 take,以及如何将它们组合使用来解决常见问题。
1. std::views::filter
filter 适配器接受一个谓词(predicate),只保留满足条件的元素。其实现基于迭代器协议,内部使用 std::ranges::find_if 或自定义跳过逻辑,使得过滤过程在遍历时才进行。
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector <int> nums = {1, 2, 3, 4, 5, 6};
auto evens = nums | std::views::filter([](int n){ return n % 2 == 0; });
for (int n : evens) std::cout << n << ' '; // 输出 2 4 6
}
典型使用场景
- 日志过滤:只保留错误级别的日志条目。
- 输入校验:在读取数据流时即去除无效记录。
- 延迟加载:在处理大文件时只对符合条件的行做进一步操作。
2. std::views::transform
transform 与标准库中的 std::transform 类似,但它返回的是一个视图。你可以在流中做任何类型转换、计算或包装操作,仍保持懒惰。
#include <iostream>
#include <vector>
#include <ranges>
#include <string_view>
int main() {
std::vector<std::string> words = {"hello", "world", "ranges"};
auto lengths = words | std::views::transform([](auto&& s){ return s.size(); });
for (auto len : lengths) std::cout << len << ' '; // 输出 5 5 6
}
典型使用场景
- 字段映射:从结构体列表中提取某一字段。
- 数据序列化:把对象转换为字符串或字节流。
- 多级转换:与
filter结合先筛选再变换。
3. std::views::take
take 适配器允许你截取前 N 个元素。与 std::vector 的 subvector 或 std::slice 不同,take 只截取一次视图,随后所有操作仍保持懒惰。
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector <int> seq = {10, 20, 30, 40, 50, 60};
auto first_three = seq | std::views::take(3);
for (int n : first_three) std::cout << n << ' '; // 输出 10 20 30
}
典型使用场景
- 分页:仅显示当前页面的数据。
- 样本抽取:从大集合中随机或顺序抽取前 N 项进行预览。
- 限流:在异步流中限制并发处理的数量。
4. 组合使用实例:处理日志文件
假设有一个日志文件,每行格式为 LEVEL: message,我们想要提取所有错误级别日志的前 5 行的消息。
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <ranges>
#include <vector>
int main() {
std::ifstream file("log.txt");
std::string line;
std::vector<std::string> logs;
// 读取文件为行
while (std::getline(file, line))
logs.push_back(line);
auto error_messages = logs
| std::views::filter([](const std::string& l){ return l.rfind("ERROR:", 0) == 0; })
| std::views::transform([](const std::string& l){ return l.substr(6); }) // 去掉前缀
| std::views::take(5);
std::cout << "Top 5 error logs:\n";
for (auto& msg : error_messages)
std::cout << "- " << msg << '\n';
}
此示例演示了:
- 先通过
filter筛选出错误日志; - 再用
transform去掉ERROR:前缀; - 最后
take(5)截取前 5 条。
整个过程都是懒惰的:只有在遍历 error_messages 时,才会触发对应的 filter 与 transform 操作,避免了不必要的内存拷贝与临时对象。
5. 性能与注意事项
- 懒惰性:所有视图都是惰性求值,真正迭代时才执行。若链过长,可能导致多次遍历同一元素;可使用
std::ranges::view::all或std::ranges::to<std::vector>把中间结果缓存。 - 生命周期:视图内部捕获外部引用时,请确保引用的生命周期足够长。使用
std::ref或std::cref可以安全捕获引用。 - 容器兼容性:大多数容器(
std::vector,std::array,std::deque等)都支持视图;自定义容器需要满足std::ranges::input_range约束。
6. 小结
std::views::filter、std::views::transform 与 std::views::take 为 C++20 提供了强大的“流式”数据处理工具。通过组合这些适配器,你可以以声明式、可读性高且高效的方式处理复杂的数据转换、筛选和截取任务。掌握它们后,许多常见的算法任务都可以用几行代码完成,减少样板代码并降低错误率。祝你在 C++20 的范围世界里玩得开心!