在 C++20 之后,标准库引入了 std::ranges 子域,为容器、算法以及视图(view)提供了更自然、更安全、更简洁的接口。相较于传统的基于迭代器的算法调用,std::ranges 通过范围(range)来隐藏迭代器细节,让代码更易读、易维护,同时提供了更强的类型安全性。本文将通过几个实际例子来说明 std::ranges 的强大之处,并展示如何在日常项目中快速上手。
1. 传统算法写法与 ranges 对比
#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>
int main() {
std::vector <int> nums = {1, 2, 3, 4, 5};
// 传统写法:使用 std::copy_if
std::vector <int> even;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(even),
[](int n){ return n % 2 == 0; });
// 传统写法:求和
int sum = std::accumulate(nums.begin(), nums.end(), 0);
std::cout << "Even numbers: ";
for (int n : even) std::cout << n << ' ';
std::cout << "\nSum: " << sum << '\n';
}
上述代码在做两个操作:筛选偶数、计算总和。虽然功能完整,但代码中反复出现了迭代器的使用,显得冗长且容易出错。std::ranges 让这些操作可以更简洁地链式调用。
2. ranges 的核心概念
| 关键词 | 说明 |
|---|---|
| Range | 表示一段可遍历的元素序列,例如 `std::vector |
、std::array、std::initializer_list` 等。 |
|
| View | 对原始 Range 的无所有权(non-owning)包装,支持延迟求值(lazy evaluation),如 std::views::filter, std::views::transform 等。 |
| Action | 对 View 进行一次性计算的操作,如 std::ranges::for_each, std::ranges::sort 等。 |
这些概念使得 ranges 能够在表达式层面上构建“管道”,并在需要时才触发执行。
3. 使用 std::ranges 进行筛选和求和
#include <vector>
#include <ranges>
#include <numeric>
#include <iostream>
int main() {
std::vector <int> nums = {1, 2, 3, 4, 5};
// 1. 通过视图筛选偶数
auto evens = nums | std::views::filter([](int n){ return n % 2 == 0; });
// 2. 将视图转换为 vector
std::vector <int> even_vec(evens.begin(), evens.end());
// 3. 直接在范围上求和
int sum = std::ranges::accumulate(nums, 0);
std::cout << "Even numbers: ";
for (int n : even_vec) std::cout << n << ' ';
std::cout << "\nSum: " << sum << '\n';
}
关键点
- 管道符号 (
|):将容器与视图链式连接。可视为“从左往右”处理数据。 - 无迭代器暴露:不再显式写出
begin()/end(),提高代码可读性。 - 延迟求值:
evens并未立刻产生结果,只有在遍历或转换为容器时才执行。
4. 链式多重视图
假设我们想先筛选偶数,再对其平方,最后只保留大于 10 的结果。可以一次性完成:
auto result = nums |
std::views::filter([](int n){ return n % 2 == 0; }) | // 只取偶数
std::views::transform([](int n){ return n * n; }) | // 平方
std::views::filter([](int n){ return n > 10; }); // 只保留 > 10
for (int v : result) std::cout << v << ' '; // 输出 16 36 64 ...
整个流程以表达式形式写出,极大地提升可维护性。
5. 与容器互操作
虽然 ranges 主要针对容器,但也可以和自定义容器一起使用,只要满足 Range 的概念。下面示例演示自定义链表与 ranges 的结合:
#include <ranges>
#include <iostream>
struct Node {
int value;
Node* next = nullptr;
};
class LinkedList {
public:
using iterator = /* 需要自行实现 */;
// 只要满足 std::ranges::input_range 就能使用
// 这里简化省略完整实现
};
int main() {
LinkedList list;
// 填充数据...
// 通过 ranges 进行操作
for (int v : list | std::views::filter([](int n){ return n % 2 == 0; })) {
std::cout << v << ' ';
}
}
实现细节与容器无关,核心是让 LinkedList::iterator 满足输入迭代器的概念。
6. 性能与安全
- 延迟求值:
std::views在遍历时逐个产生元素,避免了临时容器的复制开销。 - 类型安全:
ranges的模板推导更严格,能在编译期捕获错误。例如,错误使用std::views::filter与非可调用对象会触发编译错误。 - 可组合性:视图可以自由组合,且不产生副作用,符合函数式编程的理念。
7. 如何快速上手?
- 更新编译器:确保使用支持 C++20 的编译器(GCC 10+, Clang 12+, MSVC 16.10+)。
- 头文件:仅需 `#include `。
- 从简单案例做起:先在小型程序中尝试
std::views::filter、std::views::transform等。 - 阅读标准:官方文档提供了完整 API 列表与使用示例。
- 逐步迁移:将已有的
std::copy_if、std::transform替换为对应的ranges版本,逐步完善。
8. 结语
std::ranges 是 C++20 重要的语言/库特性之一,借助它可以让代码更短、更易读、更安全。无论是对容器的直接操作,还是对自定义数据结构的使用,ranges 都提供了统一的、现代化的编程方式。建议从小项目开始实践,一旦熟悉后在更大规模代码中逐步推广,必将带来显著的开发效率提升。