在 C++20 之前,使用 STL 容器进行链式过滤、变换以及组合时,往往需要写一连串的 std::copy_if、std::transform 或者自己实现迭代器包装。C++20 的 ranges 库为这类操作提供了直观而高效的语法。下面通过一个完整的示例,展示如何利用 std::ranges 实现一个链式过滤、变换以及聚合的流程,并讨论其性能与可维护性优势。
1. 背景与需求
假设我们有一份用户数据,结构如下:
struct User {
int id;
std::string name;
int age;
double balance;
};
现在的业务需求是:
- 过滤出年龄在 18 岁以上且余额大于 1000 的用户。
- 将这些用户的姓名转换为大写。
- 计算这些用户的平均余额。
传统实现(C++14)大约需要 30 行代码,且每一步都需要显式的循环或算法调用。
2. C++20 ranges 的优势
- 表达式式语义:可以像写数学表达式一样写出链式操作。
- 懒执行:只有在最终需要结果时才真正执行,避免不必要的拷贝。
- 类型安全:编译器能在编译期检查大多数错误。
3. 示例代码
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
#include <numeric>
#include <cctype>
#include <string>
struct User {
int id;
std::string name;
int age;
double balance;
};
// 辅助函数:将字符串转换为大写
inline std::string to_upper(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return std::toupper(c); });
return s;
}
int main() {
// 初始化数据
std::vector <User> users = {
{1, "alice", 23, 1200.5},
{2, "bob", 17, 800.0},
{3, "carol", 35, 1500.0},
{4, "dave", 19, 950.0}
};
// 1. 过滤条件
auto filtered = users
| std::views::filter([](const User& u){
return u.age >= 18 && u.balance > 1000.0;
});
// 2. 变换:姓名大写
auto names = filtered
| std::views::transform([](const User& u){
return to_upper(u.name);
});
// 3. 输出姓名列表
std::cout << "符合条件的用户姓名(大写):\n";
for (const auto& name : names) {
std::cout << " " << name << '\n';
}
// 4. 计算平均余额(需要先提取余额)
auto balances = filtered
| std::views::transform([](const User& u){ return u.balance; });
double avg_balance = 0.0;
size_t count = 0;
for (double bal : balances) {
avg_balance += bal;
++count;
}
if (count) avg_balance /= count;
std::cout << "\n平均余额: " << avg_balance << '\n';
return 0;
}
代码说明
- 过滤:
std::views::filter接收一个 lambda,返回符合条件的子范围。 - 变换:
std::views::transform对每个元素应用一个转换函数。 - 遍历:使用范围 for 循环,内部会按需按元素产生。
- 聚合:为了计算平均值,先提取余额为一个新范围,然后手动累加。若想更简洁,可结合
std::ranges::accumulate或自定义累加器。
4. 性能与可读性
- 懒加载:过滤、变换都不会导致中间临时容器的生成。
- 单遍:在计算平均余额时,实际上只遍历一次。
- 可维护:每一步逻辑清晰、分离,修改过滤条件或变换方式只需改动对应 lambda。
5. 常见陷阱
- 视图是非持久的:
filtered、names、balances都是视图,不保存数据。若在后续使用中需要多次遍历,最好缓存为std::vector。 - 引用生命周期:若使用 lambda 捕获外部变量,请确保生命周期足够长。
6. 进一步扩展
- 使用
std::ranges::filter_view、std::ranges::transform_view的命名空间别名views,让代码更短。 - 引入
std::ranges::to(C++23)将视图转换为容器。 - 与
std::ranges::views::split结合,处理更复杂的数据流水线。
7. 结语
C++20 的 ranges 库以其简洁、懒执行与强类型安全,为链式数据处理提供了强大的工具。掌握 views::filter 与 views::transform 的组合使用,可以大幅提升代码的可读性与运行效率,是现代 C++ 开发者不可或缺的技能之一。