C++20 引入的 ranges 库为容器操作提供了更为直观、表达式化的语法。相比传统的 for 循环、迭代器组合,ranges 让我们能以更接近“管道”的方式编写代码,既易读又能让编译器在编译期完成更多优化。本文将从基础使用、常见视图(view)以及自定义视图的实现几个方面,详细探讨如何在实际项目中高效运用 C++20 ranges。
1. Ranges 与传统迭代器的区别
传统写法:
std::vector <int> v = {1,2,3,4,5};
for(auto it = v.begin(); it != v.end(); ++it){
std::cout << *it << ' ';
}
在这个循环里,我们要显式地处理迭代器的起点和终点,甚至要在每一步手动解引用。若想做过滤、映射等操作,需要配合 std::transform、std::copy_if 等算法,代码显得冗长。
而使用 ranges 则可以这样写:
#include <ranges>
#include <iostream>
std::vector <int> v = {1,2,3,4,5};
for(auto x : v | std::views::filter([](int n){ return n%2==0; })) {
std::cout << x << ' ';
}
这里我们使用了 | 管道符,把容器与一个 filter 视图连接起来。filter 视图会在迭代时动态判断元素是否满足条件。整个过程无需显式管理迭代器,语义更为直观。
2. 常用视图(views)快速上手
| 视图 | 作用 | 示例 |
|---|---|---|
std::views::all |
获取容器的默认视图 | `auto v = std::vector |
| {1,2,3}; auto view = v | std::views::all;` | |
std::views::filter |
按条件筛选 | v | std::views::filter([](int n){ return n>2; }) |
std::views::transform |
对每个元素做映射 | v | std::views::transform([](int n){ return n*n; }) |
std::views::take |
截取前 N 个 | v | std::views::take(3) |
std::views::drop |
跳过前 N 个 | v | std::views::drop(2) |
std::views::reverse |
反转 | v | std::views::reverse |
std::views::zip |
组合两个容器 | auto zipped = std::views::zip(v, u); |
std::views::concat |
合并容器 | auto merged = std::views::concat(v, u); |
这些视图都是轻量级的惰性操作,它们不会立即产生新的容器,而是延迟执行,直到真正需要访问元素时才触发。
3. 将 ranges 与算法结合
ranges 允许我们使用 std::ranges:: 命名空间下的算法。与传统算法不同,新的算法可以直接接受视图作为参数,返回一个视图(或值),而不是像 std::sort 需要修改原容器。示例:
#include <ranges>
#include <vector>
#include <algorithm>
#include <iostream>
std::vector <int> data = {5,3,8,1,4};
auto sorted = data | std::views::sort(); // 视图返回已排序的视图
for(int x : sorted)
std::cout << x << ' '; // 输出 1 3 4 5 8
在这个例子中,std::views::sort() 并没有修改原始 data,而是返回了一个已经排好序的视图。若想要真正复制排序结果,可结合 std::ranges::to<std::vector>()(C++23 的功能):
auto sorted_vec = data | std::views::sort() | std::ranges::to<std::vector>();
4. 自定义视图(Custom View)
有时我们需要一种内置视图未提供的特殊行为。C++20 通过 std::ranges::view_interface 让实现自定义视图变得简单。下面演示一个自定义视图 my_transform_view,它对输入容器进行平方操作。
#include <ranges>
#include <iostream>
#include <vector>
template<std::ranges::input_range R>
requires std::is_arithmetic_v<std::ranges::range_value_t<R>>
class my_transform_view : public std::ranges::view_interface<my_transform_view<R>> {
R base_;
public:
explicit my_transform_view(R base) : base_(std::move(base)) {}
auto begin() {
return std::ranges::begin(base_);
}
auto end() {
return std::ranges::end(base_);
}
template<class It>
class iterator {
It current_;
public:
using iterator_category = std::input_iterator_tag;
using value_type = decltype((*current_)*(*current_));
iterator(It current) : current_(current) {}
value_type operator*() const { return (*current_)*(*current_); }
iterator& operator++() { ++current_; return *this; }
bool operator==(const iterator& other) const { return current_ == other.current_; }
};
iterator<std::ranges::iterator_t<R>> begin() { return iterator{begin(base_)}; }
iterator<std::ranges::iterator_t<R>> end() { return iterator{end(base_)}; }
};
template<std::ranges::input_range R>
my_transform_view(R) -> my_transform_view <R>;
int main() {
std::vector <int> nums = {1,2,3,4};
for(int val : nums | my_transform_view{}) {
std::cout << val << ' ';
}
}
上述代码中,my_transform_view 对每个元素做平方,并通过继承 std::ranges::view_interface 自动获得了范围(range)接口。调用时只需 nums | my_transform_view{} 即可。
5. 性能与编译期优化
因为 ranges 采用惰性求值,链式视图的所有操作在内部会被聚合为一次遍历,避免了中间容器的产生。编译器还能在编译期推导并消除不必要的临时对象,特别是当视图与 std::ranges::to 结合使用时,编译器可以生成更高效的循环。实际测量表明,使用 ranges 的代码在性能上与手写优化后的循环相当,甚至更优。
6. 代码示例:从文件读取整数并计算平方和
#include <iostream>
#include <fstream>
#include <vector>
#include <ranges>
#include <numeric>
int main() {
std::ifstream fin("numbers.txt");
std::vector <int> nums{std::istream_iterator<int>(fin), std::istream_iterator<int>()};
// 只取偶数,平方后求和
auto result = std::ranges::fold_left(
nums | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; }),
0LL, std::plus{}
);
std::cout << "偶数平方和 = " << result << '\n';
}
上述程序完整演示了文件读取、过滤、映射、聚合的完整管道,全部使用 ranges 表达式,代码简洁且易于维护。
7. 结语
C++20 的 ranges 为容器操作提供了更优雅、声明式的写法。它让代码更像“流水线”,读者可以在单行内完成复杂的容器变换,且编译器能在编译期完成大量优化。掌握 ranges 的基本视图和算法,再结合自定义视图,便能在日常项目中大幅提升代码质量和开发效率。未来,随着标准继续演进,ranges 的功能会愈发丰富,值得每位 C++ 开发者持续关注。