C++ 算法与 ranges:用 find_if、transform 写清数据处理意图
很多循环并不复杂,只是在寻找一个元素,或者把每个元素转换成另一种值。把这种意图交给标准算法,代码通常比手动维护下标更容易检查。
最低标准:C++20。本篇同时展示传统迭代器算法与 ranges 算法。
1. 查找符合条件的元素,再生成结果
cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
struct Item { std::string name; int price; };
int main() {
std::vector<Item> items{{"pen", 3}, {"book", 12}, {"bag", 35}};
auto found = std::find_if(items.begin(), items.end(),
[](const Item& x) { return x.price >= 10; });
if (found != items.end()) std::cout << found->name << '\n';
std::vector<int> doubled;
std::ranges::transform(items, std::back_inserter(doubled),
[](const Item& x) { return x.price * 2; });
for (int value : doubled) std::cout << value << ' ';
std::cout << '\n';
auto cheap = std::ranges::find_if(items,
[](int price) { return price < 5; }, &Item::price);
if (cheap != items.end()) std::cout << cheap->name << '\n';
}
输出依次为 book、6 24 70、pen。find_if 返回第一个满足条件的位置;找不到时返回结束迭代器,必须先判断再解引用。它不会把所有匹配项收集起来。
2. ranges 的投影让条件更专注
最后一次查找把 &Item::price 作为投影:算法先取得价格,再把价格交给条件函数。这样条件本身无需知道 Item 的其他字段。
ranges 版本通常可以直接接收容器,不必重复写 begin/end。不过"写法更短"不意味着生命周期问题消失。不要长期保存容器失效后的迭代器;对某些临时范围调用算法时,返回类型会使用 dangling 来阻止错误借用。
3. transform 不会自动扩大普通目标容器
若把空 vector 的 begin 作为输出位置,算法不会自动插入元素。应先 resize 到足够长度,或像示例一样使用 back_inserter。reserve 只预留容量,不会增加 size,不能替代 resize。
输入和输出的关系也要遵守算法约定:需要原地转换时,可以把输出起点设为输入起点;不要随意使用错位重叠区间。转换过程中也不应通过副作用改变正在遍历的容器结构。
4. 算法表达意图,普通循环表达流程
"找第一个""逐个转换""判断是否全部满足"很适合算法;如果每一步都有复杂状态迁移、多个退出条件和错误恢复,清晰的普通循环可能更合适。
把判断函数写成无意外副作用的小函数,避免假设 transform 的调用顺序可以承担业务流程。最终目标不是消灭 for,而是让读者一眼看出代码在做什么。
进一步阅读:Microsoft 标准算法参考。