在 C++20 中,std::ranges 提供了一组强大而优雅的工具,让我们能够以声明式的方式描述集合的变换。相比传统的循环与临时容器,范围视图(views)采用惰性求值,既节省内存,又提高了代码的可读性。本文将从概念、典型用例、性能优化以及常见陷阱四个角度,阐述如何在实际项目中高效运用 C++20 范围视图。
1. 基础概念:范围、视图与管道
- 范围(range):一个支持
begin()和end()的对象(或两者是返回迭代器的函数)。标准库中的std::vector、std::array、C 风格数组等均为范围。 - 视图(view):是一个范围的变换,它本身不拥有数据,而是通过惰性方式“看到”原始范围并按需生成结果。典型的视图包括
std::views::filter、std::views::transform、std::views::take等。 - 管道(pipeline):将视图链式组合的语法糖。C++20 引入了
<=>与operator|的重载,使得range | view1 | view2 | view3形式可读性极佳。
代码示例:简单管道
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector <int> data{1, 2, 3, 4, 5, 6};
auto result = data
| std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; })
| std::views::take(2);
for (int v : result)
std::cout << v << ' '; // 输出:4 16
}
上述代码将原始向量中的偶数筛选出来,再平方,最后取前两个结果,整个过程无须显式循环。
2. 典型用例
2.1 统计符合条件的元素数量
#include <ranges>
#include <vector>
size_t count_large(const std::vector <int>& v, int threshold) {
return std::ranges::count_if(v, [threshold](int n){ return n > threshold; });
}
这里我们直接利用 std::ranges::count_if,无需手动计数。
2.2 生成斐波那契数列
#include <iostream>
#include <ranges>
#include <vector>
auto fib_sequence(int n) {
std::vector<std::pair<int, int>> cache{ {0, 1} };
return std::views::iota(0, n)
| std::views::transform([&cache](int i) {
if (i < static_cast<int>(cache.size()))
return cache[i].first;
while (static_cast <int>(cache.size()) <= i) {
auto [a, b] = cache.back();
cache.push_back({b, a + b});
}
return cache[i].first;
});
}
int main() {
for (int x : fib_sequence(10))
std::cout << x << ' ';
}
虽然示例略显繁琐,但演示了视图中如何结合状态来生成自定义序列。
2.3 链接多重筛选和排序
#include <vector>
#include <ranges>
#include <algorithm>
#include <iostream>
struct Person { std::string name; int age; };
int main() {
std::vector <Person> people = {
{"Alice", 30}, {"Bob", 25}, {"Charlie", 35},
{"Dave", 30}, {"Eve", 28}
};
auto sorted = people
| std::views::filter([](const Person& p){ return p.age >= 30; })
| std::views::transform([](const Person& p){ return p.name; })
| std::views::common; // 转为可反复迭代的容器
std::vector<std::string> result(sorted.begin(), sorted.end());
std::sort(result.begin(), result.end());
for (const auto& name : result)
std::cout << name << '\n';
}
此处先过滤年龄大于等于 30 的人,然后提取姓名,再转成可重复遍历的容器(std::views::common),最后进行排序。
3. 性能与实现细节
3.1 惰性求值 vs 立即求值
视图的惰性特性意味着不产生临时容器,遍历时逐个生成值,内存占用最低。只有在真正需要结果时(如 std::ranges::for_each 或 std::vector 构造函数),视图才会触发计算。
3.2 views::common 的角色
大多数视图返回的是 非可重复遍历(不具备 std::ranges::common_range)的范围,意味着只能一次性遍历。例如,views::filter 返回的范围只能从 begin() 到 end() 迭代一次。若想多次遍历,需使用 views::common 或复制到容器中。
3.3 内存分配与缓存
当视图链中包含状态或需要缓存的操作(如 std::views::unique)时,编译器会生成相应的内部缓存结构。一般情况下,这些缓存是固定大小或按需分配的,几乎不会导致显著额外开销。
4. 常见陷阱与最佳实践
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 过滤空值 | std::views::filter([](auto&& x){ return x; }) 对布尔值有效,但对指针需要显式比较 |
使用 std::views::filter([](auto&& p){ return p != nullptr; }) |
| 多次遍历 | 视图默认一次性 | 用 std::views::common 或缓存到容器 |
| 性能评估 | 未测量 | 使用 std::ranges::for_each 与 std::chrono 对比传统循环 |
| 视图链过长 | 可读性下降 | 用 auto rng = ... | std::views::transform(...); 先赋值,再组合,或定义小函数 |
5. 结语
C++20 的范围视图与管道式编程让集合操作更像数学表达式:简洁、直观且高效。掌握好惰性求值、适当的缓存与多次遍历技巧,就能让日常代码从“繁琐循环”跃升为“优雅管道”,同时保持甚至提升性能。希望本文能为你在项目中快速上手并充分利用这一强大特性提供实用参考。