在 C++20 标准中,Ranges 被引入以彻底改变我们对容器和迭代器的使用方式。通过将容器与视图(views)、适配器(adaptors)以及管道符号(|)结合使用,Ranges 让代码既简洁又易于理解。本文将从基本概念出发,逐步演示如何使用 Ranges 进行常见的容器操作,并讨论其优点与潜在陷阱。
1. 什么是 Ranges?
Ranges 主要由三部分组成:
- 范围(Range):任何可通过
begin()和end()获取迭代器的对象,例如 `std::vector `、`std::array` 或 `std::string`。在 C++20 中,范围也可以是 `std::ranges::subrange` 之类的自定义类型。 - 视图(View):对已有范围进行“懒惰”变换的工具。例如
std::views::filter、std::views::transform、std::views::take等,它们不会复制数据,而是在遍历时即时计算。 - 适配器(Adaptor):对视图进行进一步变换的工具,常见的有
std::ranges::take_exactly、std::ranges::stride等。
通过管道符号 |,我们可以把多个适配器串联起来,形成一个完整的处理链。
2. 基础示例:过滤和映射
假设我们有一个整数向量,想要取出所有偶数并将其平方:
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector <int> vec{1, 2, 3, 4, 5, 6};
auto result = vec | std::views::filter([](int x){ return x % 2 == 0; })
| std::views::transform([](int x){ return x * x; });
for (int v : result) {
std::cout << v << ' ';
}
std::cout << '\n';
}
输出:
4 16 36
注意,result 本身不是一个容器,而是一个可遍历的范围。直到我们遍历它时才会执行过滤和映射。
3. 视图与迭代器的结合
如果你需要在传统算法(如 std::for_each)中使用视图,可以将其包装为 std::ranges::subrange:
auto subrange = std::ranges::subrange(result.begin(), result.end());
std::for_each(subrange.begin(), subrange.end(), [](int x){ std::cout << x << '\n'; });
但更常见的做法是直接使用范围语义,或者将视图转换为 std::vector:
std::vector <int> vec2(result.begin(), result.end());
4. 组合适配器
Ranges 支持链式适配器组合,极大提升表达力。例如,要取前 5 个偶数的平方:
auto final_view = vec | std::views::filter([](int x){ return x % 2 == 0; })
| std::views::transform([](int x){ return x * x; })
| std::views::take(5);
这里 take 是一个视图适配器,限制了元素数量。若你想取第 3 到第 7 个元素,可以使用 views::slice:
auto sliced = vec | std::views::slice(2, 7); // 索引从 0 开始
5. 与 std::ranges::for_each 的协作
C++20 引入了 std::ranges::for_each,它接受范围而不是迭代器对:
std::ranges::for_each(result, [](int x){ std::cout << x << '\n'; });
这使得遍历代码更加简洁。
6. 性能与懒惰求值
视图是懒惰的:它们不会在创建时执行任何计算。只有当你真正访问元素时,才会触发相应的变换。这意味着:
- 内存占用:与传统容器相比,视图不需要额外的存储空间。
- 计算时机:如果你只需要查看部分元素,后面的变换不必被执行。
- 可能的性能瓶颈:如果链中有多个昂贵的变换,仍然会在遍历时一次性执行,导致每个元素多次处理。
7. 适配器常用列表
| 适配器 | 作用 |
|---|---|
views::filter |
过滤元素 |
views::transform |
变换元素 |
views::reverse |
反转序列 |
views::take |
取前 N 个元素 |
views::drop |
跳过前 N 个元素 |
views::stride |
以步长选择元素 |
views::concat |
连接多个序列 |
views::join |
将二维范围变为一维 |
views::zip |
组合多个序列(C++23) |
8. 代码片段:统计字符串中不同单词出现次数
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <ranges>
#include <sstream>
int main() {
std::string text = "hello world hello ranges c++20 ranges";
std::istringstream iss(text);
std::unordered_map<std::string, int> freq;
auto words = std::views::istream<std::string>(iss)
| std::views::transform([](std::string s){ return std::move(s); });
for (auto const& w : words) {
++freq[w];
}
for (auto const& [word, count] : freq) {
std::cout << word << " : " << count << '\n';
}
}
输出示例:
hello : 2
world : 1
ranges : 2
c++20 : 1
此例展示了 std::views::istream 将输入流视作范围,配合 transform 与计数器,构建了一个简洁的统计程序。
9. 常见陷阱与调试技巧
-
视图失效
当基底容器被销毁或修改时,视图可能变得无效。请确保视图的生命周期不超过其来源容器。 -
过度链式调用
过长的视图链可导致可读性下降。建议在需要时拆分为中间变量,或使用constexpr定义视图函数。 -
性能分析
对链中每一步的复杂度进行估算,避免在循环中出现不必要的复制。可使用std::ranges::cpp20::views::transform的noexcept标记来判断是否会抛异常。 -
调试视图
直接打印视图不行。可用std::ranges::to<std::vector>()(C++23)或手动复制到容器,再打印。
10. 小结
C++20 Ranges 为容器操作提供了强大的语义与表达力,能够让代码更具可读性、可维护性,并在大多数情况下保持良好的性能。掌握视图与适配器的基本用法后,你会发现许多传统手写循环与算法可以被更简洁、更声明式的表达方式所取代。未来 C++23 将继续丰富 Ranges(如 views::zip、views::cartesian_product 等),值得持续关注。
祝你在 C++20 的 Ranges 世界里玩得开心,写出更优雅的代码!