在现代C++中,std::ranges 为容器和迭代器提供了一套强大的适配器与算法,极大地提升了代码的可读性和表达力。本文将从基本概念入手,演示如何使用范围适配器对数据进行链式处理,并说明常见的使用陷阱与最佳实践。
一、为什么使用范围适配器?
传统的 STL 代码往往需要嵌套 std::copy_if、std::transform 等算法,且每一步都需要显式声明临时容器或迭代器。例如,过滤偶数后取平方再求和的写法:
std::vector <int> src{1,2,3,4,5,6,7,8,9,10};
std::vector <int> tmp;
std::copy_if(src.begin(), src.end(), std::back_inserter(tmp),
[](int v){ return v % 2 == 0; });
std::transform(tmp.begin(), tmp.end(), tmp.begin(),
[](int v){ return v * v; });
int sum = std::accumulate(tmp.begin(), tmp.end(), 0);
这段代码需要多个临时变量、重复的迭代器写法,难以在一行中表达整个数据流。使用范围适配器可以写成:
int sum = src | std::views::filter([](int v){ return v % 2 == 0; })
| std::views::transform([](int v){ return v * v; })
| std::views::elements <int>() // 转成可迭代的视图
| std::views::fold(0, std::plus<>{});
(std::views::fold 需要自定义,或直接使用 std::ranges::accumulate)
这种链式写法既直观又省去了中间容器,提升了代码可读性。
二、核心适配器
| 适配器 | 作用 | 示例 |
|---|---|---|
std::views::filter |
过滤元素 | | std::views::filter([](int v){ return v > 10; }) |
std::views::transform |
转换元素 | | std::views::transform([](int v){ return std::to_string(v); }) |
std::views::take |
取前 N 个 | | std::views::take(5) |
std::views::drop |
跳过前 N 个 | | std::views::drop(3) |
std::views::reverse |
反转 | | std::views::reverse |
std::views::join |
展平嵌套容器 | | std::views::join |
std::views::stride |
取步长 | | std::views::stride(2) |
std::views::concat |
连接两个范围 | std::views::concat(v1, v2) |
注意:
std::views::transform的返回类型是一个视图而不是容器;若需要迭代或取值,视图本身即是可迭代的,常用for (auto&& x : view)。
三、常见陷阱
-
视图的生命周期
std::views::filter等视图内部存储了原始容器的引用。若原始容器被销毁,视图将悬空。
解决方案:保持原始容器在视图使用期间的生命周期,或将视图转换为实质性容器:auto new_vec = std::vector(view.begin(), view.end()); -
惰性求值
视图默认是惰性的,只有在遍历或需要终止条件时才会触发。若在链式调用中使用std::views::take,后续的transform等将只处理前 N 个元素。
提示:理解惰性求值可避免不必要的计算。 -
自定义适配器
std::ranges::view需要满足std::ranges::view_interface,实现begin,end,size等。自定义适配器时请务必继承std::ranges::view_interface并使用constexpr,否则可能导致编译错误。 -
并行执行
std::ranges::for_each支持并行策略,但仅在可并行迭代器上有效。使用std::execution::par_unseq时需确保操作是无副作用的。
四、完整示例:统计文件中单词出现次数
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include <ranges>
#include <algorithm>
int main() {
std::ifstream fin("sample.txt");
std::string word;
std::unordered_map<std::string, int> freq;
// 读取单词并统计
std::ranges::for_each(std::istream_iterator<std::string>(fin),
std::istream_iterator<std::string>(),
[&](const std::string& w){ ++freq[w]; });
// 输出频率高于 3 的单词
auto high_freq = freq | std::views::values
| std::views::filter([](int c){ return c > 3; });
std::cout << "单词出现次数 > 3 的统计如下:\n";
for (int c : high_freq)
std::cout << c << '\n';
}
这里利用
std::views::values直接访问unordered_map的值,链式过滤后再遍历输出。若想按单词排序,可在freq之上使用std::views::keys | std::views::transform([](auto&& k){ return std::string(k); })并与std::ranges::sort组合。
五、总结
- 范围适配器 通过惰性求值和链式调用,提供了更简洁、可读的算法表达方式。
- 需要注意 视图生命周期 与 惰性求值 的影响,避免悬空引用与多余计算。
- 在 并行 与 自定义 适配器时,遵循标准库的接口要求,确保兼容性。
随着 C++23 对 ranges 的进一步完善,未来将出现更多内置适配器与更直观的语法糖。掌握 std::ranges 已成为现代 C++ 开发者必备的技能之一。